简介
维护了一个产品,其前端框架采用CEF,笔者对于前端知识的了解知之甚少,所以就有了这篇CEF前端开发框架入门的文章
Chromium Embedded Framework (CEF)是一个基于Google Chromium项目的开源Web browser控件,支持Windows, Linux, Mac平台。在产品端,据 CEF 官网数据,CEF 框架装机量超过 1 亿,很多大家耳熟能详的桌面端应用都在使用 CEF 框架:QQ 桌面端、微信桌面端、网易云音乐桌面端、 MATLAB 、 FoxMail 、OBS Studio 等。也就是说,很多人的电脑上不止有一个 CEF 框架支持的项目
从产品端来看,CEF仍具有强大的生命力和广泛的应用场景。所以我们开始对CEF的探索
官方sample下载
我们以官方sample作为快速入门的例子。首先到Chromium Embedded Framework (CEF) Automated Builds下载CEF的二进制包
找到你需要的平台,我这里是在Windows上开发,所以选择了Windows 64-bit Standard Distribution
将其解压后将会有一些重要的目录
- Debug&Release目录:这两个目录下存放CEF的运行时库。官方直接提供了二进制产物,如果你想要从源码构建出CEF运行库,那么需要下载Chromium和CEF的源码,这需要你有梯子、良好的网速以及性能不错的电脑,会比较麻烦
- include目录:该目录提供了CEF运行库的头文件
- libcef_dll目录:这是cef运行库的C++封装。libcef 动态链接库导出C API使得使用者不用去关心CEF运行库和基础代码,libcef_dll_wrapper工程(即libcef_dll目录中的代码)负责把 C API 封装成 C++ API
- tests目录:这里存放了利用libcef以及libcef_dll_wrapper来编写浏览器的Demo,具体来说里面有一个复杂的例子cefclient以及一个较为简单的例子cefsimple,后面就会以cefsimple为例进行分析
接着我们开始构建工程
cmake构建工程&编译
sample源码下载完成后我们使用cmake构建工程并编译
在解压的目录中创建build的目录
命令行进入build目录后直接使用cmak构建,编译工程
cd build
cmake ..
cmake --build .
cmake会在build目录下构建并编译对应平台的工程
在Windows平台上将会生成cef.sln的VS工程
我们找到产物cefsimple.exe程序,双击或者命令行运行
cefsimple.exe --url=https://www.baidu.com
一切顺利的话你将会得到这样的一个窗口
接下来我们对cefsimple.exe的源码进行解析
主函数wWinMain
找到cefsimple.exe的源码,它在tests/cefsimple/中。只有4个.cc
文件,代码量很少。它的主函数在文件cefsimple_win.cc
中
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
int exit_code;
// ...
// CEF applications have multiple sub-processes (render, GPU, etc) that share
// the same executable. This function checks the command-line and, if this is
// a sub-process, executes the appropriate logic.
exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}
// Parse command-line arguments for use in this method.
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());
// ...
// 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 the CEF browser process. May return false if initialization
// fails or if early exit is desired (for example, due to process singleton
// relaunch behavior).
if (!CefInitialize(main_args, settings, app.get(), sandbox_info)) {
return CefGetExitCode();
}
// Run the CEF message loop. This will block until CefQuitMessageLoop() is
// called.
CefRunMessageLoop();
// Shut down CEF.
CefShutdown();
return 0;
}
上面对代码进行了一定程度的删减,剩下比较重要的部分
CefExecuteProcess
CefExecuteProcess应该在应用程序的入口函数处被调用,其标准写法如下
// CEF applications have multiple sub-processes (render, 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是多进程架构,主进程为Browser,负责窗口管理、界面绘制和网络交互;Blink引擎的渲染和Js的执行被放在一个独立的Render进程中。默认的进程模型中,每个新的标签页都会创建一个Render进程,其他进程则会按需创建
默认情况下,主应用程序,我们这里就是cefsimple.exe,cefsimple.exe会被多次启动运行各自独立的进程,但是只有一次启动是作为主进程(Browser)启动的,CefExecuteProcess就会对此进行区分
如果是子进程(渲染进程等),那么CefExecuteProcess将会阻塞执行,直到子进程结束,CefExecuteProcess会返回一个大于0的值,进入if分支return,子进程资源被操作系统回收
如果是主进程,那么CefExecuteProcess会立刻返回-1,继续执行程序剩下的代码。这能够很显而易见的得出结论,CefExecuteProcess后续的代码全部都只会运行在主进程中
SimpleApp
CefRefPtr<SimpleApp> app(new SimpleApp);
紧接着就new了一个SimpleApp
的实例,SimpleApp
的声明在simple_app.h
中
class SimpleApp : public CefApp, public CefBrowserProcessHandler {
public:
SimpleApp();
// CefApp methods:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
return this;
}
// CefBrowserProcessHandler methods:
void OnContextInitialized() override;
CefRefPtr<CefClient> GetDefaultClient() override;
private:
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(SimpleApp);
};
SimpleApp同时继承于CefApp
和CefBrowserProcessHandler
CefApp可以理解为对一个进程的抽象,这个进程可以是主进程,也可以是渲染进程。CEF运行时会在某个合适的时机调用SimpleApp重写的CefApp声明的一些虚方法
但是问题又来了,前面我们只是new了一个SimpleApp,那CEF运行时是如何将主进程和这个SimpleApp关联起来的呢?
// Initialize the CEF browser process. May return false if initialization
// fails or if early exit is desired (for example, due to process singleton
// relaunch behavior).
if (!CefInitialize(main_args, settings, app.get(), sandbox_info)) {
return CefGetExitCode();
}
答案就在CefInitialize这里,主进程运行到CefInitialize时,就会将SimpleApp的实例app与主进程进行关联
因为sample中的SimpleApp实例只会在主进程被使用,所以SimpleApp也实现了CefBrowserProcessHandler中定义的一些虚方法。主进程在运行到某个合适的时机后,也会调用SimpleApp重写的CefBrowserProcessHandler中声明的虚方法,这里需要理解的是,CefBrowserProcessHandler中的虚方法只有在主进程中才会回调,如果不是主进程,即使你重写了这些虚方法,并且也和CEF运行时进行了关联,也不会被回调
CefBrowserProcessHandler中的自定义方法
上面已经了解了CEF运行时会在合适的时机调用SimpleApp重写的CefBrowserProcessHandler中的虚方法,具体有如下
CefBrowserProcessHandler::OnContextInitialized
:在CEF上下文初始化后,在主进程的UI线程中回调该虚方法CefBrowserProcessHandler::OnBeforeChildProcessLaunch
:启动子进程时回调该方法,该方法提供修改子进程命令行参数的机会。当启动的是渲染进程时,将在主进程的UI线程上调用;当启动的是GPU进程时,将在主进程的IO线程上调用CefBrowserProcessHandler::GetDefaultClient
:获取默认的CefClient
SimpleApp中重写了OnContextInitialized,所以我们这里继续看一下SimpleApp::OnContextInitialized的实现
OnContextInitialized
void SimpleApp::OnContextInitialized() {
// ...
// SimpleHandler implements browser-level callbacks.
CefRefPtr<SimpleHandler> handler(new SimpleHandler(use_alloy_style));
// ...
// If using Views create the browser using the Views framework, otherwise
// create the browser using the native platform framework.
if (use_views) {
// Create the BrowserView.
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
handler, url, browser_settings, nullptr, nullptr,
new SimpleBrowserViewDelegate(runtime_style));
// ...
// Create the Window. It will show itself after creation.
CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(
browser_view, runtime_style, initial_show_state));
} else {
// ...
}
}
上面同样省略了不重要的代码
首先创建了SimpleHandler
的实例handler,SimpleHandler是CefClient
、CefDisplayHandler
、CefLifeSpanHandler
、CefLoadHandler
四者的子类。然后使用CEF提供的API CefBrowserView::CreateBrowserView
得到CefBrowserView
的实例browser_view
,同时该API还将CefClient实例handler和这个CefBrowserView对象browser_view进行了绑定
最后使用CefWindow::CreateTopLevelWindow
传入CefBrowserView对象后创建一个窗体
这是OnContextInitialized所做的
对于CefClient的进一步理解
上面有提到,在创建CefBrowserView对象时,使用handler和browser_view进行了绑定,handler是一个SimpleHandler,SimpleHandler是CefClient的子类,同时从CreateBrowserView API的设计也能看出,需要传入一个CefClient的实例
也就是说,通过CreateBrowserView创建一个View对象时,需要一个CefClient的实例进行绑定。这个CefClient是什么呢?
class CefClient : public virtual CefBaseRefCounted {
public:
///
/// Return the handler for audio rendering events.
///
/*--cef()--*/
virtual CefRefPtr<CefAudioHandler> GetAudioHandler() { return nullptr; }
// ...
///
/// Return the handler for browser display state events.
///
/*--cef()--*/
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() { return nullptr; }
// ...
};
首先找到CefClient的声明,它定义了非常多的Get***Handler
的虚函数
这些Get***Handler的方法会在合适的时候被CEF运行时调用,比如说某个页面的Title发生变化时,和这个页面(CefBrowserView)绑定的CefClient的GetDisplayHandler方法就会被调用,返回一个CefDisplayHandler
CefDisplayHandler定义了OnTitleChange
虚函数,我们可以重写这个虚函数,在OnTitleChange中修改窗口的Title,CEF运行时在获取到这个CefDisplayHandler后调用OnTitleChange,最终完成窗口Title的修改。这些过程不是我们调用的,而是CEF框架完成的,我们只需要完成具体的实现即可
本质上来说,CefClient就是每个渲染进程在主进程中的抽象,渲染进程中发生的各种事件都会通过进程间通信给到主进程,同时因为CefClient和渲染进程之间存在绑定关系,所以可以找到产生该事件的渲染进程绑定的CefClient,再通过CefClient获取到对应的Handler,通过Handler回调进行具体的业务处理
总结
以上对CEF官方sample中的cefsimple源码进行了分析,这还很浅显,笔者会继续对CEF进行探索并给出总结
最后,祝大家不管在哪里都能照顾好自己,身心健康,平平安安,这样你的家人、你的朋友才不会担心
新年快乐
公众号名称:zl.rs