Cef功能開發經驗總結

轉載請說明原出處,謝謝~~:http://blog.csdn.net/zhuhongshu/article/details/70159672

這是我開發Cef功能時對踩過的坑,進行的總結,話說Cef坑真的不少。好在踩完後用起來還是挺爽的。最終的代碼可以下載網易雲信PC Demo C++源碼點我跳轉

資料準備

這是我集成過程中查到的一些資料,包括了Cef開發的各方面資料

在調試Cef時需要Cef的pdb和源碼:

Cef基本結構

CefApp接口

CefApp接口提供了不同進程的可定製回調函數,每一個進程對應一個CefApp接口。CefBrowserProcessHandler對應瀏覽器進程的回調,CefRenderProcessHandler對應渲染進程的回調。我們應該繼承CefApp、CefBrowserProcessHandler、CefRenderProcessHandler接口。如果完全使用多進程模式,可以分別在瀏覽器進程和渲染進程裏分開繼承接口

CefApp::OnBeforeCommandLineProcessing方法裏可以附加傳入給Cef的命令行參數,這裏可以附加很多控制參數

CefRenderProcessHandler::OnWebKitInitialized方法可以在渲染進程初始化時用來註冊JS擴展代碼,實現C++與JS交互

CefRenderProcessHandler::OnFocusedNodeChanged方法可以檢測當前獲取到焦點html元素,獲取到一些元素信息可以通過進程通信發送給瀏覽器進程來輔助做進一步的判斷

CefRenderProcessHandler::OnProcessMessageReceived方法用於接收瀏覽器進程發來的消息,在做C++與JS交互時會用到

CefClient接口

每一個CefBrowser對象會對應一個CefClient接口,用於處理瀏覽器頁面的各種回調信息,包括了Browser的生命週期,右鍵菜單,對話框,狀態通知顯示,下載事件,拖曳事件,焦點事件,鍵盤事件,離屏渲染事件。隨着Cef版本的更新這些接口也會擴展和更新,多數對Cef進行行爲控制的方法都集中在這些接口,如果對Cef有新的功能需求,一般都可以先翻翻這些接口中有沒有提供相關功能

CefClient::OnProcessMessageReceived方法用於接收渲染進程發到的消息,在做C++與JS交互時會用到

CefSettings結構體

CefSettings結構體定義了Cef的全局配置信息,比如指定單進程模式、指定渲染子進程路徑、設置localstorage路徑、設置日誌等級、Cef資源文件路徑。其中對於項目最重要的字段是single_process、multi_threaded_message_loop、windowless_rendering_enabled,分別用於指定單進程模式、多線程渲染模式、離屏渲染模式。

兼容現有的消息循環

如果是UI線程消息循環構架較簡單的項目,可以直接調用CefRunMessageLoop來使用Cef自帶的消息循環,它會阻塞線程直到調用了CefQuitMessageLoop函數,CefRunMessageLoop是兼容傳統的Win32消息循環的。

不過NIM項目底層是使用谷歌base庫的多線程構架,所以沒法直接使用CefRunMessageLoop。(PS:實際上Cef的底層消息循環也是谷歌的base庫)

要讓NIM的消息循環兼容Cef消息循環,有兩種方法。

第一種方法

第一種方法是使用CefDoMessageLoopWork函數代替CefRunMessageLoop來完全消息消息循環。CefDoMessageLoopWork函數的作用是讓Cef執行一次消息循環,這個函數不會阻塞線程,所以需要在我們現有的消息循環裏的適當情況下主動去調用CefDoMessageLoopWork函數,如果調用的太頻繁會很消耗CPU,如果調用頻率太低會導致Cef來不及處理內部消息,讓Cef界面反映變慢,所以這個函數的調用時機很重要。

因爲CefDoMessageLoopWork函數應該在原本的消息循環中調用,而base庫的UI線程消息循環是封裝好的。這裏首先說一下定製base庫消息循環的方法。在WinMain入口函數裏調用UI消息循環的代碼如下:

{
    MainThread thread; // 創建主線程
    thread.RunOnCurrentThreadWithLoop(nbase::MessageLoop::kUIMessageLoop); // 執行主線程循環
}

在RunOnCurrentThreadWithLoop方法的第二個參數裏可以指定一個消息分派器指針dispatcher,dispatcher繼承自nbase::Dispatcher。base庫中的UI消息循環代碼如下:

PreProcessMessage(msg);

if (state_->dispatcher)
{
    if (!state_->dispatcher->Dispatch(msg))
        state_->should_quit = true;
}
else
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

PostProcessMessage(msg);

如果我們指定了RunOnCurrentThreadWithLoop方法的第二個參數,就不會調用原本的消息循環了,所以可以在這個dispatcher裏定製消息循環。我實現CefMessageLoopDispatcher類並重寫Dispatch接口。

BOOL CefMessageLoopDispatcher::IsIdleMessage(const MSG* pMsg)
{
    switch (pMsg->message)
    {
    case WM_MOUSEMOVE:
    case WM_NCMOUSEMOVE:
    case WM_PAINT:
        return FALSE;
    }

    return TRUE;
}

bool CefMessageLoopDispatcher::Dispatch(const MSG &msg)
{
    static BOOL bDoIdle = TRUE;

    TranslateMessage(&msg);
    DispatchMessage(&msg);

    if (IsIdleMessage(&msg))
    {
        bDoIdle = TRUE;
    }

    while (bDoIdle && !::PeekMessage(const_cast<MSG*>(&msg), NULL, 0, 0, PM_NOREMOVE))
    {
        CefDoMessageLoopWork();
        bDoIdle = FALSE;
    }

    return true;
}

在定製消息循環裏,如果判斷當前消息隊列爲空並且剛纔處理的消息不會指定的幾個消息,就去調用CefDoMessageLoopWork函數。WM_PAINT、WM_MOUSEMOVE等消息的處理比較複雜,所以不在這裏調用CefDoMessageLoopWork函數

這個方法基本可以使用,但是還存在一些問題,這裏CefDoMessageLoopWork函數的調用機制還不夠好,Cef界面不夠順暢,而且因爲Cef與項目base庫的衝突,導致在程序結束時有些問題。這個方法有待優化

第二種方法

CefSettings結構體的multi_threaded_message_loop(多線程消息循環)爲false時,可以調用CefRunMessageLoop或者CefDoMessageLoopWork函數來觸發Cef消息循環,這時瀏覽器進程的UI線程就是調用CefRunMessageLoop或者CefDoMessageLoopWork函數的線程。如果CefSettings結構體的multi_threaded_message_loop爲true時。瀏覽器進程的UI線程是另外的線程。設置multi_threaded_message_loop爲true則使用多線程消息循環。

通過對比Cef Demo的多線程消息循環代碼,可以確定在NIM項目中直接開啓多線程消息循環,不需要修改現有消息循環代碼就可以正常使用Cef了。不過需要注意的是,使用多線程消息循環後某些函數就無法使用了,比如CreateBrowserSync,這種函數要求必須在Cef的UI線程調用。

另外,在很多版本的Cef裏,如果開啓了多線程消息循環,會導致程序在結束時觸發中斷,這屬於Cef的bug,不過在release版本的Cef中沒有問題。應該在項目中使用這個方法。不過使用了多線程消息循環後,很多Cef對象觸發的回調函數,都是在Cef的UI線程而不是我們的UI線程,所以這時操作我們的UI線程就比較麻煩,要注意一些多線程問題,儘量把操作轉發到我們的UI線程,不轉發的話必須確定所操作的代碼不會影響我們的UI線程,切記!

CefClient接口介紹

CefLifeSpanHandler

CefBrowser對象的生命週期事件的回調接口。

  • OnAfterCreated:當調用CreateBrowser函數創建瀏覽器對象後會立馬觸發這個回調,在這裏可以保存瀏覽器對象的指針
  • DoClose:當調用CloseBrowser函數後觸發這個回調
  • OnBeforeClose:當瀏覽器對象即將銷燬時會觸發這個回調,在這裏一定要釋放所有對CefBrowser對象的引用,否則會導致程序無法退出。切記這個坑。
  • OnBeforePopup:當單擊了網頁中會彈出新窗口的鏈接時,會觸發這個回調。我們的項目裏應該禁止新窗口的彈出,而在原控件中跳轉鏈接

CefRenderHandler

要使用離屏渲染功能,就必須要實現這個接口類。因爲項目中使用的duilib庫,目前使用分層窗體機制實現異形窗體效果,不支持顯示子窗口只能自繪控件,所以沒法使用比較簡單的子窗口的形式顯示Cef瀏覽器對象。只能使用離屏渲染方法,離屏渲染的數據會通過CefRenderHandler接口回調。

首先必須要開啓CefSettings結構體的windowless_rendering_enabled字段

  • GetRootScreenRect:瀏覽器對象創建後觸發的回調,返回最外層窗體在屏幕中的位置
  • GetViewRect:在瀏覽器對象初始化後,或者瀏覽器大小改變時,觸發這個回調來獲取瀏覽器對象的位置。因爲瀏覽器對象會平鋪滿整個控件,所以這裏返回控件的位置。其中返回的左上位置要準確,否則Cef在處理一些座標信息時會出錯
  • GetScreenPoint:在這裏把傳入的座標值,由客戶區座標轉換爲屏幕座標
  • OnCursorChange:當需要修改鼠標光標時觸發這個回調
  • OnPaint:當瀏覽器對象有新的渲染數據後,會觸發這個回調,包含了髒區和渲染數據。應該保存這些數據,然後在適當的時候貼到目標窗體上
  • OnPopupShow:當瀏覽器中要彈出內部對話框時(比如彈出一個下拉菜單),觸發這個回調,通知要顯示或者隱藏彈出框
  • OnPopupSize:當瀏覽器中要彈出內部對話框時,觸發這個回調,通知彈出框的位置和大小

離屏渲染的實現

離屏渲染的效率不如真窗口渲染,如果不是必須要離屏渲染的情況,還是用真窗口比較好。CefControl控件實現了duilib嵌入Cef瀏覽器對象。

在控件初始化觸發Init函數時,調用CreateBrowser函數創建CefBrowser對象,這會觸發CefRenderHandler::GetViewRect回調,在這個回調裏返回控件的位置。隨後網頁第一次渲染時觸發CefRenderHandler::OnPaint回調。

繪製渲染數據的流程

  1. 當網頁渲染數據改變、或者我們主動調用了CefBrowser對象的Invalidate方法時,會觸發CefRenderHandler::OnPaint回調。
  2. 我寫了一個內存位圖緩衝類MemoryDC來保存Cef傳來的渲染數據,在CefControl控件中dc_cef_成員變量負責保存渲染數據。在CefRenderHandler::OnPaint回調裏,根據渲染數據初始化dc_cef_,然後根據髒區把渲染數據拷貝到dc_cef_中。
  3. 數據拷貝完之後,調用CefControl控件的Invalidate方法通知窗體重繪控件
  4. 在CefControl控件的Paint方法裏,把dc_cef_的位圖數據拷貝到duilib傳入的HDC中

CefControl對事件的處理

修改CefBrowser尺寸
在離屏渲染模式下,無法直接修改CefBrowser對象的尺寸。CefControl控件重寫SetPos函數,在這裏調用CefBrowser對象的WasResized接口通知CefBrowser對象需要改變尺寸,之後GetViewRect接口會被觸發,這時依然是返回CefControl控件的位置就可以了。之後OnPaint接口會被自動觸發,按照前一節的流程進行一次渲染數據的刷新

設置CefBrowser隱藏(顯示)
CefControl控件重寫SetVisible函數和SetInternVisible函數,在這裏調用CefBrowser對象的WasHidden接口通知CefBrowser對象隱藏或顯示

對系統消息的處理

  1. 在控件初始化觸發Init函數時,調用窗體類的AddMessageFilter函數把自己註冊到窗體的消息過濾隊列裏。CefControl控件繼承IUIMessageFilter接口類並重寫MessageHandler函數。當系統消息進入窗體後會依次調用消息過濾隊列指針來過濾消息。在MessageHandler函數裏處理我們感興趣的消息,其他消息並不過濾
  2. 處理各種鼠標類消息時,判斷如果鼠標不在控件範圍內則不處理相關消息。獲取當前鼠標的座標,因爲CefBrowser的座標值是以自身左上角作爲原點的,所以獲取的鼠標座標要減去CefControl控件的左上角座標值。其中處理ButtonDown、ButtonUp、MouseMove消息時,不會中斷消息繼續傳遞給窗體,這裏需要讓duilib窗體類處理SetCapture、ReleaseCapture等函數
  3. 處理鍵盤消息時,判斷當前控件是否獲取焦點,只處理有焦點的情況
  4. WM_SETCURSOR消息處理,在MessageHandler函數攔截WM_SETCURSOR消息,直接調用窗體類的默認消息處理函數,不讓duilib處理這個消息。CefRenderHandler::OnCursorChange接口會修改鼠標光標並修改窗體的默認光標樣式,而duilib處理WM_SETCURSOR消息時會另外修改光標,所以需要攔截

渲染Popup彈出框

  1. 瀏覽器中,彈出框的渲染數據是需要自己額外處理的。如下拉菜單等彈出框,否則瀏覽器中不會顯示出彈出框
  2. 當需要顯示彈出框時,CefRenderHandler::OnPopupSize接口會傳入彈出框的位置和尺寸等數據,在這裏把數據保存到rect_popup_成員變量
  3. 之後會觸發CefRenderHandler::OnPaint回調,並且渲染類型會被指定爲彈出框類型PET_POPUP。這時把彈出框的渲染數據保存到MemoryDC類型的dc_cef_popup_成員變量中
  4. 數據拷貝完之後,調用CefControl控件的Invalidate方法通知窗體重繪控件
  5. 在CefControl控件的Paint方法裏,把dc_cef_popup_的位圖數據按照rect_popup_的信息拷貝到duilib傳入的HDC中
  6. 當彈出框消失時,觸發CefRenderHandler::OnPopupShow接口,這裏重置rect_popup_的信息,並且通知CefBrowser刷新頁面

多進程渲染

Cef3支持多進程和單進程渲染,但是單進程渲染不夠穩定,只應該在Debug模式下作爲調試目的使用。在Cef3.1916等好幾個版本中,調試狀態下使用單進程模式,當程序初始化或者退出時,會觸發中斷。但是在多進程模式下沒有問題。官方也明確說明不推薦使用單進程模式

CefManager類實現了Cef3的初始化和銷燬功能。初始化函數Initialize裏調用的CefExecuteProcess函數會檢測當前的進程類型,如果是瀏覽器進程則函數會直接返回,在其他進程的話這個函數會阻塞直接進程銷燬。

ClientApp類繼承CefBrowserProcessHandler和CefRenderProcessHandler,可以同時處理瀏覽器進程和渲染進程的消息。原本多進程模式中,瀏覽器進程和渲染進程可以同用一個程序。但是由於我們的主程序的代碼比較複雜,如果讓主程序多開進程的話,會佔用較多的內存和CPU,同時觸發不必要的問題。所以專門另寫了一個cef_render項目來作爲渲染子進程

cef_render項目代碼比較簡單,主要代碼都是繼承CefRenderProcessHandler接口的CefRenderProcessHandler類。考慮到代碼周全,以後可以在cef_render項目補充一些崩潰Dump處理等代碼。務必要保證主程序的CefRenderProcessHandler接口實現代碼與cef_render程序的CefRenderProcessHandler接口實現代一致。否則單進程和多進程模式下會出現不同的處理結果

在瀏覽器進程啓動時,通過附加參數可以指定渲染子進程的路徑

command_line->AppendSwitchWithValue("browser-subprocess-path", "render.exe");

C++與JS交互

C++調用JS

在browser進程和render進程都可以直接執行JS代碼,直接調用CefFrame對象的ExecuteJavaScript方法就可以

JS調用C++

網頁中的一些JS回調和對網頁的JS擴展,都必須在渲染進程操作。讓JS調用C++的方法有三個,http://www.cnblogs.com/guolixiucai/p/4943748.html裏面介紹了兩種,https://github.com/fanfeilong/cefutil裏面是更復雜更強的第三種。

我們項目裏,只需要給JS開放一個函數接口,而且接口並不複雜,所以直接採用JS擴展的方法註冊JS回調函數就可以。

在CefRenderProcessHandler::OnWebKitInitialized接口裏,註冊JS擴展代碼

std::string extensionCode =
    "(function() {"
    "  CefTestWebFunction = function(param) {"
    "    native function CefTestWebFunction(param);"
    "    return CefTestWebFunction(param);"
    "    };"
    "})();";

CefRefPtr<CefV8Handler> handler = new CefJSHandler();
CefRegisterExtension("v8/extern", extensionCode, handler);

CefRegisterExtension函數會執行擴展代碼。網上例子都是創建一個全局對象,然後把JS函數和變量綁定到這個對象上。這裏直接申明一個FunExternal的全局函數。當JS代碼中調用FunExternal函數時,會根據native關鍵字後的函數名,去通知C++代碼調用對應的native函數

CefJSHandler類繼承CefV8Handler接口並實現Execute方法,在CefRegisterExtension傳入CefJSHandle指針,當JS代碼需要調用native函數時會,會主動觸發CefJSHandler::Execute方法

bool CefJSHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
{
    if (name == "CefTestWebFunction" && arguments.size() == 1)
    {
        for (auto &it : arguments)
        {
            if (it->IsString())
            {
                CefString param = it->GetStringValue();

                CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
                CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kJsCallbackMessage);

                message->GetArgumentList()->SetString(0, name);
                message->GetArgumentList()->SetString(1, param);
                browser->SendProcessMessage(PID_BROWSER, message);

                retval = CefV8Value::CreateBool(true);
            }
        }
        return true;
    }

    // Function does not exist.
    return false;
}

在這裏可以獲取到JS要調用的函數名,以及傳入的參數等信息。獲取到這些信息後,把他們包裝爲CefProcessMessage結構,通過IPC把信息發送到Browser進程進行異步處理。調用SendProcessMessage方法把信息發送到Browser進程

瀏覽器進程的CefClient::OnProcessMessageReceived方法接收到Render進程發來的消息。Browser進程處理消息後,可以通過C++調用JS的方法去通知Web端消息處理結果

進程結束的流程

https://github.com/fanfeilong/cefutil/blob/master/doc/CEF_Close.md裏有完整的Cef結束流程分析和處理代碼,不過由於我們的項目不單單隻有Cef組件,而且使用場景和Cef Demo中的不一樣,所以採用了不同的關閉流程

CefControl控件的銷燬流程

  1. 在CefControl控件的析構函數裏調用CloseBrowser(true)方法通知瀏覽器對象要關閉
  2. BrowserHandler::DoClose接口被觸發,這裏不需要做額外處理,直接返回就可以
  3. 之後BrowserHandler::OnBeforeClose接口被觸發,在這裏一定要釋放所有對CefBrowser對象的引用,否則會導致程序無法退出。

進程退出流程

  1. 用戶單擊右下角托盤的退出菜單項
  2. 觸發到LoginCallback::DoLogout函數,在這裏會調用到代碼nim_comp:: WindowsManager::GetInstance()->DestroyAllWindows();,這裏銷燬所有的窗體,所有控件被銷燬,自然就會觸發所有CefControl控件的銷燬流程,所有瀏覽器對象被關閉
  3. LoginCallback::DoLogout函數裏之後會調用到UILogoutCallback函數,這裏原本會調用PostQuitMessage(0)函數結束消息循環,但是我們應該等待所有瀏覽器對象關閉後在結束消息循環,否則會發生錯誤。而CefBrowser的關閉是異步的,所以無法保證調用UILogoutCallback函數時所有CefBrowser被關閉
  4. 我在CefManager類實現PostQuitMessage函數,在這裏等待所有CefBrowser關閉後再結束消息循環
  5. 程序正常結束

CefManager::PostQuitMessage函數裏判斷當前瀏覽器對象的數量來決定是否退出消息循環,如果還有瀏覽器對象沒有關閉就等待500毫秒後再檢測:

void CefManager::PostQuitMessage(int nExitCode)
{
    if (browser_count_ == 0)
    {
        Post2UI([nExitCode]()
        {
            ::PostQuitMessage(nExitCode);
        });
    }
    else
    {
        auto cb = [nExitCode]()
        {
            CefManager::GetInstance()->PostQuitMessage(nExitCode);
        };

        nbase::ThreadManager::PostDelayedTask(kThreadUI, cb, nbase::TimeDelta::FromMilliseconds(500));
    }
}

把Cef組件集成到雲信NIM項目

  1. tool_kits\cef目錄中寫好的Cef模塊組件拷貝到自己項目的對應目錄,並且添加到解決方案中。其中cef_render項目是Cef渲染子進程,是一個獨立的exe;libcef_dll_wrapper項目是Cef導出的C語言接口的C++包裝類;cef_module項目是核心封裝代碼,把cef功能封裝爲可以在nim demo中直接使用的類,其中包含了對cef功能進行管理的CefManager和CefControl、CefNativeControl兩個控件等。
  2. nim_win_demo\gui\cef目錄的源文件拷貝都自己項目的某個目錄,這裏面CefControl、CefNativeControl兩個控件的測試窗口代碼,可有可無
  3. 進入libs目錄,解壓cef_sandbox.rar壓縮包並把cef_sandbox.lib、cef_sandbox_d.lib文件放到libs目錄;進入libs\x64目錄,解壓cef_sandbox.rar壓縮包並把cef_sandbox.lib、cef_sandbox_d.lib文件放到libs\x64目錄。這裏面是編譯cef組件時,爲cef模塊增加sandbox功能的靜態庫。bin\cef目錄是cef模塊依賴的cef相關dll。主程序初始化時會從bin\cef目錄加載cef所需dll
  4. 配置nim_demo項目屬性,在鏈接器\輸入\延遲加載的DLL中加入libcef.dll;libEGL.dll;libGLESv2.dll(因爲我們把cef所需的dll都放到bin\cef目錄了,這樣不會導致目錄混亂,但是爲了順利加載cef dll,需要延遲加載;如果不想用延遲加載,就把bin\cef目錄的dll都直接放到bin目錄)
  5. WinMain函數中第一句加入(用於延遲加載cef dll,如果不延遲加載,則不需要這句)
    nim_ui:: InitManager::GetInstance()->AddCefDllToPath();
    在開始雲信組件初始化之前加入如下代碼用於初始化cef功能(一定要在雲信組件初始化之前)
    if (!nim_cef::CefManager::GetInstance()->Initialize(true)
    return 0;
    在開始UI線程消息循環之後加入如下代碼用於清理cef功能
    nim_ui::InitManager::GetInstance()->CleanupUiKit();

  6. 找到原項目中調用::PostQuitMessage函數的地方,修改爲nim_cef:: CefManager::GetInstance()->PostQuitMessage(0);

  7. 其他配置如果有疑問可以參見Cef Nim Demo的配置

讓Cef支持Flash、mp3、mp4

現在附帶的nim demo中使用的cef相關dll是專門下載了cef源碼增加mp3、mp4功能後重新編譯的(在Windows下編譯Cef3.2623並加入mp3、mp4支持(附帶源碼包和最終DLL) )。所以如果使用我提供的dll,可以直接支持mp3、mp4播放(官方直接下載的cef不支持)。如果對cef功能有其他需求的話請自行下載編譯cef並替換demo中的dll

demo中附帶的dll都是release版本,沒有附帶debug版本

cef_module項目中已經默認支持flash播放,bin\cef\PepperFlash目錄中附帶了支持flash播放所需的dll,如果不需要flash功能,可以刪除這個目錄

cef_module項目中提供了兩個控件來展示cef瀏覽器,分別爲CefControl、CefNativeControl,CefControl用於離屏渲染模式,CefNativeControl用於真窗口模式,根據需求來選擇使用這兩個控件的一個。離屏渲染模式的話控件自己控制瀏覽器的渲染,所以可以與nim duilib結合的更完美,支持透明異形窗體;真窗口模式因爲Cef需要依託一個子窗口,由Cef自己渲染,所以無法支持透明異形窗體。對於絕大多數需求,使用離屏渲染模式的CefControl更好,因爲與duilib結合更完美。但是如果網頁的內容刷新非常頻繁(尤其是用於播放Flash時),應該使用真窗口模式,否則Flash播放導致的頻繁繪製操作會讓程序的CPU佔用率飆升!

我們的代碼默認是開啓離屏渲染模式的,如果有播放Flash的需求或者其他瀏覽器畫面頻繁的需求時,應該關閉離屏渲染模式而使用真窗口模式,關於方法時Winmain函數中初始化cef功能時參數傳入falsenim_cef:: CefManager::GetInstance()->Initialize(false)。另外我們的duilib窗口默認是使用支持透明異形的分層窗口,是不支持子窗口的,所以如果使用cef的真窗口模式,那麼應該關閉duilib窗口的分層窗口樣式,關閉方法是創建窗口的Window::Create函數的第五個參數isLayeredWindow設置爲false。demo中CefForm、CefNativeForm這兩個窗體類分別用於演示離屏渲染模式(對應CefControl控件)和真窗口模式(對應CefNativeControl控件)的功能。這兩個窗口的創建代碼在MainForm::OnClicked中有演示代碼

禁用Cef模塊功能

cef_module項目中預處理宏中增加了兩個控件Cef模塊功能的宏SUPPORT_CEF、SUPPORT_CEF_FLASHSUPPORT_CEF宏控制是否啓用cef功能,SUPPORT_CEF_FLASH控制cef是否支持flash播放功能(只有SUPPORT_CEF宏啓用時這個宏纔有效)。

如果不需要cef帶來的瀏覽器功能,可以在cef_module項目中去掉SUPPORT_CEF宏,這樣cef相關的功能就被禁用。同時*bin\cef*目錄就可以刪除掉而不影響程序運行。

如果需要cef功能但是並不需要flash功能,可以在cef_module項目中去掉SUPPORT_CEF_FLASH宏。同時bin\cef\PepperFlash**目錄可以刪除掉、**libs目錄的cef_sandbox.lib、cef_sandbox_d.lib文件也可以刪掉。

在開啓了Flash功能後,要在編譯時加入cef_sandbox.lib等靜態庫,否則在使用flash功能時會有一個黑框彈出(這輸入cef的bug),關於這個bug的其他解決辦法,詳見:解決cef加載flash時彈出黑框的問題。同時程序將無法通過附加參數指定渲染子進程(此時必須用主進程exe來做渲染子進程),這時也就不需要cef_render項目編譯的render.exe了。如果禁用Flash功能,則會讓render.exe來作爲渲染子進程

集成過程中遇到的其他坑

通過這些時間用Cef,發現坑其實不少,而且各個版本的坑不一樣。

  1. multi_threaded_message_loop導致中斷:在2623、2526版本,Debug模式中,如果開啓了multi_threaded_message_loop,當程序退出時,必定會觸發中斷。這個屬於Cef的bug,在官方demo中也有這個問題,但是在Release模式中是沒有問題的。
  2. 2357版本在程序處理重定向信息後,會導致渲染進程崩潰,這個版本無法用於項目
  3. 1916版本各個功能使用正常。但是在在Debug模式下某些網頁打開時會出中斷警告(但並不是錯誤),可能是因爲對新html標準支持不夠;Debug模式下單進程模式在退出時會觸發中斷。但是在Release模式下都正常使用
  4. 設置CefSettings的cache_path字段(也就是LocalStorage),一定要注意不要在路徑末尾添加”\\”,否則會觸發中斷
  5. 在多進程模式下,必須設置子進程的程序名(不管是使用原程序作爲子進程,還是單獨一個程序作爲子進程)。當Cef調用LoadUrl函數加載網頁時,會查找子進程的絕對路徑去啓動渲染進程,如果不設置子進程名字,會導致查路徑找發生錯誤,導致VS在Debug模式下卡死
  6. 如果開發者不負責Cef相關功能的開發,可以修改CefManager::AddCefDllToPath函數的代碼,讓Cef不管在Debug模式還是Release模式下都使用Release版本的Cef Dll文件。這樣做不會發生錯誤,而且上面提到的多數坑都不會被觸發
  7. 如果在使用Cef模塊中遇到一些崩潰或者其他異常現象,請先使用release模式+開啓多進程模式再運行一次,很多問題都是debug模式或者單進程模式導致的
  8. 如果使用flash功能,就需要在編譯時加入cef_sandbox.lib等靜態庫,否則在使用flash功能時會有一個黑框彈出(這輸入cef的bug)。而加入sandbox功能後,在某些電腦上離屏渲染功能就無法順利創建子進程,導致沒有畫面。這時就要在子進程創建前檢查命令行參數,發現不是flash進程就增加no-sandbox參數來關閉sandbox功能
  9. 發現一個非常奇葩的bug,離屏渲染+多線程消息循環模式下,在瀏覽器對象上右擊彈出菜單,是無法正常關閉的。翻cef源碼後發現菜單是用TrackPopupMenu函數創建的,在MSDN資料上查看後發現調用TrackPopupMenu前需要給其父窗口調用SetForegroundWindow,但是在cef源碼中沒有調用。最終翻cef源碼後得到的解決方法是在cef的UI線程創建一個窗口,這個窗體的父窗口必須是在主程序UI線程創建的,這樣操作之後就不會出現菜單無法關閉的bug了,雖然不知道爲什麼但是bug解決了

總結

離屏渲染+異形窗體

CefWindowInfo::SetAsWindowless的transparent參數必須設置爲true!
離屏渲染+異形窗體

Flash播放效果

Flash播放效果

Redrain QQ:491646717 2017.4.13

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章