二、起步阶段
- 入口程序
- 在上一讲中我们已经创建了一个名为 main.cpp 的程序文件,这是我们整个程序的入口文件,接下来我们先把这个文件的代码写好:
#include <windows.h> #include "App.h" //整个应用的入口函数 int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { CefEnableHighDPISupport(); CefMainArgs main_args(hInstance); CefSettings settings; int exit_code = CefExecuteProcess(main_args, nullptr, nullptr); if (exit_code >= 0) { return exit_code; } CefRefPtr<App> app(new App()); CefInitialize(main_args, settings, app.get(), nullptr); CefRunMessageLoop(); CefShutdown(); return 0; }- 这是整个应用程序的入口,应用启动后执行的第一段逻辑,我们在这段逻辑中完成了一些初始化工作、为不同的进程分派了不同的处理逻辑、开启了 CEF 的消息循环、在应用退出后释放 CEF 占用的资源等。
- 操作系统入口函数
- wWinMain是操作系统指定的应用程序的入口函数。这个函数有 4 个参数,参数前有 In 修饰符表示该参数是必填的输入参数,有 In_opt 修饰符的意思是该参数是可选的输入参数。
- 这 4 个参数都是由操作系统传递给 wWinMain 方法的:
- hInstance 是应用程序的实例句柄,也叫模块句柄;
- hPrevInstance 没有实际意义,是老版本 Windows 系统的历史遗留产物;
- lpCmdLine 是命令行参数;
- nCmdShow 表示应用程序的窗口是最小化、最大化还是正常显示。
- 这个方法返回一个 int 类型的数字,操作系统会直接抛弃这个值。但如果有外部应用唤起你这个应用,那么这个返回值对于它来说可能是有意义的,一般返回 0 表示应用程序正常退出,返回其他值表示应用程序因异常而退出。
- 应用程序初始化
- CefEnableHighDPISupport 方法启用高分屏支持,如果不调用这个方法,你的应用程序在一些高分辨率的屏幕下将显示得很模糊。
- CefMainArgs 是 CEF 对应用程序实例句柄的包装类,用于多进程启动。这里我们使用 hInstance 实例化了这个类的对象,名为 main_args 。
- CefSettings 是 CEF 的配置对象,类似日志级别、调试端口等都是通过这个对象设置的,这里我们就全部使用默认值。
- CEF 框架如何启动多个进程
- CefExecuteProcess 负责启动进程。这里需要详细介绍一下,一般我们启动一个 CEF 应用,你会发现任务管理器里有好几个进程。这些进程中除了主进程是由用户启动的外,其他子进程都是 CEF 框架通过 CefExecuteProcess 方法启动的。
- 主进程启动后,执行到此方法时,此方法会立即返回 -1 。接下去主进程就会进入 CEF 的消息循环,在适当的时候主进程会以特殊的命令行参数,多次启动你的可执行文件,这样就创建了多个子进程。子进程启动后也会执行到这个 CefExecuteProcess 方法,但子进程执行此方法会被阻塞(不会继续执行后面的逻辑),当子进程执行完它们的任务后,这个方法将返回一个大于等于 0 的值。也就是说子进程在第10行代码处就退出执行了,子进程不会执行 12~18 行代码。
- CefExecuteProcess 方法的第一个参数就是我们前面介绍的 CefMainArgs 对象,第二个参数可以是不同进程的业务处理对象,也可以为空。基于一切从简的原则,我们这里传递了一个空指针。第三个参数与 Chromium 沙箱有关,此处我们也没有设置。
- CEF 特有的智能指针
- CefRefPtr 是一个智能指针类型,它内部有一个引用计数,持有这个指针的使用者越多,计数就越高,使用完了之后,计数也会相应地减少;当没有任何使用者之后,指针就会自动释放它指向的对象。
- 这与现代 C++ 中的智能指针并没有什么明显区别,CEF 框架大量使用了类似的智能指针,减轻了开发者的心智负担。
- CEF 框架初始化及消息循环
- 接下来主进程会执行 CefInitialize 方法,这个方法负责初始化 CEF 的浏览器进程处理类(注意:后文我们提到的浏览器进程与前文提到的主进程属于同一个进程)。这个方法的第一个参数仍然是我们前面创建的 CefMainArgs 对象,第二个参数是 CefSettings 对象,第三个参数就是 App 对象的指针,这里是通过 CefRefPtr 智能指针的 get 方法获取的。第四个参数与沙箱有关,这里我们依然置空。
- CefRunMessageLoop 负责开启 CEF 消息循环,这个方法会阻塞后面代码的执行,一直到应用程序的某个地方调用了 CefQuitMessageLoop 方法之后,这个方法才会退出执行。(CefQuitMessageLoop 方法会发射应用程序退出的消息,CefRunMessageLoop 方法会收到这个消息,收到这个消息后就退出方法了。)
- CefShutdown 方法会结束主进程,释放资源。最后应用程序退出。
- 浏览器进程入口
- 操作系统调用完程序的入口函数后,CEF 框架就通过其自身的消息循环机制接管了接下来的执行工作,在上一小节中我们提到了自定义的 App 对象,并且把这个对象传递给了 CEF 的 CefInitialize 方法,CEF 框架收到这个对象之后,会把浏览器进程的一些逻辑交给 App 对象执行,也就是说 App 对象就是我们浏览器进程的入口程序。先看一下它的头文件的代码:
#pragma once #include "include/cef_app.h" class App : public CefApp, public CefBrowserProcessHandler { public: App() = default; CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; } void OnContextInitialized() override; private: IMPLEMENT_REFCOUNTING(App); }; - 头文件里的宏
- 头文件中 #pragma once 宏指令告诉编译器这个文件只会被编译一次。这是现代 C++ 编译器新增的一个指令,这个指令出现之前 C++ 开发者都是通过如下方式来保证头文件不会被重复编译的。
#ifndef _FileA #define _FileA // code #endif- 这种方式虽然可以兼容古老的低版本编译器,但书写起来繁琐,编译时要预先分析文件内容,又非常低效,所以推荐使用 #pragma once 指令。
- App 类的头文件中使用了宏 IMPLEMENT_REFCOUNTING ,这个宏为 App 类附加了一些特殊的方法,这些方法保证 App 类型的对象指针可以被 CefRefPtr 包裹。
- 头文件中 #pragma once 宏指令告诉编译器这个文件只会被编译一次。这是现代 C++ 编译器新增的一个指令,这个指令出现之前 C++ 开发者都是通过如下方式来保证头文件不会被重复编译的。
- 浏览器进程的行为
- App 类继承自 CefApp 和 CefBrowserProcessHandler 类,这两个基类提供了对浏览器进程的行为描述,比如:OnContextInitialized(浏览器进程的渲染线程初始化成功后被调用)、OnBeforeCommandLineProcessing (命令行参数被 CEF 和 Chromium 处理之前触发)等。我们在 App 类里只重写了 2 个基类方法,下面我们一个一个来介绍。
- GetBrowserProcessHandler 方法返回浏览器进程的处理类实例指针,这里我们返回了 App 类的实例指针自身(也就是它自己)。浏览器进程内可能会有多个线程在执行,任何一个线程都有可能调用这个方法。
- OnContextInitialized 方法在浏览器进程的主线程初始化成功后被调用,代表着浏览器进程已经初始化成功了。
- 窗口创建逻辑
- 我们在 OnContextInitialized 方法中创建了第一个窗口,这个方法代码在类的源码文件中完成,下面我们来看一下它的代码:
//App.cpp #include "App.h" #include "include/cef_browser.h" #include "include/views/cef_browser_view.h" #include "include/views/cef_window.h" #include "include/wrapper/cef_helpers.h" #include "WindowDelegate.h" //CEF主进程上下文环境初始化成功 void App::OnContextInitialized() { CEF_REQUIRE_UI_THREAD(); auto url = "https://www.baidu.com"; CefBrowserSettings settings; CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(nullptr, url, settings, nullptr, nullptr, nullptr); CefWindow::CreateTopLevelWindow(new WindowDelegate(browser_view)); } - 这个方法的实现逻辑稍微多一些,我们一步步详细解释这段代码的执行逻辑。
- CEF_REQUIRE_UI_THREAD() 是一个宏,这个宏保证执行此方法的是浏览器进程的主线程。
- url 字符串变量用于存放应用的首页地址。
- settings 对象,是 CEF 配置对象,它的作用我们前文已经说过了,这里同样也是只用它默认的配置。
- browser_view 是我们通过 CefBrowserView 类的 CreateBrowserView 静态方法创建了一个 BrowserView 对象。这个方法的第一个参数是一个 CefClient 类型的智能指针,它负责处理一切与页面相关的事件,比如下载、拖动、聚焦等,这里我们没有设置它的值,直接传递了一个空指针,要求 CEF 框架按照默认的行为处理页面相关的事件; url 字符串和 settings 对象作为第二个和第三个参数;第四个参数是一个用户自定义的附加信息对象,我们暂时用不到它;第五个参数是一个处理页面请求的对象,我们就使用 CEF 默认的请求处理机制,所以暂时也用不到它;第六个参数是 BrowserView 的代理对象,我们可以通过这个对象处理一些与 BrowserView 相关的事件,比如与该 BrowserView 关联的 Browser 对象创建成功的事件,为了简便,我们也没有用这个参数。
- 最后我们通过 CefWindow 类的 CreateTopLevelWindow 静态方法创建了一个窗口,这个方法只有一个参数,就是 CefWindowDelegate 对象。WindowDelegate 是我们自定义的一个类型,这个类型继承自 CefWindowDelegate 对象,所以它的实例也是 CefWindowDelegate 类型的对象,我们通过这个 WindowDelegate 对象设置窗口的大小,聚焦窗口内的页面等,接下去我们就介绍它。
- 我们在 OnContextInitialized 方法中创建了第一个窗口,这个方法代码在类的源码文件中完成,下面我们来看一下它的代码:
- 窗口代理对象
- 前文我们介绍了当浏览器进程的主线程初始化成功后,App 对象的 OnContextInitialized 方法会被执行,在这个方法的最后,我们通过 CreateTopLevelWindow 方法为 CEF 框架提供了一个窗口代理对象。 CEF 框架会把与窗口创建有关的逻辑交给这个对象来执行,接下来我们就看一下这个对象的头文件代码:
// WindowDelegate.h #pragma once #include "include

本文介绍了CEF框架的基础知识,包括创建程序的入口函数,初始化CEF框架,以及自定义协议处理本地资源的方法。通过CefApp和CefBrowserProcessHandler,我们可以控制浏览器进程的行为,而CefResourceHandler则用于处理自定义协议的资源请求。
最低0.47元/天 解锁文章
1673





