本小節主要了解CEF的多程序模型,為後面的JavaScript與C++ 通信做一些知識儲備,順便再次梳理一下CEF的啟動流程。
1.任務管理器觀察多程序
CEF基于Chromium,是多程序模型。怎麼了解多程序?啟動一個Chrome浏覽器,多打開幾個網站,然後再打開資料總管檢視Chrome的程序,現在對任務管理器做一些調整,目的是檢視每個程序所啟動的指令行以及指令行所攜帶的參數。如下圖:勾選 “PID”和“指令行”
此時可以看到Chrome浏覽器開啟的程序:
可以看到Chrome浏覽器啟動了多個程序,
2852
這個程序的指令行後面并沒有攜帶任何其它參數,它就是browser程序 ,而其它程序如
4296
這個程序攜帶了
--type=renderer ...
多個參數,它是 render程序
打開CEF的自帶的cefsimple 應用也能看到多個程序的存在。那麼這些程序是怎麼啟動的?都有哪些作用?
2.Chromium的程序分類
- browser 沒有type參數時預設為browser程序
- renderer 渲染程序
- plugin 插件程序
- ppapi-broker
- ppapi
- sandbox-ipc
- utility
- zygote
- gpu-process
這裡我們必須要了解的是 browser 程序 和 render程序
browser程序: 處理視窗建立、視窗繪制、網絡互動以及大部分的主要邏輯。browser程序通常就是宿主程序。
render程序: 每個頁面都是運作在自己的程序裡,這些程序我們稱之為render程序。render程序會在視窗中渲染出web頁面(引用了CSS,JavaScript,圖檔等的HTML檔案)。
需要注意的是,browser 程序中會進行視窗繪制,并不是指繪制HTML内容,而是承載網頁内容的那個窗體殼,同樣render程序也不是用來建立窗體的程序。
3.程序怎麼啟動
檢視sefsimple示例程式的入口函數:
// ...其它省略
int exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
if (exit_code >= 0) {
return exit_code;
}
// ...其它省略
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());
CefSettings settings;
settings.no_sandbox = true;
CefRefPtr<SimpleApp> app(new SimpleApp);
// 初始化CEF
CefInitialize(main_args, settings, app.get(), sandbox_info);
// 開啟消息循環
CefRunMessageLoop();
// 釋放CEF
CefShutdown();
// ...其它省略
建立程序是用
CefExecuteProcess
函數建立的。這個函數在
libcef_dll_wrapper.cc
中,它内部又調用了cef_execute_process方法(libcef_dll.cc),cef_execute_process又調用了libcef/browser/context.cc檔案内實作的CefExecuteProcess方法。源碼中有這樣一句代碼:
int CefExecuteProcess(const CefMainArgs& args,
CefRefPtr<CefApp> application,
void* windows_sandbox_info) {
base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
// 省略...
// If no process type is specified then it represents the browser process and
// we do nothing.
std::string process_type =
command_line.GetSwitchValueASCII(switches::kProcessType);
if (process_type.empty())
return -1;
CefMainDelegate main_delegate(application);
// Execute the secondary process.
// 省略...
content::ContentMainParams params(&main_delegate);
params.instance = args.instance;
// 省略...
params.argc = args.argc;
params.argv = const_cast<const char**>(args.argv);
return content::ContentMain(params);
#endif
它分析了指令行參數,提取”type”參數,如果為空,說明是Browser程序,傳回-1,sefsimple 入口函數繼續向後執行,建立了
SimpleApp
它實作了
CefApp
接口,給了我們控制CEF的入口,接着就進行了初始化。初始化完畢之後,回調了
SimpleApp::OnContextInitialized()
方法,在這個方法中我們建立了 浏覽器,并用到了
SimpleHandler
而它實作了
CefClient
如果”type”參數不為空,做一些判斷,最後調用了content::ContentMain方法,建立子程序,直到這個方法結束,子程序才結束。
編譯後的可執行檔案為"sefsimple.exe", 在程序管理器中可以看到每個程序都是從 sefsimple.exe開始的,隻不過是參數不同而已。
觀察CefExecuteProcess 方法的第二個參數,它是一個CefApp 類型的,在我們自己的例子程式和sefsimple 中都傳遞了NULL,實際上它也可以為它傳遞一個 CefApp對象。
分析完這些,現在我們清楚了,Browser程序,需要CefApp(SimpleApp實作了這個接口)和CefClient(SimpleHandler實作了這個接口)。而Renderer程序隻要CefApp。 前面我們自己編寫的示例和sefsimple 都傳遞了NULL。
現在再打開示例程式cefclient的入口函數:
int RunMain(HINSTANCE hInstance, int nCmdShow) {
// ...省略
CefMainArgs main_args(hInstance);
void* sandbox_info = nullptr;
// Create a ClientApp of the correct type.
CefRefPtr<CefApp> app;
ClientApp::ProcessType process_type = ClientApp::GetProcessType(command_line);
if (process_type == ClientApp::BrowserProcess)
app = new ClientAppBrowser();
else if (process_type == ClientApp::RendererProcess)
app = new ClientAppRenderer();
else if (process_type == ClientApp::OtherProcess)
app = new ClientAppOther();
// Execute the secondary process, if any.
int exit_code = CefExecuteProcess(main_args, app, sandbox_info);
if (exit_code >= 0)
return exit_code;
// ...省略
return result;
}
這段代碼上我們看到了這段代碼根據 程序類型,建立了不同的CefApp,看看
ClientAppBrowser
(建立browser程序用)和
ClientAppRenderer
(建立render程序用) 有什麼不一樣的?
// client_app_browser.h
class ClientAppBrowser : public ClientApp, public CefBrowserProcessHandler {
//...
}
// client_app_renderer.h
class ClientAppRenderer : public ClientApp, public CefRenderProcessHandler {
//...
}
可以看到明顯的:
browser程序 需要 CefApp和 CefClient, CefApp要提供CefBrowserProcessHandler 接口,
render程序 隻需要 CefApp ,CefApp要提供 CefRenderProcessHandler 接口。
CefBrowserProcessHandler
接口用來處理 browser程序 的一些回調,
CefRenderProcessHandler
接口用來處理 render程序 的一些回調。
我們自己的例子和 sefsimple 示例程式沒有針對Render程序做特别處理,因為 CefApp 沒有實作CefRenderProcessHandler 接口,是以會缺失一部分功能。當我們需要在JavaScript中調用 C++代碼時,這就完成不了了。
4 單程序啟動
我們在開發的時候,如果在多程序的情況下,調試将變得比較困難,有沒有辦法讓我們開發的時候在單程序中運作?答案是為添加啟動參數。
4.1 快捷方式上添加啟動參數
我們前面開發的示例,Debug目錄中為可執行檔案建立一個快捷方式,然後為快捷方式添加啟動參數==–single-process==
4.2 VisioStudio 2019中添加啟動參數
在指令參數後面添加 –single-process
再次啟動後,發現任務管理器中就隻有一個程序了。
5 單一執行體與分離執行體
前面我們自己編寫的例子,cefsimple 和cefclient 這兩個例子都使用的是單一執行體。所謂單一執行體就是啟動同一個 exe檔案來分别建立不同的程序。(多程序)
當以單一執行體運作時,如我們自己的例子和cefsimple ,cefclient 它們都是先執行程序,然後再初始化的。
所謂分離執行體就是啟動 browser程序 的是一個exe檔案,而其它程序比如render程序 執行的是另外一個exe,它是先初始化,告訴 browser程序(主程序)分離的執行體是哪個exe檔案,然後再執行子程序。當使用獨立的子程序執行體時,我們需要2個分開的可執行工程和入口函數。
下面就看看如何開發分離執行體的應用。
5.1 分離執行體項目
5.1.1 建立分離體項目
在原有的解決方案中再添加一個子項目,名稱為QyRender,将來的分離執行體就是
QyRender.exe
。 這個項目使用 QT Console Application ,因為它不會用到圖形界面
添加項目配置include目錄和lib引用
告訴作業系統使用Windows的方式運作這個exe,如果不設定這一步,當這個程序啟動後,會彈出控制台視窗。
5.1.2 實作CefApp,CefRenderProcessHandler
我們說建立渲染程序的時候,為CefExecuteProcess 函數傳遞一個 CefApp,用于将來JavaScript與C++的通信,暫時這個類中什麼也不做,先放在這裡
// QyAppRender.h
#include <qobject.h>
#include "include/cef_app.h"
class QyAppRenderer :public CefApp, public CefRenderProcessHandler {
public:
QyAppRenderer();
//重寫CefApp 中的GetRenderProcessHandler方法
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() OVERRIDE{
return this;
}
//實作 CefRenderProcessHandler 接口中的方法
void OnBrowserCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefDictionaryValue> extra_info) OVERRIDE;
private:
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(QyAppRenderer);
};
// QyAppRender.cpp
#include "QyAppRenderer.h"
/// <summary>
/// 構造方法空實作
/// </summary>
QyAppRenderer::QyAppRenderer() {
}
/// <summary>
/// 當CefBrowser對象已經建立的時候回調,将來JS與c++通信的時候會用到,現在隻做空實作
/// </summary>
/// <param name="browser"></param>
/// <param name="extra_info"></param>
void QyAppRenderer::OnBrowserCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefDictionaryValue> extra_info) {
}
5.1.3 分離體項目main函數
分離器項目的main函數很簡單,拿到指令行參數,建立 CefApp (QyAppRenderer)後執行CefExecuteProcess即可
#include "include/cef_app.h"
#include "include/cef_command_line.h"
#include "QyAppRenderer.h"
int main(int argc, char *argv[])
{
HINSTANCE hInstance = GetModuleHandle(nullptr);
//構造指令行
CefMainArgs main_args(hInstance);
// Optional implementation of the CefApp interface.
// 可選擇性地實作CefApp接口
CefRefPtr<QyAppRenderer> app(new QyAppRenderer);
// Execute the sub-process logic. This will block until the sub-process should exit.
// 執行子程序邏輯,此時會堵塞直到子程序退出。
return CefExecuteProcess(main_args, app.get(),nullptr);
}
5.2 主項目main函數
在主項目的 main函數中,就不需要執行 CefExecuteProcess了,隻需要在 CefSettings中配置執行體exe檔案路徑即可。主項目啟動的就是browser 進行它隻需要執行初始化cef和開始消息循環就完事了
#include "mainwindow.h"
#include <QtWidgets/QApplication>
#include "include/cef_command_line.h"
#include "include/cef_sandbox_win.h"
#include "cef/simple_app.h"
#include <qdebug.h>
int main(int argc, char *argv[])
{
// Enable High-DPI support on Windows 7 or newer.
CefEnableHighDPISupport();
// 通過GetModuleHandle 擷取 HINSTANCE
HINSTANCE hInstance = GetModuleHandle(nullptr);
// 開啟 QT 消息循環
SimpleApp* cefApp=new SimpleApp;
QApplication a(argc, argv);
//CEF 指令行參數
CefMainArgs main_args(hInstance);
CefSettings settings;
settings.no_sandbox = true;
settings.multi_threaded_message_loop = true;
//分離的執行體
QString executerPath = QApplication::applicationDirPath().append("\\QyRender.exe");
CefString(&settings.browser_subprocess_path).FromASCII(executerPath.toStdString().c_str());
MainWindow w(cefApp, nullptr);
w.show();
CefRefPtr<SimpleApp> app(cefApp);
// 初始化CEF
CefInitialize(main_args, settings, app.get(), nullptr);
int ret = a.exec();
// Shut down CEF.
CefShutdown();
return ret;
}
這裡的重點是 settings.browser_subprocess_path設定為執行體檔案路徑,去掉CefExecuteProcess 函數
5.3 編譯整個解決方案
編譯以後,會把可執行檔案放入到解決方案的Debug目錄中。
可以看到有兩個可執行檔案,現在啟動 QyCefVS.exe, 檢視程序:
注意分離的 QyRender雖然名字有個render,但是它是可以啟動除了 browser 程序以外所有程序的。
代碼請通路 GitHub qt_cef_07分支