cef GeneralUsage


本文翻譯自https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage

介紹

cef是個基於chromium的開源項目。跟chromium項目不同,cef主要聚焦於 google chrome應用開發,cef集中於促進嵌入式瀏覽器在第三方應用軟件中的使用。cef提供了穩定程度達到產品等級的API和使用特定chromium發布分支還有二進制發布包,從而將用戶和底層的chromium還有blink代碼隔離開來。大部分cef的功能都有默認實現,因此用戶只需要很少的或壓根不需要額外的工作。

下面是一些cef的使用場景

  • 在一個現有的本機應用程序中嵌入一個兼容html5的web瀏覽器控件
  • 創建一個輕量級的本地“shell”應用程序,該程序托管一個主要使用Web技術開發的用戶界面。
  • 在具有自定義的繪畫框架的應用中離屏渲染一個web內容
  • 充當現有Web屬性和應用程序自動測試的主機。

cef3是基於多進程的chromium content api實現的。cef多進程架構優勢包括:

  • 提升性能表現和穩定性(JavaScript和插件運行在獨立的進程)
  • 支持視網膜顯示。(Support for Retina displays.)
  • GPU加速
  • 非常酷的新特性例如webrtc和語言輸入
  • 通過devtools提供更好的自動化UI測試和遠程調試工具還有ChromeDriver2
  • 更快的調用現有的和未來將要有的web特性和標准的速度。

本篇文檔將介紹一些有關使用cef3開發應用的基本概念。

開始

使用二進制發布包

cef3的二進制發布包可以從這里下載,包括各種平台(Windows, Mac OS X或者Linux)需要的所有文件和特定版本的cef3。不管是什么平台,都有着同樣的基本結構

  • cefclient 包含配置為使用二進制發行版中的文件構建的cefclient示例應用程序。這個應用程序演示了廣泛的CEF功能。
  • cefsimple 包含配置為使用二進制發行版中的文件構建的cefclient示例應用程序。這個應用程序演示了創建瀏覽器窗口所需的最小功能。
  • Deubg 包含了cef shared library的debug構建。
  • include 包含了所有cef需要的頭文件
  • libcef_dll 包含所有使用CEF c++ API的應用程序必須鏈接的libcef_dll_wrapper靜態庫的源代碼。
  • Release 包含一個版本構建CEF共享庫(libcef)和任何其他需要在平台上運行的庫。
  • Resources 包含使用CEF(僅適用於Windows和Linux)的應用程序所需的資源。這包括.pak文件(帶有全局資源的二進制文件)和其他可能的文件,這些文件取決於平台。

每個二進制發布包還包含了一個README.txt文件,用來詳細解釋平台相關的發布包。還有一個LICENSE.txt文件,包含了cef的BSD許可。當發布一個基於CEF的應用時,你需要在你應用的發布版本中的某個地方包含這個文件。

基於CEF二進制發布文件開發的應用程序可以適用標准平台構建工具。這些工具包括Windows上的Visual Studio,Mac OS X上的Xcode,Linux上的gcc/make。項目下載頁面包含關於特定二進制版本所需的操作系統和構建工具版本的信息。在Linux上構建時,還要注意所列出的包依賴項。

重要概念

在開發基於cef3的應用程序之前,需要理解一些重要的基本概念。

C++封裝

libcef共享庫暴露了一個C API,該API將用戶與CEF運行時和代碼庫隔離開來。libcef_dll_wrapper項目以源代碼的形式作為二進制發行包的一部分存在。導出的C API封裝在C++ API中,然后鏈接到客戶端應用程序。C/C++ API轉換層的代碼是用translator工具自動生成的。

進程

CEF3運行時是多進程的。被稱作"browser"進程的主進程掌管着窗口創建,繪制和網絡連接。這通常與主機應用程序相同,大多數應用程序邏輯將在瀏覽器進程中運行。Blink渲染和JavaScript執行則在一個獨立的被稱作"render"進程中。一些應用邏輯,例如JS綁定和DOM訪問,也都運行在render進程中。默認的進程模型(默認情況下,進程模型?)將為每一個origin(scheme+域名)提供一個新的render進程。其他進程將在需要時產生,例如掌管類似flash的插件的"plugin"進程,還有掌管加速合成的"gpu"進程

默認情況下,可執行的主應用程序(即二進制程序)將被執行多次用來表示獨立的進程(即和chormium類似,所有進程的入口都是主程序),可以通過命令行參數傳遞給CefExecuteProcess函數,如果主應用程序太大了,需要花費很長的時間加載,或者不適合非browser進程的其他進程,則可以用獨立的(另外的)可執行程序提供給非browser進程。能在CefSettings.browser_subprocess_path變量中配置。

由CEF3產生的獨立進程使用進程間通信(IPC)進行通信。在browser進程的應用程序邏輯和render進程可以通過來回發送異步消息進行通信。render進程中的JavaScriptIntegration可以暴露用於在browser進程中處理的異步api。

線程

在CEF3中,每個進程都有多個線程在運行。想要知道完整的線程列表,可以查看cef_threaad_id_t枚舉。這里有一些常用的線程:

  • TID_UI 線程是browser進程的主線程,這個線程和主程序線程是同一個線程(如果CefSettings.multi_threaded_message_loop為false的話)
  • TID_IO 線程用於browser進程處理IPC和網絡信息
  • TID_FILE 線程用於browser進程和文件系統打交道,阻塞操作只能在這個線程上執行,或者再創建一個進程(CefThread)執行阻塞操作。
  • TID_RENDERER 線程是render進程的主線程,所有的Blink和V8交互必須在這個線程上進行。

由於CEF具有多線程特性,因此使用消息傳遞或鎖定來保護數據成員免於訪問多個線程非常重要。CefPostTask系列函數支持線程之間輕松的異步消息傳遞,就是PostTask。

可以使用CefCurrentlyOn函數來驗證當前線程是否是預期線程。CEF示例應用程序使用以下定義來驗證方法是否在預期線程上執行。這些定義包含在include / wrapper / cef_helpers.h頭文件中。

#define CEF_REQUIRE_UI_THREAD()       DCHECK(CefCurrentlyOn(TID_UI));
#define CEF_REQUIRE_IO_THREAD()       DCHECK(CefCurrentlyOn(TID_IO));
#define CEF_REQUIRE_FILE_THREAD()     DCHECK(CefCurrentlyOn(TID_FILE));
#define CEF_REQUIRE_RENDERER_THREAD() DCHECK(CefCurrentlyOn(TID_RENDERER));

為了支持對代碼塊的同步訪問,CEF通過include/base/cef_lock.h頭文件提供了base::Lock和base::AutoLock類型。例如:

#include "include/base/cef_lock.h"

// Class declaration.
class MyClass : public CefBase {
 public:
  MyClass() : value_(0) {}
  // Method that may be called on multiple threads.
  void IncrementValue();
 private:
  // Value that may be accessed on multiple theads.
  int value_;
  // Lock used to protect access to |value_|.
  base::Lock lock_;
  IMPLEMENT_REFCOUNTING(MyClass);
};

// Class implementation.
void MyClass::IncrementValue() {
  // Acquire the lock for the scope of this method.
  base::AutoLock lock_scope(lock_);
  // |value_| can now be modified safely.
  value_++;
}

引用計數

所有框架類都實現了CefBase接口,並且所有實例指針均使用CefRefPtr智能指針實現處理,該智能指針實現通過調用AddRef()和Release()自動處理引用計數。實現這些類的最簡單方法如下:

class MyClass : public CefBase {
 public:
  // Various class methods here...

 private:
  // Various class members here...

  IMPLEMENT_REFCOUNTING(MyClass);  // Provides atomic refcounting implementation.
};

// References a MyClass instance
CefRefPtr<MyClass> my_class = new MyClass();

字符串

CEF定義了自己的數據類型來表示字符串,有下面幾個原因

  • libcef庫和主機應用程序可以使用不同的運行時來管理堆內存。需要使用分配內存的相同運行時釋放所有對象(包括字符串)。
  • 可以編譯libcef庫以支持不同的基礎字符串類型(UTF8,UTF16或寬)。默認值為UTF16,但可以通過修改cef_string.h中的定義並重新編譯CEF 來更改。選擇寬字符串類型時,請記住大小會因平台而異。

命令行參數

許多CEF3和Chromium的功能特才行都可以用命令行參數來設置。這些參數采用 --some-argument [= optional-param]的形式,並通過CefExecuteProcess()CefMainArgs結構傳遞到CEF中

  • 要從命令行禁用參數處理,請在將CefSettings傳遞到CefInitialize()之前,將CefSettings.command_line_args_disabled設置為true。
  • 要在主機應用程序內部指定CEF / Chromium命令行參數,請實現CefApp:: OnBeforeCommandLineProcessing()方法。
  • 要將特定於應用程序的(非CEF / Chromium)命令行參數傳遞給子流程,請實現CefBrowserProcessHandler :: OnBeforeChildProcessLaunch()方法。

想知道更多參數?可以看shared/common/client_switches.cc中的注釋

應用結構

每一個CEF3應用都有通用的結構

  • 提供一個入口函數用於初始化CEF並運行子進程可執行邏輯或CEF消息循環的其中之一。
  • 提供一個CefApp的實現用於處理特定的進程回調。
  • 提供一個CefClient的實現用於處理特定的瀏覽器實例回調。
  • 調用CefBrowserHost::CreateBrowser()來創建一個browser實例並使用CefLifeSpanHandler來管理這個browser的聲明周期

入口函數

按照上述的進程小節的描述,CEF3應用程序是多進程的。這些進程可以都用同一個可執行程序或者用獨立的可執行程序們。進程的執行起始於一個入口函數。例子程序在cefclient/cefclient_win.cc``cefclient/cefclient_gtk.cc``cefclient/cefclient_mac.mm中。

當啟動子進程時,CEF將必須使用CefMainArgs結構傳遞到CefExecuteProcess來指定配置信息。Liunux和Mac OS X上,就是main函數的argc和argv。在Windows平台。則是實例句柄(HINSTANCE),它被傳遞給wWinMain()函數。還可以通過GetModuleHandle(NUll)檢索實例句柄。

單一可執行文件

當作為單個可執行文件運行時,需要入口函數來區分不同的進程類型。Windows和Linux都支持單一可執行結構,而Mac OS X不支持。

int main(int argc, char* argv[]) {
  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Execute the sub-process logic, if any. This will either return immediately for the browser
  // process or block until the sub-process should exit.
  int exit_code = CefExecuteProcess(main_args, app.get());
  if (exit_code >= 0) {
    // The sub-process terminated, exit now.
    return exit_code;
  }

  // Populate this structure to customize CEF behavior.
  CefSettings settings;

  // Initialize CEF in the main process.
  CefInitialize(main_args, settings, app.get());

  // Run the CEF message loop. This will block until CefQuitMessageLoop() is called.
  CefRunMessageLoop();

  // Shut down CEF.
  CefShutdown();

  return 0;
}

CefExecuteProcess函數就能執行和區分不同的進程,browser進程會立刻返回-1,其他進程則會執行完畢后返回對應進程的exit_code

獨立子進程可執行程序

使用獨立子進程可執行程序時候,需要兩個獨立的可執行文件,和兩個獨立的入口函數

主應用入口函數:

// Program entry-point function.
int main(int argc, char* argv[]) {
  // Load the CEF framework library at runtime instead of linking directly
  // as required by the macOS sandbox implementation.
  CefScopedLibraryLoader library_loader;
  if (!library_loader.LoadInMain())
    return 1;

  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Populate this structure to customize CEF behavior.
  CefSettings settings;

  // Specify the path for the sub-process executable.
  CefString(&settings.browser_subprocess_path).FromASCII(“/path/to/subprocess”);

  // Initialize CEF in the main process.
  CefInitialize(main_args, settings, app.get());

  // Run the CEF message loop. This will block until CefQuitMessageLoop() is called.
  CefRunMessageLoop();

  // Shut down CEF.
  CefShutdown();

  return 0;
}

子進程應用入口函數:

// Program entry-point function.
int main(int argc, char* argv[]) {
  // Initialize the macOS sandbox for this helper process.
  CefScopedSandboxContext sandbox_context;
  if (!sandbox_context.Initialize(argc, argv))
    return 1;

  // Load the CEF framework library at runtime instead of linking directly
  // as required by the macOS sandbox implementation.
  CefScopedLibraryLoader library_loader;
  if (!library_loader.LoadInHelper())
    return 1;

  // Structure for passing command-line arguments.
  // The definition of this structure is platform-specific.
  CefMainArgs main_args(argc, argv);

  // Optional implementation of the CefApp interface.
  CefRefPtr<MyApp> app(new MyApp);

  // Execute the sub-process logic. This will block until the sub-process should exit.
  return CefExecuteProcess(main_args, app.get());
}

消息循環集成

CEF也能集成到已有應用的消息循環機制中,而不是使用CEF自帶的消息循環。有兩種方法可以做到。

  1. 調用CefDoMessageLoopWork()來代替CefRunMessageLoop()。每一次調用都將表示一個CEF循環執行一次。這個方法應該謹慎使用,調用不夠頻繁將使CEF消息循環陷入飢餓,並對瀏覽器性能產生負面影響。過於頻繁的調用該方法則會占用CPU。
  2. 設置CefSettings.multi_threaded_message_loop = true(僅限Windows)。這將導致CEF在與主應用程序線程不同的線程上運行瀏覽器UI線程。使用這種方法,就不需要調用CefDoMessageLoopWork()CefRunMessageLoop()CefInitialize()CefShutDown()仍應該在主應用程序的主線程上調用。您將需要提供自己的與主應用程序線程進行通信的機制(例如,參見cefclient_win.cpp中的消息窗口用法)。您可以在Windows的cefclient中通過運行“ --multi-threaded-message-loop”命令行標志來測試此模式。

CefSettings

CefSettings允許配置CEF設置,下面是一些通常使用的配置成員:

  • browser_subprocess_path 配置子進程啟動獨立的可以執行文件的路徑。
  • multi_threaded_message_loop 設置為true可以使瀏覽器進程消息循環在單獨的線程中運行。
  • command_line_args_disabled 設置為true可以禁用命令行參數配置。
  • ``cache_path` 將緩存數據存儲在磁盤上的路徑,如果為空,則內存高速緩存將用於某些功能,而臨時磁盤高速緩存將用於其他功能。如果指定了緩存路徑,則諸如localStorage之類的HTML5數據庫將僅在會話之間持久存在。
  • locale 將傳遞給Blink的語言環境字符串。如果為空,則使用默認的"en-US"。在使用語言環境以優先級順序解析的語言環境確定語言環境的Linux上,將忽略此值:LANGUAGE,LC_ALL,LC_MESSAGES和LANG。也可以使用“ lang”命令行開關進行配置。
  • log_file 用於設置調試日志的目錄+文件名,如果為空,則使用默認的"debug.log",並將文件寫入應用程序目錄,也能使用命令行參數進行配置。
  • log_severity 日志等級,日志文件中僅記錄此等級或比此等級還要高的日志。也能用命令行參數配置,等級有"verbose""info""warning""error""error-report"和"disable"
  • resources_dir_path 資源目錄的標准路徑。如果此值為空,則cef.pak和/或devtools_resources.pak文件必須位於Windows / Linux上的模塊目錄中或Mac OS X上的應用程序包Resources目錄中。也可以使用“ resources-dir-path”命令進行配置線路開關。
  • locales_dir_path 語言環境目錄的標准路徑。如果此值為空,那么語言環境目錄必須位於模塊目錄中。在Mac OS X上,始終從應用程序包Resources目錄中加載打包文件的情況下,此值將被忽略。也可以使用“ locales-dir-path”命令行開關進行配置。
  • remote_debugging_port 設置為1024到65535之間的值,以在指定的端口上啟用遠程調試。例如,如果指定8080,則遠程調試URL將為http:// localhost:8080。可以從任何CEF或Chrome瀏覽器窗口中遠程調試CEF。也可以使用“ remote-debugging-port”命令行開關進行配置。

CefBrowser和CefFrame

CefBrowser和CefFrame對象用於給browser發送命令和在回調方法中檢索狀態信息。每個CefBrowser對象將有一個主CefFrame對象代表top-level frame,有0或多個CefFrame對象代表sub-frame。例如,一個瀏覽器加載了兩個iframe的話將會有三個CefFrame對象(一個top-level frame和兩個iframe)

在瀏覽器main frame加載一個URL:

browser->GetMainFrame()->LoadURL(some_url);

讓瀏覽器后退:

browser->GoBack();

檢索獲取main frame HTML contents:

// Implementation of the CefStringVisitor interface.
class Visitor : public CefStringVisitor {
 public:
  Visitor() {}

  // Called asynchronously when the HTML contents are available.
  virtual void Visit(const CefString& string) OVERRIDE {
    // Do something with |string|...
  }

  IMPLEMENT_REFCOUNTING(Visitor);
};

browser->GetMainFrame()->GetSource(new Visitor());

CefBrowser和CefFrame對象存在於瀏覽器進程和renderer進程中。

CefApp

CefApp接口提供了特定進程的回調,重要的回調包括:

  • OnBeforeCommandLineProcessing 提供了編程方式設置命令行參數
  • OnRegisterCustomSchemes 提供了注冊自定義scheme
  • GetBrowserProcessHandler 可以返回browser進程的特定函數,包括OnContextInitialized()函數
  • GetRenderProcessHandler 可以返回render進程的特定的函數,包括相關JS的回調和進程消息。

一個實現CefApp的例子可以看cefsimple/simple_app.hcefsimple/simple_app.cc

CefClient

CefClient接口提供了可以訪問特定的browser實例的回調,一個簡單的CefClient實例可以被任意數量的browser對象共享。重要的回調包括:

  • 回調函數例如browser的生命周期的各個回調,右鍵菜單,對話框,顯示通知,拖拽事件,焦點事件,鍵盤事件等等。這些回調都是可選的,可以實現也可以不實現。
  • OnProcessMessageReceived 當收到render進程發來的IPC通信時,這個回調就會被觸發。

一個實現CefClient的例子可以看cefsimple/simple_handler.hcefsimple/simple_handler.cc

browser生命周期

browser的生命周期起始於調用CefBrowserHost::CreateBrowser()或者CefBrowserHost::CreateBrowserSync()。執行這倆函數的一個比較便利的地方就是在CefBrowserProcesHandler::OnContextInitialized()回調中或者對應平台的消息回調中,例如windows上的WM_CREATE。

// Information about the window that will be created including parenting, size, etc.
// The definition of this structure is platform-specific.
CefWindowInfo info;
// On Windows for example...
info.SetAsChild(parent_hwnd, client_rect);

// Customize this structure to control browser behavior.
CefBrowserSettings settings;

// CefClient implementation.
CefRefPtr<MyClient> client(new MyClient);

// Create the browser asynchronously. Initially loads the Google URL.
CefBrowserHost::CreateBrowser(info, client.get(), “http://www.google.com”, settings, NULL);

CefLifeSpanHandler類提供了管理browser生命周期的必要的回調。下面是從相關函數和成員中抽取出的代碼。

class MyClient : public CefClient,
                 public CefLifeSpanHandler,
                 ... {
  // CefClient methods.
  virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE {
    return this;
  }

  // CefLifeSpanHandler methods.
  void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
  bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
  void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;

  // Member accessors.
  CefRefPtr<CefBrowser> GetBrower() { return m_Browser; }
  bool IsClosing() { return m_bIsClosing; }

 private:
  CefRefPtr<CefBrowser> m_Browser;
  int m_BrowserId;
  int m_BrowserCount;
  bool m_bIsClosing;

  IMPLEMENT_REFCOUNTING(MyClient);
};

OnAfterCreated()函數在browser對象創建完成后會被立刻調用。主機應用可以使用這個方法來保證一個正確的browser對象的引用。

void MyClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  if (!m_Browser.get())   {
    // Keep a reference to the main browser.
    m_Browser = browser;
    m_BrowserId = browser->GetIdentifier();
  }

  // Keep track of how many browsers currently exist.
  m_BrowserCount++;
}

銷毀browser的話可以調用CefBrowserHost::CloseBrowser()

// Notify the browser window that we would like to close it. This will result in a call to 
// MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
browser->GetHost()->CloseBrowser(false);

如果browser的父親是另一個窗口,那么關閉事件可能是來源於該父窗口的OS功能(例如,單擊父窗口的關閉按鈕)。然后,父窗口需要調用CloseBrowser(false)並等待第二個OS關閉事件,以告知瀏覽器已允許關閉。如果通過JavaScriptonbeforeunload事件回調處理或者DoClose()回調取消了關閉操作,則不會發送第二個操作系統關閉事件。請注意以下示例中的IsClosing()檢查,對於第一個操作系統關閉事件,它將返回false,而對第二個操作系統關閉事件,則返回true(在調用DoClose之后)。

在Windows,父窗口來處理:

case WM_CLOSE:
  if (g_handler.get() && !g_handler->IsClosing()) {
    CefRefPtr<CefBrowser> browser = g_handler->GetBrowser();
    if (browser.get()) {
      // Notify the browser window that we would like to close it. This will result in a call to 
      // MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
      browser->GetHost()->CloseBrowser(false);

      // Cancel the close.
      return 0;
    }
  }

  // Allow the close.
  break;

case WM_DESTROY:
  // Quitting CEF is handled in MyHandler::OnBeforeClose().
  return 0;
}

在Linux上,通過"delete_event"處理:

gboolean delete_event(GtkWidget* widget, GdkEvent* event,
                      GtkWindow* window) {
  if (g_handler.get() && !g_handler->IsClosing()) {
    CefRefPtr<CefBrowser> browser = g_handler->GetBrowser();
    if (browser.get()) {
      // Notify the browser window that we would like to close it. This will result in a call to 
      // MyHandler::DoClose() if the JavaScript 'onbeforeunload' event handler allows it.
      browser->GetHost()->CloseBrowser(false);

      // Cancel the close.
      return TRUE;
    }
  }

  // Allow the close.
  return FALSE;
}

在OS X上關閉是更加復雜的,看下cefsimple/cefsimple_mac.mm文件里面的注釋來全面地了解一下。

DoClose()方法設置m_blsClose標志並返回false發送第二個OS關閉事件。

bool MyClient::DoClose(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  // Closing the main window requires special handling. See the DoClose()
  // documentation in the CEF header for a detailed description of this
  // process.
  if (m_BrowserId == browser->GetIdentifier()) {
    // Set a flag to indicate that the window close should be allowed.
    m_bIsClosing = true;
  }

  // Allow the close. For windowed browsers this will result in the OS close
  // event being sent.
  return false;
}

當OS函數收到第二個OS關閉事件時,就允許父窗口關閉了。這之后將調用OnBeforeClose()。確保在OnBeforeClose()函數中釋放關於browser對象的任何引用。

void MyHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
  // Must be executed on the UI thread.
  REQUIRE_UI_THREAD();

  if (m_BrowserId == browser->GetIdentifier()) {
    // Free the browser pointer so that the browser can be destroyed.
    m_Browser = NULL;
  }

  if (--m_BrowserCount == 0) {
    // All browser windows have closed. Quit the application message loop.
    CefQuitMessageLoop();
  }
}

詳細的例子實現可以看cefclient。

離屏渲染

使用離屏渲染時,CEF不會創建本機瀏覽器窗口。相反,CEF向主機應用程序提供無效區域和像素緩沖區,並且主機應用程序將鼠標,鍵盤和焦點事件通知給CEF。離屏渲染目前不支持加速合成,因此與窗口瀏覽器相比,性能可能會受到影響。離屏瀏覽器將收到與窗口瀏覽器相同的通知,包括上一節中所述的生命周期回調。要使用屏幕外渲染你需要:

  1. 實現CefRenderHandler接口。所有的方法都應該被實現,除非特殊指定。
  2. 在傳遞CefWindowInfo結構體給CefBrowserHost::CreateBrowser()之前調用CefWindowInfo::SetAsWindowLess()。如果沒有父窗口傳遞給CefWindowInfo::SetAsWindowLess()一些功能可能無法使用,例如右鍵菜單等。
  3. CefRenderHandler::GetViewRect()方法用於獲取期望的view矩形區域。
  4. CefRenderHandler::OnPaint()方法用於提供一個無效區域並更新像素buffer。cefclient使用OpenGL繪制buffer,但你的應用可以使用任何你想用的技術來繪制。
  5. 通過CefBrowserHost::WasResized()來調整browser的大小。內部將會調用GetViewRect()來獲取新的size然后並調用OnPaint()
  6. 調用CefBrowserHost::SendXXX() 來發送一個鼠標、鍵盤或者聚焦事件給browser
  7. 調用CefBrowserHost::CloseBrowser()來銷毀browser

運行cefclient的時候加上--off-screen-rendering-enabled命令行參數來看看例子。

Posting Tasks

tasks能在單個進程的多個不同線程之間用CefPostTask系列函數傳遞(詳情見include/cef_task.h頭文件)。task將會在目標線程的消息循環中異步地執行。

CEF提供base::Bindbase::Callback模板回調類型來傳遞和綁定方法,對象和參數來傳遞給CefPostTask。完整版使用方法可以看include/base/cef_callback.h頭文件的注釋。include/wrapper/cef_closure_task.h頭文件提供了將base::Closure轉換成CefTask的一些輔助函數,見下面的例子

#include “include/base/cef_bind.h”
#include “include/wrapper/cef_closure_task.h”

// To execute a bound function:

// Define a function.
void MyFunc(int arg) { /* do something with |arg| on the UI thread */ }

// Post a task that will execute MyFunc on the UI thread and pass an |arg|
// value of 5.
CefPostTask(TID_UI, base::Bind(&MyFunc, 5));

// To execute a bound method:

// Define a class.
class MyClass : public CefBase {
 public:
  MyClass() {}
  void MyMethod(int arg) { /* do something with |arg| on the UI thread */ }
 private:
  IMPLEMENT_REFCOUNTING(MyClass);
};

// Create an instance of MyClass.
CefRefPtr<MyClass> instance = new MyClass();

// Post a task that will execute MyClass::MyMethod on the UI thread and pass
// an |arg| value of 5. |instance| will be kept alive until after the task
// completes.
CefPostTask(TID_UI, base::Bind(&MyClass::MyMethod, instance, 5));

如果本機應用需要持有一個任務循環的引用,可以使用CefTaskRunner類。例如,獲取UI現成的任務循環

CefRefPtr<CefTaskRunner> task_runner = CefTaskRunner::GetForThread(TID_UI);

Inter-Process Communication (IPC)

自從CEF可以運行在多進程模式下,提供進程間的交流機制就變得十分必要。CefBrowserCefFrame對象存在於browser進程和renderer進程中,這有助於簡化該過程。每個CefBrowserCefFrame對象還具有與之關聯的唯一ID值,這個ID在互相通信的進程之間相匹配。

進程啟動消息

為了在啟動時為所有renderer進程提供相同的信息,請在瀏覽器進程中實現CefBrowserProcessHandler::OnRenderProcessThreadCreated()函數。在這個函數中會將信息傳遞給渲染進程的CefRenderProcessHandler :: OnRenderThreadCreated()函數。

處理運行時消息

可以用CefProcessMessage類在運行時的進程間傳遞消息。這些消息與特定的CefBrowserCefFrame相關聯,並使用CefFrame::SendProcessMessage()方法發送。處理消息應包含通過CefProcessMessage :: GetArgumentList()所需的任何狀態信息

// Create the message object.
CefRefPtr<CefProcessMessage> msg= CefProcessMessage::Create(“my_message”);

// Retrieve the argument list object.
CefRefPtr<CefListValue> args = msg>GetArgumentList();

// Populate the argument values.
args->SetString(0, “my string”);
args->SetInt(0, 10);

// Send the process message to the main frame in the render process.
// Use PID_BROWSER instead when sending a message to the browser process.
browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, msg);

CefRenderProcessHandler::OnProcessMessageReceived()中使用從browser進程發往renderer進程的消息。從renderer進程發往browser進程的消息則可以在CefClient::OnProcessMessageReceived()中使用。

bool MyHandler::OnProcessMessageReceived(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefProcessId source_process,
    CefRefPtr<CefProcessMessage> message) {
  // Check the message name.
  const std::string& message_name = message->GetName();
  if (message_name == “my_message”) {
    // Handle the message here...
    return true;
  }
  return false;
}

異步JavaScript綁定

JavaScriptIntegration在renderer進程中實現,但是需要頻繁的與browser進程交流。JavaScript API通過closures和promises設計成了異步的。

通用消息路由

CEF提供了一種通用實現,用於在renderer進程中運行的JavaScript和browser進程中運行的C++之間路由異步消息。應用程序通過從標准CEF C++回調(OnBeforeBrowseOnProcessMessageRecievedOnContextCreated等)傳遞數據來與路由器進行交互。渲染器側路由器支持通用的JavaScript回調注冊和執行,而瀏覽器側路由器則通過一個或多個應用程序提供的Handler實例支持特定於應用程序的邏輯。

自定義實現

一個基於CEF的應用程序也可以提供屬於它自己實現的異步JavaScript綁定。一個最簡單的實現如下所示

  1. 在renderer進程的JavaScript綁定傳遞一個回調函數
// In JavaScript register the callback function.
app.setMessageCallback('binding_test', function(name, args) {
  document.getElementById('result').value = "Response: "+args[0];
});
  1. renderer進程保存一個回調函數的引用
// Map of message callbacks.
typedef std::map<std::pair<std::string, int>,
                 std::pair<CefRefPtr<CefV8Context>, CefRefPtr<CefV8Value> > >
                 CallbackMap;
CallbackMap callback_map_;

// In the CefV8Handler::Execute implementation for “setMessageCallback”.
if (arguments.size() == 2 && arguments[0]->IsString() &&
    arguments[1]->IsFunction()) {
  std::string message_name = arguments[0]->GetStringValue();
  CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
  int browser_id = context->GetBrowser()->GetIdentifier();
  callback_map_.insert(
      std::make_pair(std::make_pair(message_name, browser_id),
                     std::make_pair(context, arguments[1])));
}
  1. renderer過程將異步IPC消息發送到瀏覽器過程,請求執行工作。
  2. browser進程收到IPC消息並執行工作。
  3. 完成工作后,browser進程會將結果通過異步IPC發回給renderer進程
  4. renderer進程收到IPC消息並執行result中的回調函數。
// Execute the registered JavaScript callback if any.
if (!callback_map_.empty()) {
  const CefString& message_name = message->GetName();
  CallbackMap::const_iterator it = callback_map_.find(
      std::make_pair(message_name.ToString(),
                     browser->GetIdentifier()));
  if (it != callback_map_.end()) {
    // Keep a local reference to the objects. The callback may remove itself
    // from the callback map.
    CefRefPtr<CefV8Context> context = it->second.first;
    CefRefPtr<CefV8Value> callback = it->second.second;

    // Enter the context.
    context->Enter();

    CefV8ValueList arguments;

    // First argument is the message name.
    arguments.push_back(CefV8Value::CreateString(message_name));

    // Second argument is the list of message arguments.
    CefRefPtr<CefListValue> list = message->GetArgumentList();
    CefRefPtr<CefV8Value> args = CefV8Value::CreateArray(list->GetSize());
    SetList(list, args);  // Helper function to convert CefListValue to CefV8Value.
    arguments.push_back(args);

    // Execute the callback.
    CefRefPtr<CefV8Value> retval = callback->ExecuteFunction(NULL, arguments);
    if (retval.get()) {
      if (retval->IsBool())
        handled = retval->GetBoolValue();
    }

    // Exit the context.
    context->Exit();
  }
}
  1. CefRenderProcessHandler :: OnContextReleased()中釋放與上下文有關聯的所有v8引用。
void MyHandler::OnContextReleased(CefRefPtr<CefBrowser> browser,
                                  CefRefPtr<CefFrame> frame,
                                  CefRefPtr<CefV8Context> context) {
  // Remove any JavaScript callbacks registered for the context that has been released.
  if (!callback_map_.empty()) {
    CallbackMap::iterator it = callback_map_.begin();
    for (; it != callback_map_.end();) {
      if (it->second.first->IsSame(context))
        callback_map_.erase(it++);
      else
        ++it;
    }
  }
}

同步請求

在一些少見的場景下有必要實現browser進程和renderer進程之間的同步通信。應該盡可能避免這種情況,因為這樣會對render進程產生性能上的影響。但是,如果你必須要同步,那么考慮一下使用同步XMLHttpRequests,它將會阻塞render進程並等待browser進程網絡層處理。browser進程可以處理使用自定義協議或網絡攔截的請求。

網絡層

默認情況下,將以對主機應用程序透明的方式處理CEF3中的網絡請求。對於希望與網絡層建立更緊密關系的應用程序,CEF3公開了一系列與網絡相關的功能。與網絡相關的回調可能發生在不同的線程上,因此請確保注意文檔並適當保護您的數據成員。

自定義請求

最簡單的加載網頁的方式就是通過CefFrame::LoadURL()

browser->GetMainFrame()->LoadURL(some_url);

如果應用程序想要發送一些復雜的請求,例如包括一些自定義的請求頭或者上傳一些數據,可以使用CefFrame::LoadRequest()方法。這個方法只有CefRequest對象一個參數。

// Create a CefRequest object.
CefRefPtr<CefRequest> request = CefRequest::Create();

// Set the request URL.
request->SetURL(some_url);

// Set the request method. Supported methods include GET, POST, HEAD, DELETE and PUT.
request->SetMethod(“POST”);

// Optionally specify custom headers.
CefRequest::HeaderMap headerMap;
headerMap.insert(
    std::make_pair("X-My-Header", "My Header Value"));
request->SetHeaderMap(headerMap);

// Optionally specify upload content.
// The default “Content-Type” header value is "application/x-www-form-urlencoded".
// Set “Content-Type” via the HeaderMap if a different value is desired.
const std::string& upload_data = “arg1=val1&arg2=val2”;
CefRefPtr<CefPostData> postData = CefPostData::Create();
CefRefPtr<CefPostDataElement> element = CefPostDataElement::Create();
element->SetToBytes(upload_data.size(), upload_data.c_str());
postData->AddElement(element);
request->SetPostData(postData);

瀏覽器獨立請求

應用程序可以通過CefURLRequest類發送與特定瀏覽器不相關的網絡請求。實現CefURLRequestClient接口以處理結果響應。CefURLRequest可以在browser和render過程中使用。

class MyRequestClient : public CefURLRequestClient {
 public:
  MyRequestClient()
    : upload_total_(0),
      download_total_(0) {}

  void OnRequestComplete(CefRefPtr<CefURLRequest> request) OVERRIDE {
    CefURLRequest::Status status = request->GetRequestStatus();
    CefURLRequest::ErrorCode error_code = request->GetRequestError();
    CefRefPtr<CefResponse> response = request->GetResponse();

    // Do something with the response...
  }

  void OnUploadProgress(CefRefPtr<CefURLRequest> request,
                        int64 current,
                        int64 total) OVERRIDE {
    upload_total_ = total;
  }

  void OnDownloadProgress(CefRefPtr<CefURLRequest> request,
                          int64 current,
                          int64 total) OVERRIDE {
    download_total_ = total;
  }

  void OnDownloadData(CefRefPtr<CefURLRequest> request,
                      const void* data,
                      size_t data_length) OVERRIDE {
    download_data_ += std::string(static_cast<const char*>(data), data_length);
  }

  bool GetAuthCredentials(bool isProxy,
                          const CefString& host,
                          int port,
                          const CefString& realm,
                          const CefString& scheme,
                          CefRefPtr<CefAuthCallback> callback) OVERRIDE {
    return false;  // Not handled.
  }

 private:
  int64 upload_total_;
  int64 download_total_;
  std::string download_data_;

 private:
  IMPLEMENT_REFCOUNTING(MyRequestClient);
};

發送請求:

// Set up the CefRequest object.
CefRefPtr<CefRequest> request = CefRequest::Create();
// Populate |request| as shown above...

// Create the client instance.
CefRefPtr<MyRequestClient> client = new MyRequestClient();

// Start the request. MyRequestClient callbacks will be executed asynchronously.
CefRefPtr<CefURLRequest> url_request = CefURLRequest::Create(request, client.get(), nullptr);
// To cancel the request: url_request->Cancel();

CefURLRequest發出的請求還可以通過CefRequest :: SetFlags()方法指定自定義行為。支持的標志位包括:

  • UR_FLAG_SKIP_CACHE如果設置,則在處理請求時將跳過緩存。
  • UR_FLAG_ALLOW_CACHED_CREDENTIALS如果設置了cookie,則可能與請求一起發送並從響應中保存。還必須設置UR_FLAG_ALLOW_CACHED_CREDENTIALS。
  • UR_FLAG_REPORT_UPLOAD_PROGRESS如果設置了請求,則在有正文的情況下將生成上傳進度事件。
  • UR_FLAG_NO_DOWNLOAD_DATA如果設置,則不會調用CefURLRequestClient :: OnDownloadData方法。
  • UR_FLAG_NO_RETRY_ON_5XX如果設置為5XX,重定向錯誤將傳播到觀察者,而不是自動重試。當前,這僅適用於源自瀏覽器進程的請求。

例如,跳過緩存而不報告下載數據:

request-> SetFlags(UR_FLAG_SKIP_CACHE | UR_FLAG_NO_DOWNLOAD_DATA);

請求處理

CEF3支持兩種方法來處理應用程序內部的網絡請求。方案處理程序方法允許為針對特定來源(方案+域)的請求注冊處理程序。請求攔截方法允許根據應用程序的判斷處理任意請求。

使用HTTP方案而不是自定義方案可以避免一系列潛在問題。

如果選用自定義方案(“ HTTP”,“ HTTPS”等以外的其他方案),則必須在CEF中進行注冊,以使其發揮預期的作用。如果您希望自定義方案的行為類似於HTTP(支持POST請求並強制執行HTTP訪問控制(CORS)限制),則應將其注冊為“標准”方案。如果您打算對其他方案執行跨域請求或通過XMLHttpRequest將POST請求發送到方案處理程序,則應使用HTTP方案而不是自定義方案,以避免潛在的問題。如果希望使用自定義方案,則必須通過CefApp :: OnRegisterCustomSchemes()回調來注冊屬性,該回調必須在所有進程中實現。

void MyApp::OnRegisterCustomSchemes(CefRefPtr<CefSchemeRegistrar> registrar) {
  // Register "client" as a standard scheme.
  registrar->AddCustomScheme("client", true, ...);
}

通用資源管理器

CEF提供了一種通用的實現,用於管理來自一個或多個數據源的資源請求。該用戶注冊用於不同數據源的處理程序,例如磁盤上的目錄,zip存檔或自定義實現,並且管理器處理請求。應用程序通過從標准CEF C++回調(OnBeforeResourceLoadGetResourceHandler)傳遞數據來與路由器進行交互。

Scheme 處理

一個scheme handler是通過CefRegisterSchemeHandlerFactory()函數注冊的,調用該函數的一個比較好的地方是在CefBrowserProcessHandler::OnContextInitialized()函數。例如,你可以注冊一個client://myapp/請求的回調處理:

CefRegisterSchemeHandlerFactory("client", “myapp”, new MySchemeHandlerFactory());

處理程序可與內置方案(HTTP,HTTPS等)和自定義方案一起使用。使用內置方案時,請為您的應用程序選擇一個唯一的域名(例如“ myapp”或“ internal”)。實現CefSchemeHandlerFactoryCefResourceHandler類來處理請求並提供響應數據。如果使用自定義方案,請不要忘記實現上述CefApp :: OnRegisterCustomSchemes方法。

如果在請求時知道響應數據,則CefStreamResourceHandler類提供CefResourceHandler的便捷默認實現。

// CefStreamResourceHandler is part of the libcef_dll_wrapper project.
#include “include/wrapper/cef_stream_resource_handler.h”

const std::string& html_content = “<html><body>Hello!</body></html>”;

// Create a stream reader for |html_content|.
CefRefPtr<CefStreamReader> stream =
    CefStreamReader::CreateForData(
        static_cast<void*>(const_cast<char*>(html_content.c_str())),
        html_content.size());

// Constructor for HTTP status code 200 and no custom response headers.
// There’s also a version of the constructor for custom status code and response headers.
return new CefStreamResourceHandler("text/html", stream);

請求攔截

CefRequestHandler::GetResourceHandler()方法支持隨心所欲的攔截請求。它使用與方案處理程序方法相同的CefResourceHandler類。如果使用自定義方案,請不要忘記實現上述CefApp::OnRegisterCustomSchemes方法。

CefRefPtr<CefResourceHandler> MyHandler::GetResourceHandler(
      CefRefPtr<CefBrowser> browser,
      CefRefPtr<CefFrame> frame,
      CefRefPtr<CefRequest> request) {
  // Evaluate |request| to determine proper handling...
  if (...)
    return new MyResourceHandler();

  // Return NULL for default handling of the request.
  return NULL;
}

響應過濾

CefRequestHandler::GetResourceHandler()方法支持過濾請求回復數據

其他回調

CefRequestHandler接口為多樣的網絡相關事件提供回調(包括認證,cookie處理,多余協議處理,認證錯誤等等)

代理解析

使用與Google Chrome相同的命令行標志在CEF3中配置代理設置。

--proxy-server=host:port
      Specify the HTTP/SOCKS4/SOCKS5 proxy server to use for requests. An individual proxy
      server is specified using the format:

        [<proxy-scheme>://]<proxy-host>[:<proxy-port>]

      Where <proxy-scheme> is the protocol of the proxy server, and is one of:

        "http", "socks", "socks4", "socks5".

      If the <proxy-scheme> is omitted, it defaults to "http". Also note that "socks" is equivalent to
      "socks5".

      Examples:

        --proxy-server="foopy:99"
            Use the HTTP proxy "foopy:99" to load all URLs.

        --proxy-server="socks://foobar:1080"
            Use the SOCKS v5 proxy "foobar:1080" to load all URLs.

        --proxy-server="sock4://foobar:1080"
            Use the SOCKS v4 proxy "foobar:1080" to load all URLs.

        --proxy-server="socks5://foobar:66"
            Use the SOCKS v5 proxy "foobar:66" to load all URLs.

      It is also possible to specify a separate proxy server for different URL types, by prefixing
      the proxy server specifier with a URL specifier:

      Example:

        --proxy-server="https=proxy1:80;http=socks4://baz:1080"
            Load https://* URLs using the HTTP proxy "proxy1:80". And load http://*
            URLs using the SOCKS v4 proxy "baz:1080".

--no-proxy-server
      Disables the proxy server.

--proxy-auto-detect
      Autodetect  proxy  configuration.

--proxy-pac-url=URL
      Specify proxy autoconfiguration URL.

如果代理需要認證, CefRequestHandler::GetAuthCredentials()回調執行的時候將會有一個值為true的|isProxy|參數並檢索用戶名和密碼

bool MyHandler::GetAuthCredentials(
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    bool isProxy,
    const CefString& host,
    int port,
    const CefString& realm,
    const CefString& scheme,
    CefRefPtr<CefAuthCallback> callback) {
  if (isProxy) {
    // Provide credentials for the proxy server connection.
    callback->Continue("myuser", "mypass");
    return true;
  }
  return false;
}

由於網絡代理解析(例如,如果在Windows上選中了“自動檢測代理設置”,則可能會延遲應用程序啟動期間Web內容的加載)。為了獲得最佳的用戶體驗,請考慮將您的應用程序設計為首先顯示靜態初始頁面,然后使用元刷新將其重定向到實際網站-重定向將被阻止,直到代理解析完成為止。出於測試目的,可以使用“ --no-proxy-server”命令行標志禁用代理解析。通過在命令行中運行“ chrome --url = ...”,代理解析延遲也可以在Google Chrome中復制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM