前端开发框架Chromium Embedded Framework (CEF)入门

简介

维护了一个产品,其前端框架采用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同时继承于CefAppCefBrowserProcessHandler

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是CefClientCefDisplayHandlerCefLifeSpanHandlerCefLoadHandler四者的子类。然后使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zl.rs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值