文章CEF开发环境搭建提到了如何配置cef windows开发环境, 接下来梳理开发cef过程中对其框架的理解。

什么是CEF

Chromium 嵌入式框架 (CEF, Chromium Embedded Framework) 是一个用于将基于 Chromium 的浏览器嵌入到其他应用程序中的简单框架。

CEF 是一个 BSD 许可的开源项目, 与Google Chrome 应用程序开发的 Chromium 项目不同,CEF 专注于促进第三方应用程序中的嵌入式浏览器用例。

CEF 至今存在三个版本, 而仍在开发受支持的版本是CEF3

  • CEF1, 单进程实现, 使用chromium webkit API(已停产)
  • CEF2, 基于Chromium浏览器构建的多进程实现(已停产)
  • CEF3, 使用 Chromium Content API(称为"Alloy runtime")或完整的 Chrome UI(称为"Chrome runtime")实现多进程

依赖项

CEF 项目依赖几个由第三方维护的项目, 分别是:

  • Chromium: 提供通用的功能, 比如网络栈、多线程、消息循环、日志记录及进程控制。 实现Blink与V8通讯的代码。
  • Blink: Chromium 使用的渲染实现。提供 DOM 解析、布局、事件处理、渲染和 HTML5 JavaScript API。一些 HTML5 实现分布在 Blink 和 Chromium 代码库之间。
  • V8:Javascript引擎
  • Skia: 用于渲染非加速内容的 2D 图形库。
  • Angle:适用于 Windows 的 3D 图形转换层,可将 GLES 调用转换为 DirectX。

整体架构

CEF3 使用多个进程来保护整个应用程序免受渲染引擎或其他组件中的错误和故障的影响。它还限制每个渲染引擎进程对其他进程和系统其余部分的访问。在某些方面,这为网页浏览带来了内存保护和访问控制为操作系统带来的好处。

通常主应用程序进程称为"浏览器"进程。同时创建渲染器、插件、GPU 等子进程。主进程主要处理窗口创建、UI 和网络访问,并且大多数应用程序逻辑将在浏览器进程中运行。

Blink 渲染和 JavaScript 执行发生在单独的"render"进程中。一些应用程序逻辑(例如 JavaScript 绑定和 DOM 访问)也将在渲染进程中运行。默认进程模型将为每个唯一来源(scheme + domain)生成一个新的渲染进程。其他进程将根据需要生成。

在 Windows 和 Linux 上,主进程和子进程可以使用相同的可执行文件。在 OS X 上,您需要为子进程创建单独的可执行文件和应用程序包。

基础用法

提供一个入口点函数来初始化 CEF 并运行子进程可执行逻辑或 CEF 消息循环。

构建嵌入cef程序基本流程

当主进程与子进程共用可执行程序文件的情形:

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);

  // 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;
}
  1. 在入口函数处, 进行一些基础初始化工作。 然后调用CefExecuteProcess启动子进程。 而调用CefExecuteProcess必须传入CefMainArgs参数。 可通过执行代码CefMainArgs main_args(hInstance)CefMainArgs main_args(argc, argv)(后者适用于Linux或MacOS)构建此参数。 CefExecuteProcess通过不同的命令行参数启动不同的子进程。而CefApp参数对于不同的子进程, 可以编写不同的CefApp的继承类进行处理。
  2. CefExecuteProcess之后的代码只会被浏览器进程执行。 渲染进程、插件进程、GPU进程都将不会执行后面的逻辑。
  3. 调用CefInitialize对浏览器进程进行初始化, 此过程会创建第一个浏览器实例, CefBrowserProcessHandler::OnContextInitialized被调用后,初始化完成。
  4. 在适当的时机,调用CreateBrower()CreateBrowserSync()并传入一个CefClient实例,创建浏览器窗口。
  5. 调用CefRunMessageLoop()CefDoMessageLoopWork()启动消息循环
  6. 在进程退出之前调用CefShutdown()对Cef进行一些清理工作。

当主进程与子进程为独立的可执行文件时:

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);

  // 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;
}

与单个可执行程序相比, 多个可执行程序的差异就是在主进程中无需执行CefExecuteProcess, 仅需通过CefSettingsbrowser_subprocess_path参数设置子进程可执行文件路径即可。 其他与单个可执行文件无异。

子进程可执行文件的基本逻辑如下:

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);

  // 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());
}

核心代码就是仅需调用CefExecuteProcess传入对应CefApp的类实例即可。

CEF线程

每个cef进程都是一个多线程程序, 在枚举类型cef_thread_id_t中定义了可能的线程类型。下面仅仅对几个常用的线程进行说明:

  • TID_UI: 该线程是浏览器进程中的主线程。如果使用 CefSettings.multi_threaded_message_loop 值 false 调用 CefInitialize(),则此线程将与主应用程序线程相同。
  • TID_IO: 该线程用于浏览器进程中处理IPC和网络消息。
  • TID_FILE_*: 该线程用于浏览器进程与文件系统交互。阻塞操作只能在此线程或客户端应用程序创建的 CefThread 上执行。
  • TID_RENDERER: 该线程是渲染进程中的主线程。所有 Blink 和 V8 交互都必须在此线程上进行。

消息循环

cef 消息循环处理有三种方式:

  1. 直接使用CefRunMessageLoop, 在windows平台中,该函数会处理win32的消息。 而无需单独对win32消息进一步处理了。
  2. 在Windows平台中, 如果桌面应用已经有了自己的消息循环, 可以在适当的时机调用CefDoMessageLoopWork来处理cef消息。手动调用CefDoMessageLoopWork一定确保其频率, 频率低导致cef性能降低, 高则会消耗无谓的cpu资源。
  3. 如果你的应用中已经有自己的消息循环, 可以通过设置CefSettings.multi_threaded_message_loop参数为true,来确保cef消息循环的执行, 此时cef消息循环将在独立的线程中, 与主应用程序的线程不同。 更明确的说, cef浏览器ui线程将在独立的线程中处理。 如果与cef浏览器ui线程通讯, 需要添加额外的逻辑。 可参考cef提供的示例中MainMessageLoopMultithreadedWin的处理逻辑(通过消息的方式进行同步)

常用类

CefBrowser与CefFrame

CefBrowser 和 CefFrame 对象用于向浏览器发送命令并在回调方法中检索状态信息。每个 CefBrowser 对象将有一个表示顶级框架的主 CefFrame 对象和零个或多个表示子框架的 CefFrame 对象。例如,加载两个 iframe 的浏览器将有三个 CefFrame 对象(顶级框架和两个 iframe)。

例如在主框架中加载url:

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

浏览器后退操作:

browser->GoBack();

CefBrowser 和 CefFrame 对象存在于浏览器进程和渲染进程中。可以通过 CefBrowser::GetHost() 方法在浏览器进程中控制主机行为。例如,可以按如下方式检索窗口浏览器的句柄:

// CefWindowHandle is defined as HWND on Windows, NSView* on MacOS
// and (usually) X11 Window on Linux.
CefWindowHandle window_handle = browser->GetHost()->GetWindowHandle();

还有其他方法可用于历史记录导航、加载字符串和请求、发送编辑命令、检索文本/html 内容等。

CefApp

CefApp 接口提供对特定于进程的回调的访问。重要的回调包括:

  • OnBeforeCommandLineProcessing 提供了以编程方式设置命令行参数的机会。
  • OnRegisterCustomSchemes 提供了注册自定义方案的机会。常见的方案包括http、https、ftp等等
  • GetBrowserProcessHandler 返回特定于浏览器进程的功能的处理程序,继承自CefBrowserProcessHandler, 主要处理OnContextInitialized
  • GetRenderProcessHandler 返回特定于渲染进程的功能的处理程序,继承自CefRenderProcessHandler。这包括与 JavaScript 相关的回调和进程消息。

通常会实现GetBrowserProcessHandlerGetRenderProcessHandler回调。 用于针对浏览器进程与渲染进程的逻辑处理。

CefClient

CefClient 主要用于浏览器进程,添加各种处理回调。单个 CefClient 实例可以在任意数量的浏览器之间共享。重要的回调包括:

  • 处理程序用于处理浏览器生命周期(GetLifeSpanHandler)、上下文菜单、对话框、显示通知、拖动事件、焦点事件、键盘事件等。大多数处理程序都是可选的。通常通过形如Get***Handler的接口返回对应的处理实例, 如GetLifeSpanHandlerGetDisplayHandler
  • OnProcessMessageReceived 在从渲染进程接收到 IPC 消息时调用。

浏览器生命周期

浏览器生命周期从调用 CefBrowserHost::CreateBrowser()CefBrowserHost::CreateBrowserSync() 开始。执行此逻辑的位置可以在 CefBrowserProcessHandler::OnContextInitialized()回调中或特定于平台的消息处理程序,例如 Windows 上的 WM_CREATE中。

CefLifeSpanHandler 类提供了管理浏览器生命周期所需的回调。

后台任务

可以使用 CefPostTask 系列方法在单个进程中的各个线程之间发布任务(完整列表请参阅 include/cef_task.h 头文件)。任务将在目标线程的消息循环上异步执行。

进程间通信 (IPC)

由于 CEF3 在多个进程中运行,因此有必要提供在这些进程之间进行通信的机制。CefBrowser 和 CefFrame 对象存在于浏览器和渲染进程中,这有助于促进此过程。每个 CefBrowser 和 CefFrame 对象还具有与其关联的唯一 ID 值,该值将在进程边界的两侧匹配。

如果想在进程启动时向各个进程传递数据,可以在创建时通过 CefRefPtr extra_info 参数与 CefBrowserHost::CreateBrowser 关联到特定的 CefBrowser 实例。该 extra_info 数据将通过 CefRenderProcessHandler::OnBrowserCreated 回调传递到与该 CefBrowser 关联的每个渲染器进程。

在运行时进行进程间通讯:

可以使用 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()中接收。从渲染进程发送到浏览器进程的消息在回调CefClient::OnProcessMessageReceived()中接收。

另外一种发送消息的方式是通过CefSharedProcessMessageBuilder构建消息, 通过CefSharedProcessMessageBuilder::build构建一个CefProcessMessage对象。

javascript调用c++

void V8HanderDelegate::OnContextCreated(CefRefPtr<RendererApp> app,
    CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefV8Context> context) 
{
    CEF_REQUIRE_RENDERER_THREAD();
    OutputDebugStringA("V8HanderDelegate::OnContextCreated");
    CefRefPtr<CefV8Handler> handler = new V8HandlerImpl(this);

    // Register function handlers with the 'window' object.
    auto window = context->GetGlobal();
    window->SetValue(
        CefString("sayhello"),
        CefV8Value::CreateFunction(CefString("sayhello"), handler),
        static_cast<CefV8Value::PropertyAttribute>(
            V8_PROPERTY_ATTRIBUTE_READONLY | V8_PROPERTY_ATTRIBUTE_DONTENUM |
            V8_PROPERTY_ATTRIBUTE_DONTDELETE));

    window->SetValue(CefString("sayhi"),
        CefV8Value::CreateFunction(
            CefString("sayhi"), handler),
        static_cast<CefV8Value::PropertyAttribute>(
            V8_PROPERTY_ATTRIBUTE_READONLY | V8_PROPERTY_ATTRIBUTE_DONTENUM |
            V8_PROPERTY_ATTRIBUTE_DONTDELETE));
}

在渲染进程的处理器CefRenderProcessHandler继承类中实现OnContextCreated, 在OnContextCreated中构建一个继承自CefV8Handler的v8处理类对象。然后在js的全局变量windos中添加函数。 这样就可以在js中调用对应函数来触发c++调用。

触发c++调用实在CefV8Handler::Execute回调中。 可以在这个回调中执行c++函数:

    class V8HandlerImpl final : public CefV8Handler {
    public:
        explicit V8HandlerImpl(const CefRefPtr<V8HanderDelegate>& delegate)
            : delegate_(delegate) {}
        V8HandlerImpl(const V8HandlerImpl&) = delete;
        V8HandlerImpl& operator=(const V8HandlerImpl&) = delete;

        bool Execute(const CefString& name,
            CefRefPtr<CefV8Value> object,
            const CefV8ValueList& arguments,
            CefRefPtr<CefV8Value>& retval,
            CefString& exception) override {
            if (name == "sayhello") {
                // 添加自己的处理, 注意一定return true
                return true;
            }

            if (name == "sayhi") {
                // 添加自己的处理, 注意一定return true
                return true;
            }

            return false;
        }

        IMPLEMENT_REFCOUNTING(V8HandlerImpl);
    };

c++调用javascript

c++ 调用javascript相对比较简单, 只需要得到CefFrame对象就可以执行其成员函数ExecuteJavaScript进行js调用。 在浏览器进程及渲染进程中都可以拿到该对象。 如下示例是在浏览器进程中执行js:

browser->GetMainFrame()->ExecuteJavaScript("jsfunction([1,2,3,4,5]);", browser->GetMainFrame()->GetURL(), 0);

参考