天天看點

Cef功能開發經驗總結

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

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

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

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

CefRenderProcessHandler::OnWebKitInitialized方法可以在渲染程序初始化時用來注冊JS擴充代碼,實作C++與JS互動

CefRenderProcessHandler::OnFocusedNodeChanged方法可以檢測目前擷取到焦點html元素,擷取到一些元素資訊可以通過程序通信發送給浏覽器程序來輔助做進一步的判斷

CefRenderProcessHandler::OnProcessMessageReceived方法用于接收浏覽器程序發來的消息,在做C++與JS互動時會用到

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

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

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消息循環的代碼如下:

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

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

在定制消息循環裡,如果判斷目前消息隊列為空并且剛才處理的消息不會指定的幾個消息,就去調用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線程,切記!

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

OnAfterCreated:當調用CreateBrowser函數建立浏覽器對象後會立馬觸發這個回調,在這裡可以儲存浏覽器對象的指針

DoClose:當調用CloseBrowser函數後觸發這個回調

OnBeforeClose:當浏覽器對象即将銷毀時會觸發這個回調,在這裡一定要釋放所有對CefBrowser對象的引用,否則會導緻程式無法退出。切記這個坑。

OnBeforePopup:當單擊了網頁中會彈出新視窗的連結時,會觸發這個回調。我們的項目裡應該禁止新視窗的彈出,而在原控件中跳轉連結

要使用離屏渲染功能,就必須要實作這個接口類。因為項目中使用的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回調。

當網頁渲染資料改變、或者我們主動調用了CefBrowser對象的Invalidate方法時,會觸發CefRenderHandler::OnPaint回調。

我寫了一個記憶體位圖緩沖類MemoryDC來儲存Cef傳來的渲染資料,在CefControl控件中dc_cef_成員變量負責儲存渲染資料。在CefRenderHandler::OnPaint回調裡,根據渲染資料初始化dc_cef_,然後根據髒區把渲染資料拷貝到dc_cef_中。

資料拷貝完之後,調用CefControl控件的Invalidate方法通知窗體重繪控件

在CefControl控件的Paint方法裡,把dc_cef_的位圖資料拷貝到duilib傳入的HDC中

修改CefBrowser尺寸

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

設定CefBrowser隐藏(顯示)

CefControl控件重寫SetVisible函數和SetInternVisible函數,在這裡調用CefBrowser對象的WasHidden接口通知CefBrowser對象隐藏或顯示

在控件初始化觸發Init函數時,調用窗體類的AddMessageFilter函數把自己注冊到窗體的消息過濾隊列裡。CefControl控件繼承IUIMessageFilter接口類并重寫MessageHandler函數。當系統消息進入窗體後會依次調用消息過濾隊列指針來過濾消息。在MessageHandler函數裡處理我們感興趣的消息,其他消息并不過濾

處理各種滑鼠類消息時,判斷如果滑鼠不在控件範圍内則不處理相關消息。擷取目前滑鼠的坐标,因為CefBrowser的坐标值是以自身左上角作為原點的,是以擷取的滑鼠坐标要減去CefControl控件的左上角坐标值。其中處理ButtonDown、ButtonUp、MouseMove消息時,不會中斷消息繼續傳遞給窗體,這裡需要讓duilib窗體類處理SetCapture、ReleaseCapture等函數

處理鍵盤消息時,判斷目前控件是否擷取焦點,隻處理有焦點的情況

WM_SETCURSOR消息處理,在MessageHandler函數攔截WM_SETCURSOR消息,直接調用窗體類的預設消息處理函數,不讓duilib處理這個消息。CefRenderHandler::OnCursorChange接口會修改滑鼠光标并修改窗體的預設光标樣式,而duilib處理WM_SETCURSOR消息時會另外修改光标,是以需要攔截

浏覽器中,彈出框的渲染資料是需要自己額外處理的。如下拉菜單等彈出框,否則浏覽器中不會顯示出彈出框

當需要顯示彈出框時,CefRenderHandler::OnPopupSize接口會傳入彈出框的位置和尺寸等資料,在這裡把資料儲存到rect_popup_成員變量

之後會觸發CefRenderHandler::OnPaint回調,并且渲染類型會被指定為彈出框類型PET_POPUP。這時把彈出框的渲染資料儲存到MemoryDC類型的dc_cef_popup_成員變量中

在CefControl控件的Paint方法裡,把dc_cef_popup_的位圖資料按照rect_popup_的資訊拷貝到duilib傳入的HDC中

當彈出框消失時,觸發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接口實作代一緻。否則單程序和多程序模式下會出現不同的處理結果

在浏覽器程序啟動時,通過附加參數可以指定渲染子程序的路徑

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

我們項目裡,隻需要給JS開放一個函數接口,而且接口并不複雜,是以直接采用JS擴充的方法注冊JS回調函數就可以。

在CefRenderProcessHandler::OnWebKitInitialized接口裡,注冊JS擴充代碼

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

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

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

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

在CefControl控件的析構函數裡調用CloseBrowser(true)方法通知浏覽器對象要關閉

BrowserHandler::DoClose接口被觸發,這裡不需要做額外處理,直接傳回就可以

之後BrowserHandler::OnBeforeClose接口被觸發,在這裡一定要釋放所有對CefBrowser對象的引用,否則會導緻程式無法退出。

使用者單擊右下角托盤的退出菜單項

觸發到LoginCallback::DoLogout函數,在這裡會調用到代碼nim_comp:: WindowsManager::GetInstance()->DestroyAllWindows();,這裡銷毀所有的窗體,所有控件被銷毀,自然就會觸發所有CefControl控件的銷毀流程,所有浏覽器對象被關閉

LoginCallback::DoLogout函數裡之後會調用到UILogoutCallback函數,這裡原本會調用PostQuitMessage(0)函數結束消息循環,但是我們應該等待所有浏覽器對象關閉後在結束消息循環,否則會發生錯誤。而CefBrowser的關閉是異步的,是以無法保證調用UILogoutCallback函數時所有CefBrowser被關閉

我在CefManager類實作PostQuitMessage函數,在這裡等待所有CefBrowser關閉後再結束消息循環

程式正常結束

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

把tool_kits\cef目錄中寫好的Cef子產品元件拷貝到自己項目的對應目錄,并且添加到解決方案中。其中cef_render項目是Cef渲染子程序,是一個獨立的exe;libcef_dll_wrapper項目是Cef導出的C語言接口的C++包裝類;cef_module項目是核心封裝代碼,把cef功能封裝為可以在nim demo中直接使用的類,其中包含了對cef功能進行管理的CefManager和CefControl、CefNativeControl兩個控件等。

把nim_win_demo\gui\cef目錄的源檔案拷貝都自己項目的某個目錄,這裡面CefControl、CefNativeControl兩個控件的測試視窗代碼,可有可無

進入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

配置nim_demo項目屬性,在連結器\輸入\延遲加載的DLL中加入libcef.dll;libEGL.dll;libGLESv2.dll(因為我們把cef所需的dll都放到bin\cef目錄了,這樣不會導緻目錄混亂,但是為了順利加載cef dll,需要延遲加載;如果不想用延遲加載,就把bin\cef目錄的dll都直接放到bin目錄)

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();

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

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

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功能時參數傳入false,nim_cef:: CefManager::GetInstance()->Initialize(false)。另外我們的duilib視窗預設是使用支援透明異形的分層視窗,是不支援子視窗的,是以如果使用cef的真視窗模式,那麼應該關閉duilib視窗的分層視窗樣式,關閉方法是建立視窗的Window::Create函數的第五個參數isLayeredWindow設定為false。demo中CefForm、CefNativeForm這兩個窗體類分别用于示範離屏渲染模式(對應CefControl控件)和真視窗模式(對應CefNativeControl控件)的功能。這兩個視窗的建立代碼在MainForm::OnClicked中有示範代碼

cef_module項目中預處理宏中增加了兩個控件Cef子產品功能的宏SUPPORT_CEF、SUPPORT_CEF_FLASH。SUPPORT_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檔案也可以删掉。

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

multi_threaded_message_loop導緻中斷:在2623、2526版本,Debug模式中,如果開啟了multi_threaded_message_loop,當程式退出時,必定會觸發中斷。這個屬于Cef的bug,在官方demo中也有這個問題,但是在Release模式中是沒有問題的。

2357版本在程式處理重定向資訊後,會導緻渲染程序崩潰,這個版本無法用于項目

1916版本各個功能使用正常。但是在在Debug模式下某些網頁打開時會出中斷警告(但并不是錯誤),可能是因為對新html标準支援不夠;Debug模式下單程序模式在退出時會觸發中斷。但是在Release模式下都正常使用

設定CefSettings的cache_path字段(也就是LocalStorage),一定要注意不要在路徑末尾添加”\\”,否則會觸發中斷

在多程序模式下,必須設定子程序的程式名(不管是使用原程式作為子程序,還是單獨一個程式作為子程序)。當Cef調用LoadUrl函數加載網頁時,會查找子程序的絕對路徑去啟動渲染程序,如果不設定子程序名字,會導緻查路徑找發生錯誤,導緻VS在Debug模式下卡死

如果開發者不負責Cef相關功能的開發,可以修改CefManager::AddCefDllToPath函數的代碼,讓Cef不管在Debug模式還是Release模式下都使用Release版本的Cef Dll檔案。這樣做不會發生錯誤,而且上面提到的多數坑都不會被觸發

如果在使用Cef子產品中遇到一些崩潰或者其他異常現象,請先使用release模式+開啟多程序模式再運作一次,很多問題都是debug模式或者單程序模式導緻的

如果使用flash功能,就需要在編譯時加入cef_sandbox.lib等靜态庫,否則在使用flash功能時會有一個黑框彈出(這輸入cef的bug)。而加入sandbox功能後,在某些電腦上離屏渲染功能就無法順利建立子程序,導緻沒有畫面。這時就要在子程序建立前檢查指令行參數,發現不是flash程序就增加<code>no-sandbox</code>參數來關閉sandbox功能

發現一個非常奇葩的bug,離屏渲染+多線程消息循環模式下,在浏覽器對象上右擊彈出菜單,是無法正常關閉的。翻cef源碼後發現菜單是用TrackPopupMenu函數建立的,在MSDN資料上檢視後發現調用TrackPopupMenu前需要給其父視窗調用SetForegroundWindow,但是在cef源碼中沒有調用。最終翻cef源碼後得到的解決方法是在cef的UI線程建立一個視窗,這個窗體的父視窗必須是在主程式UI線程建立的,這樣操作之後就不會出現菜單無法關閉的bug了,雖然不知道為什麼但是bug解決了

CefWindowInfo::SetAsWindowless的transparent參數必須設定為true!

Cef功能開發經驗總結
Cef功能開發經驗總結

繼續閱讀