使用CEF(三)— 從CEF官方Demo源碼入手解析CEF架構與CefApp、CefClient對象


在上文《使用CEF(2)— 基於VS2019編寫一個簡單CEF樣例》中,我們介紹了如何編寫一個CEF的樣例,在文章中提供了一些代碼清單,在這些代碼清單中提到了一些CEF的定義的類,例如CefAppCefClient等等。它們具體有什么作用,和CEF的進程架構有什么關系呢?本文將逐一進行介紹。

CEF的進程架構

CEF3 runs using multiple processes. The main process which handles window creation, painting and network access is called the “browser” process. This is generally the same process as the host application and the majority of the application logic will run in the browser process. Blink rendering and JavaScript execution occur in a separate “render” process. Some application logic, such as JavaScript bindings and DOM access, will also run in the render process. The default process model will spawn a new render process for each unique origin (scheme + domain). Other processes will be spawned as needed, such as “plugin” processes to handle plugins like Flash and “gpu” processes to handle accelerated compositing.

CEF3使用多個進程運行。處理窗口創建、繪制和網絡訪問的主要進程稱為瀏覽器進程。這通常與宿主應用程序的進程相同,大多數應用程序的邏輯將在瀏覽器進程中運行。使用Blink引擎渲染HTML和JavaScript執行在單獨的渲染進程中發生。一些應用程序邏輯(如JavaScript綁定和DOM訪問)也將在渲染進程中運行。默認進程模型將為每個唯一源地址(scheme+domain)運行一個新的渲染進程。其他進程將根據需要生成,例如處理Flash等插件的插件進程和處理加速合成的GPU進程。綜合上述文檔,我們整理一下:

瀏覽器進程(Browser Process):

  • 窗口創建、繪制
  • 網絡訪問
  • ......

渲染進程(Renderer Process):

  • 通過Blink引擎渲染HTML

  • JavaScript執行(V8引擎)

  • ......

需要注意的是,瀏覽器進程中會進行窗口繪制,並不是指繪制HTML內容,而是承載網頁內容的那個窗體殼,同樣渲染進程也不是用來創建窗體的進程。接下來,本人將以官方CefSimple Demo源碼入手,逐步介紹Cef的概念。

本來本人想要使用上一文中的編寫的simple-cef進行源碼解析,但是為了讓本文相對的獨立,所以還是決定使用官方的Demo:cefsimple進行源碼解析。盡管和simple-cef項目的內容差別不大。需要注意的是一下的源碼在解析的時候,會進行適當的刪改,讀者最好對照源碼進行閱讀更佳。PS:源碼中顯示......表明示例代碼有所刪除。

cefsimple_win.cc

// ......
// Entry point function for all processes.
int APIENTRY wWinMain(HINSTANCE hInstance,
                      HINSTANCE hPrevInstance,
                      LPTSTR lpCmdLine,
                      int nCmdShow) {
// ......

  // CEF applications have multiple sub-processes (render, plugin, GPU, etc)
  // that share the same executable. This function checks the command-line and,
  // if this is a sub-process, executes the appropriate logic.
  int exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
  if (exit_code >= 0) {
    // The sub-process has completed so return here.
    return exit_code;
  }

// ......

  // SimpleApp implements application-level callbacks for the browser process.
  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
  CefRefPtr<SimpleApp> app(new SimpleApp);

  // Initialize CEF.
  CefInitialize(main_args, settings, app.get(), sandbox_info);

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

  // Shut down CEF.
  CefShutdown();

  return 0;
}

首先第一個重要點是:

    // CEF applications have multiple sub-processes (render, plugin, GPU, etc)
    // that share the same executable. This function checks the command-line and,
    // if this is a sub-process, executes the appropriate logic.    
	int exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
    if (exit_code >= 0) {
        // The sub-process has completed so return here.
        return exit_code;
    }

這段代碼看起來有點奇怪,對於英文的翻譯如下:

CEF應用程序會創建多個子進程(渲染render,插件plugin,GPU處理,等等)但是會共用一個可執行程序。以下的函數會檢查命令行並且,如果確認是一個子進程,那么會執行相關的邏輯。

然后,我們查看該函數:CefExecuteProcess

///
// This function should be called from the application entry point function to
// execute a secondary process. It can be used to run secondary processes from
// the browser client executable (default behavior) or from a separate
// executable specified by the CefSettings.browser_subprocess_path value. If
// called for the browser process (identified by no "type" command-line value)
// it will return immediately with a value of -1. If called for a recognized
// secondary process it will block until the process should exit and then return
// the process exit code. The |application| parameter may be empty. The
// |windows_sandbox_info| parameter is only used on Windows and may be NULL (see
// cef_sandbox_win.h for details).
///
/*--cef(api_hash_check,optional_param=application,
        optional_param=windows_sandbox_info)--*/
int CefExecuteProcess(const CefMainArgs& args,
                      CefRefPtr<CefApp> application,
                      void* windows_sandbox_info);

翻譯:

該函數應當在應用程序的入口函數處被調用,用以執行一個子進程。它可以用於執行一個可執行程序來啟動一個子進程,該可執行程序可以是當前的瀏覽器客戶端可執行程序(默認行為)或是通過設置CefSettings.browser_subprocess_path指定路徑的可執行程序。如果被調用用於瀏覽器進程(在啟動命令行中沒有"type"參數),該函數會立刻返回-1。如果被調用時識別為子進程,該函數將會阻塞直到子進程退出並且返回子進程退出的返回碼。application參數可以為空(null)。windows_sandbox_info參數只能在Windows上使用或設置為NULL(詳見cef_sandbox_win.h)

從這段話我們不難推斷出,CEF在以多進程架構下啟動的時候,會多次啟動自身可執行程序。啟動的時候,會通過命令行參數傳入某些標識,由CefExecuteProcess內部進行判斷。如果是主進程,則該函數立刻返回-1,程序會繼續執行下去,那么后續繼續運行的代碼全部都運行在主進程中;如果是子進程(渲染進程等),那么該函數會阻塞住,直到子進程結束后,該函數會返回一個大於等於0的值,並在main函數直接返回,進而退出。

對CefExecuteProcess分析就到這里,細節可以閱讀官方文檔,我們繼續后續的代碼分析:

  // SimpleApp implements application-level callbacks for the browser process.
  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
  CefRefPtr<SimpleApp> app(new SimpleApp);

注釋翻譯如下

SimpleApp實現了對於瀏覽器進程在應用級別的回調。該實例CEF初始化后(initialized),在OnContextInitialized中會創建第一個browser實例

查看SimpleApp的聲明,發現該類繼承了CefApp:

class SimpleApp : public CefApp, public CefBrowserProcessHandler {
 public:
  SimpleApp();
  ......
}

於是,我們迎來了第一個重要的類:CefApp。

CefApp

CefApp在官方文檔中,就寫了一句話介紹:

The CefApp interface provides access to process-specific callbacks.

CefApp接口提供了指定進程的回調訪問。

本人一開始看到CefApp時,想到上面提到的CEF的多進程架構,結合后文還會提到的CefClient,以為所謂CefApp就是指瀏覽器進程,CefClient就對應其他的進程(一個App對應多個Client,多么的自然的理解),然而這樣錯誤的理解,讓本人在閱讀代碼的時候走了很大的彎路。

首先,我們看一下CefApp的頭文件聲明:

class CefApp : public virtual CefBaseRefCounted {
 public:
  virtual void OnBeforeCommandLineProcessing(
      const CefString& process_type,
      CefRefPtr<CefCommandLine> command_line) {}
    
  virtual void OnRegisterCustomSchemes(
      CefRawPtr<CefSchemeRegistrar> registrar) {}
    
  virtual CefRefPtr<CefResourceBundleHandler> GetResourceBundleHandler() {
    return nullptr;
  }
    
  virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() {
    return nullptr;
  }
    
  virtual CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() {
    return nullptr;
  }
};

先看其中有兩個本文討論的重點方法:GetBrowserProcessHandlerGetRenderProcessHandler。它們的文檔注釋如下:

///
// Return the handler for functionality specific to the browser process. This
// method is called on multiple threads in the browser process.
// 返回瀏覽器進程特定功能的處理程序。在瀏覽器進程中的多個線程上調用此方法。
///
virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler()
///
// Return the handler for functionality specific to the render process. This
// method is called on the render process main thread.
// 返回渲染進程特定功能的處理程序。在渲染進程中的主線程上調用此方法。
///
virtual CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler()

讀者看到這些注釋可能會疑問:為什么注釋中一會兒說在瀏覽器進程中一會兒又說在渲染進程中?難道這個類的實例還會在多個進程中使用嗎?對也不對。這個類的實例確實會在瀏覽器進程和渲染進程中使用,但是我們又知道,兩個進程之間的資源是不共享的,包括類實例,所以在瀏覽器進程運行的過程中,會使用到CefApp的某個實例化對象,而在渲染進程的運行過程中,又會使用到CefApp另一個實例化對象,它們都是CefApp子類的實例,但一定不是同一個實例對象。

我們可以這樣理解:一個CefApp對應了一個進程,而一個進程可以是瀏覽器進程(Browser Process),可以是渲染進程(Renderer Process)。因此,CefApp提供了GetBrowserProcessHandler和GetRendererProcessHandler來分別在相關進程中獲取對應的handler。

這兩個方法的實現由我們來決定,即我們可以通過編程方式來返回handler,但這兩個方法不會由我們客戶端代碼進行調用,而是CEF在運行過程中,由CEF在某個時刻來回調這兩個方法。所以,這里雖然寫了兩個GetXXXProcessHandler,但在瀏覽器進程渲染進程中只會分別調用GetBrowserProcessHandler和GetRendererProcessHandler。

按照程序運行的角度講,當瀏覽器進程運行的時候,CEF框架就會在某個時候調用CefApp::GetBrowserProcessHandler獲得由我們定義的BrowserProcessHandler實例,這個實例會在適當的時候調用它提供的一些方法(后文介紹有哪些方法);當渲染進程運行的時候,CEF框架就會在某個時候調用CefApp::GetRendererProcessHandler得到我們定義的RendererProcessHandler實例,然后在適當的時候調用RenererProcessHandler中的一些方法(后文介紹有哪些方法)。

在cefsimple的示例代碼中只有一個SimpleApp是繼承的CefApp,這個類還繼承了CefBrowserHandler,表明自身是同時也是CefBrowserHandler,這樣實現的GetBrowserProcessHandler就返回自身。那么CEF是如何將我們的CefApp實例關聯到CEF運行中的呢?

  // SimpleApp implements application-level callbacks for the browser process.
  // It will create the first browser instance in OnContextInitialized() after
  // CEF has initialized.
  CefRefPtr<SimpleApp> app(new SimpleApp);

  // Initialize CEF.
  CefInitialize(main_args, settings, app.get(), sandbox_info);

注意CefInitialize中的app.get()參數,就是將我們的CefApp關聯到CEF的運行中的。那么,有些讀者會有疑問,在示例代碼中,只看到我們創建的SimpleApp類繼承了CefApp,並通過GetBrowserProcessHandler返回自身來表明是一個瀏覽器進程的回調實例,並沒有看到體現渲染進程的代碼呢?確實,cefsimple作為helloworld級別的代碼,沒有體現這一點。在cefclient示例代碼中(更高階的CEF示例,也更復雜),你會看到:

上圖是瀏覽器進程CefApp子類ClientAppBrowser(這里的”Client“是cefclient示例代碼的“client”,請勿和下文的CefClient類混淆)。

同時你還能找到一個CefApp子類ClientAppRenderer:

你甚至還能找到一個名為ClientAppOther的CefApp子類:

那么它們在哪兒被使用到呢?

看到這里,我相信絕大多數的讀者應該能夠理解我所說的CefApp代表的是一個進程的抽象了。這塊的大體流程是,通過一個工具函數GetProcessType從命令行中解析--type=xxx(瀏覽器進程沒有這個命令參數)來判斷進程的類型,然后實例化對應的CefApp子類,最后通過CefExecuteProcess來運行進程。

在介紹了CefApp的基本概念以后,我們可以繼續分析SimpleApp

通過上文,我們知道SimpleApp是CefApp子類,並且通過只會在瀏覽器進程中,會使用到該類的實例,因為實現了接口CefBrowserProcessHandler,並且有如下代碼:

  // CefApp methods:
  virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler()
      OVERRIDE {
    return this;
  }

那么在CEF中,作為CefBrowserProcessHandler,有哪些回調可以供我們定制呢?下面是頭文件聲明,並且我也寫了下概要注釋:

class CefBrowserProcessHandler : public virtual CefBaseRefCounted {
 public:
    // Cookie處理定制化
  virtual void GetCookieableSchemes(std::vector<CefString>& schemes,
                                    bool& include_defaults) {}
    // 在CEF上下文初始化后,在瀏覽器進程UI線程中進行調用。
  virtual void OnContextInitialized() {}
    // 可定制化處理子進程啟動時的命令行參數
  virtual void OnBeforeChildProcessLaunch(
      CefRefPtr<CefCommandLine> command_line) {}
    // 打印處理
  virtual CefRefPtr<CefPrintHandler> GetPrintHandler() { return nullptr; }
    // 自定義消息循環的時候,消息循環的頻率
  virtual void OnScheduleMessagePumpWork(int64 delay_ms) {}
    // 獲取默認的CefClient
  virtual CefRefPtr<CefClient> GetDefaultClient() { return nullptr; }
};

通過閱讀該Handler的頭文件以及每個函數的調用說明,我們繼續閱讀在SimpleApp::OnContextInitialized這個函數:

void SimpleApp::OnContextInitialized() {
  CEF_REQUIRE_UI_THREAD();

  CefRefPtr<CefCommandLine> command_line =
      CefCommandLine::GetGlobalCommandLine();

  const bool enable_chrome_runtime =
      command_line->HasSwitch("enable-chrome-runtime"); // 是否啟用chrome運行時

#if defined(OS_WIN) || defined(OS_LINUX)
    // Create the browser using the Views framework if "--use-views" is specified
    // via the command-line. Otherwise, create the browser using the native
    // platform framework. The Views framework is currently only supported on
    // Windows and Linux.
    // 如果命令行中指定了"--use-views",那么使用CEF自己的視圖框架(Views framework)
    // 否則使用操作系統原生API。視圖框架目前只支持Windows和Linux。
  const bool use_views = command_line->HasSwitch("use-views");
#else
  const bool use_views = false;
#endif

  // SimpleHandler implements browser-level callbacks.
  CefRefPtr<SimpleHandler> handler(new SimpleHandler(use_views));

  // Specify CEF browser settings here.
  CefBrowserSettings browser_settings;

  std::string url;

  // Check if a "--url=" value was provided via the command-line. If so, use
  // that instead of the default URL.
  url = command_line->GetSwitchValue("url");
  if (url.empty())
    url = "http://www.google.com";

  if (use_views && !enable_chrome_runtime) {
    // Create the BrowserView.
    CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
        handler, url, browser_settings, nullptr, nullptr,
        new SimpleBrowserViewDelegate());

    // Create the Window. It will show itself after creation.
    CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(browser_view));
  } else {
    // Information used when creating the native window.
    CefWindowInfo window_info;

#if defined(OS_WIN)
    // On Windows we need to specify certain flags that will be passed to
    // CreateWindowEx().
    window_info.SetAsPopup(NULL, "cefsimple");
#endif

    // Create the first browser window.
    CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
                                  nullptr, nullptr);
  }
}

對於這段代碼,我整理了如下流程,方便讀者對照閱讀:

在這個流程中,最關鍵的3個部分被我用紅色標記出來:

  1. SimpleHandler(CefClient子類);
  2. 使用CEF的窗體視圖框架創建CefBrowserView和CefWindow;
  3. 使用操作系統原生API構建窗體。

整個過程中會創建CefClient的子類實例,然后通過CEF提供的API來將CefClient和窗體結合在一起。

對於使用CEF自己的視圖框架,有如下的步驟:

  1. 首先是調用CefBrowserView::CreateBrowserView得到CefBrowserView實例,這個過程會把CefClient實例和View對象通過API綁定。
  2. 調用CefWindow::CreateTopLevelWindow,傳入CefBrowserView實例來創建窗體。

對於使用操作系統原生API創建瀏覽器窗體,主要是如下步驟:

  1. 使用CefWindowInfo設置窗體句柄
  2. 調用CefBrowserHost::CreateBrowser將對應窗體句柄的窗體和CefClient綁定起來

當然,上述兩個窗體的創建過程涉及到CEF的窗體模塊,我們不在這里細說,但是兩個流程都離不開一個重要的類:CefClient,它具體是什么呢?接下來,我們將對CefClient進行介紹,並對SimpleHandler這個類(CefClient子類)進行一定的源碼分析。

CefClient

在官方的文檔,描述了CefClien的概念:

The CefClient interface provides access to browser-instance-specific callbacks. A single CefClient instance can be shared among any number of browsers. Important callbacks include:

CefClient接口提供對特定於瀏覽器實例的回調的訪問。一個CefClient實例可以在任意數量的瀏覽器之間共享。重要的回調包括:

  • Handlers for things like browser life span, context menus, dialogs, display notifications, drag events, focus events, keyboard events and more. The majority of handlers are optional. See the documentation in cef_client.h for the side effects, if any, of not implementing a specific handler.
  • 所有的Handler,例如瀏覽器的生命周期,上下文菜單,對話框,顯示通知,拖動事件,焦點事件,鍵盤事件等。大多數處理程序是可選的。請參閱cef_client.h中的文檔,以了解不實施特定處理程序的副作用(如果有)。
  • OnProcessMessageReceived which is called when an IPC message is received from the render process. See the “Inter-Process Communication” section for more information.
  • 從渲染過程中接收到IPC消息時調用的OnProcessMessageReceived。有關更多信息,請參見“進程間通信”部分。

首先需要解釋一下什么什么是特定瀏覽器實例,實際上,指的是以下過程產生的瀏覽器實例:

    CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
        handler, url, browser_settings, nullptr, nullptr,
        new SimpleBrowserViewDelegate());
// 或
    CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
                                  nullptr, nullptr);

通過上述兩種方式創建的瀏覽器實例,是一個概念上的實例,並不是指你能看得到的瀏覽器的窗口,窗口只是瀏覽器實例的宿主而已。而瀏覽器中發生的事件,例如:生命周期的變化,對話框等,都只會通過CefClient中返回的各種類型Handler以及這些Handler接口實例提供的方法回調。

下面時CefClient的聲明:

class CefClient : public virtual CefBaseRefCounted {
 public:
  virtual CefRefPtr<CefAudioHandler> GetAudioHandler() { return nullptr; }

  virtual CefRefPtr<CefContextMenuHandler> GetContextMenuHandler() {
    return nullptr;
  }

  virtual CefRefPtr<CefDialogHandler> GetDialogHandler() { return nullptr; }

  virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() { return nullptr; }

  virtual CefRefPtr<CefDownloadHandler> GetDownloadHandler() { return nullptr; }

  virtual CefRefPtr<CefDragHandler> GetDragHandler() { return nullptr; }
    // ...... 還有很多的Handler
        
}

在這個CefClient提供了很多GetXXXHandler方法,這些方法會在合適的時候,被CEF調用以得到對應的Handler,然后再調用返回的Handler中的方法。例如,HTML頁面中的Title發生變化的時候,就會調用CefClient::CefDisplayHandler()得到一個CefDisplayHandler實例,然后再調用其中的CefDisplayHandler::OnTitleChange,而這些過程不是我們調用的,而是CEF框架完成的。只是具體的實現有我們客戶端代碼編寫。

那么現在思考一下,為什么會有這個CefClient呢?在本人看來主要是如下的理由:

在CefClient中各種回調的事件,本質上發生的地方是渲染進程。因為每當一個瀏覽器實例(不是瀏覽器進程)創建的時候,會有一個對應的渲染進程創建(也可能由於配置,而共用一個,這里先認為默認多個一對一)。渲染進程中發生的各種V8事件、下載事件,顯示事件等觸發后,會通過進程間通訊給到瀏覽器進程,然后在瀏覽器進程中找到與之相關的CefClient,然后從CefClient中找到對應的Handler,回調Handler對應的方法。

也就是說,將在渲染進程發生的事件,用在瀏覽器進程中的CefClient一定的抽象映射,而不是直接在瀏覽器進程處理器中進行,因為一個瀏覽器進程可能會創建多個渲染進程,讓CefClient作為中間層避免耦合。

當然,文檔也為我們指出,CefClient實例與瀏覽器實例可以不是一一對應的,多個瀏覽器實例可以共享一個CefClient,如此一來我們也可以總結關於CefClient的一點:非必要情況,不要編寫具有狀態的CefClient

至此,我們通過對Demo源碼入手,對CefApp和CefClient已經有了一個整體的認識,讀者可以閱讀官方文檔來更加深入的了解:官方文檔


免責聲明!

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



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