简介:CEF(Chromium Embedded Framework)是用于将Chromium内核嵌入桌面应用的成熟开源方案,这款构建包面向Windows 32位开发者,适合需要内嵌浏览器功能、自定义渲染或跨平台UI的C++项目。压缩包共972个文件,整体约224.63MB,以502个头文件与327个C++源码为主,同时提供14个DLL动态库、56个PAK资源包、HTML页面及Manifest配置等,涵盖运行时、API声明和应用资源。版本基于Chromium 90.0.4430.85,并支持H.264视频解码,可简化视频播放场景的集成工作。包内还包含V8快照、GPU相关二进制及PDF查看组件,免去自行编译Chromium的繁琐流程,能直接作为二次开发基础。该构建尤其适合已有C++工程基础、需要在Windows 32位环境下快速集成CEF3的开发者,目前已有1089人学习和下载。
1. 拿到 cef_binary_90.5.9 之后,先搞清楚这三件事
cef_binary_90.5.9+gd330790+chromium-90.0.4430.85_windows32.zip这个名字看着像一串版本号,实际说明你已经绕过了“自己编译 Chromium”这条最重的路:你要用的是别人编好的 CEF(Chromium Embedded Framework)90.5.9 构建产物,上游内核是 Chromium 90.0.4430.85,交付目标是 Windows 32 位宿主进程。CEF 90 这一代在很多存量项目里仍然被点名选用,不是因为它新,而是因为 API 相对稳定、对 Win7 的兼容还保留着,而且 32 位交付在工控机、银行柜面、医疗设备和老旧触摸一体机里至今没有退场。做接入之前需要先理解三件事:这个 zip 解开后哪些文件必须进产物、初始化 CEF 时进程是怎么被拆成主进程和子进程的、以及 90 版本里哪些功能受版权或编译选项限制(最典型的就是 H.264)。下面顺着这三件事,把一个能跑起来的宿主程序完整接出来。
2. 解压后的目录结构:Release、Resources 与 libcef_dll_wrapper 各自负责什么
2.1 目录规划:哪些文件必须随产物发布,哪些只在开发时使用
CEF 90 的 Windows 预编译包解开后,目录划分非常清晰。Release目录里是libcef.dll、cefclient.exe、cefsimple.exe以及cef_sandbox.lib之类的运行组件;Resources目录里是 Chromium 运行时不可或缺的resources.pak、chrome_100_percent.pak、icudtl.dat和locales子目录;include目录是 C++ 头文件;而libcef_dll_wrapper是一个需要你自己编译的 C++ 封装层源码,它把 C API 翻译成好用的CefRefPtr、CefBrowser这些 C++ 接口。
cef_binary_90.5.9+gd330790+chromium-90.0.4430.85_windows32/ ├── cmake/ ├── include/ # 开发时使用,C++ 头文件 ├── libcef_dll/ # 开发时编译 libcef_dll_wrapper 的源码 ├── libcef_dll_wrapper/ # C++ 封装层,参与宿主程序编译 ├── Release/ # 运行时必须随产物发布 │ ├── libcef.dll # 内核 │ ├── cefclient.exe │ ├── cef_sandbox.lib # 开启沙箱时链接 │ └── ... ├── Resources/ # 运行时必须随产物发布 │ ├── icudtl.dat │ ├── resources.pak │ └── locales/ └── tests/发布时Release里的 DLL 和Resources里的资源文件要去掉tests目录,include和libcef_dll_wrapper不需要带出去。资源文件和 libcef.dll 之间是严格依赖关系,少一个.pak,程序可能直接白屏或者渲染出方块字。另一个容易犯的错误是只拷贝libcef.dll不拷贝icudtl.dat,结果程序启动时报 ICU 数据初始化失败,这种错误在日志里表现为Check failed: result,排查起来很浪费时间。
2.2 用 CMake 把 libcef_dll_wrapper 编译进宿主程序
官方包自带的CMakeLists.txt已经把cef_dll_wrapper这个编译目标定义好了,接入时只要在你的工程里通过add_subdirectory引进来。下面是一个最小 CMake 配置,适用于把 CEF 嵌进一个 Win32 窗口程序。
cmake_minimum_required(VERSION 3.15) project(cef_win32_demo) set(CEF_ROOT "D:/libs/cef_binary_90.5.9+gd330790+chromium-90.0.4430.85_windows32") set(CEF_USE_SANDBOX OFF) add_subdirectory(${CEF_ROOT} ${CMAKE_CURRENT_BINARY_DIR}/cef_binary) add_executable(cef_demo WIN32 win32_demo.cpp) target_link_libraries(cef_demo PRIVATE cef_dll_wrapper) target_compile_definitions(cef_demo PRIVATE CEF_USE_SANDBOX=0)CEF_USE_SANDBOX设成OFF,是因为预编译包里沙箱的 lib 和运行库有严格的编译选项匹配要求,通常只有在自己维护整套构建链时才会打开。add_subdirectory会把libcef_dll_wrapper编成一个静态库并自动带上头文件路径,宿主程序链接它之后,所有CefRefPtr、CefApp等接口就能直接用了。这里有一个关键点:libcef_dll目录里的源码是要参与编译的,不能只把头文件路径配上而不编译封装层。
2.3 CefInitialize 的最小初始化代码:多线程消息循环的取舍
CEF 90 的初始化入口是CefInitialize,调用它之前必须准备好CefMainArgs和CefSettings。我一般会在 Win32 程序里优先开multi_threaded_message_loop = true,这样 CEF 内部自己跑消息循环,宿主只用维护自己的GetMessage/DispatchMessage标准循环,不需要在空闲时反复调用CefDoMessageLoopWork,代码结构清爽很多。
#include "include/cef_app.h" #include "include/cef_browser.h" #include "include/cef_client.h" #include "include/cef_sandbox_win.h" class DemoApp : public CefApp, public CefBrowserProcessHandler { public: void OnContextInitialized() override { CefWindowInfo info; CefRect rect(0, 0, 1024, 768); info.SetAsChild(GetParentHwnd(), rect); CefBrowserSettings settings; settings.background_color = 0xFFFFFFFF; CefRefPtr<CefClient> client = new DemoClient(); CefBrowserHost::CreateBrowser(info, client, "https://example.com", settings, nullptr, nullptr); } CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; } private: IMPLEMENT_REFCOUNTING(DemoApp); }; int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { CefMainArgs args(hInstance); CefSettings settings; settings.multi_threaded_message_loop = true; settings.no_sandbox = true; CefString(&settings.cache_path).FromWString(L"./cef_cache"); CefString(&settings.log_file).FromWString(L"cef.log"); CefRefPtr<DemoApp> app(new DemoApp()); if (!CefInitialize(args, settings, app.get(), nullptr)) { return -1; } // 进入宿主的 Win32 消息循环 MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } CefShutdown(); return static_cast<int>(msg.wParam); }这段代码里no_sandbox = true是预编译包接入最常见的做法:如果不设置,CEF 会尝试初始化 Windows 沙箱,而沙箱要求宿主以/MT方式链接并且正确传入sandbox_info,否则会在启动阶段直接崩溃。cache_path指定缓存目录,不指定的话 Cookie 和 LocalStorage 都存在内存里,进程退出后登录态就丢了,这在做需要持久会话的业务时很容易被当成“Bug”上报。
OnContextInitialized是创建浏览器的推荐位置,它在浏览器主进程初始化完成后、消息循环正常运行之前被回调。此时窗口句柄必须已经创建好,所以一般把创建窗口的工作放在wWinMain的前面,或者把CreateBrowser挪到WM_CREATE消息里。如果CreateBrowser返回后界面一片空白,先检查父窗口句柄是否有效,再检查resources.pak是不是放到了libcef.dll同级的目录下。
3. CefSettings 字段与命令行开关:90 版本里影响行为的 12 个参数
3.1 CefSettings 中值得显式设置的 7 个字段
CefSettings在 CEF 90 里已经包含几十个字段,但实际项目里真正需要手动调整的通常只有下面这些。它们必须在CefInitialize之前赋值,运行期修改无效。
| 字段 | 说明 | 常见取值 |
|---|---|---|
multi_threaded_message_loop | 是否用 CEF 内部消息循环 | 宿主有独立 Win32 循环时设 true |
no_sandbox | 是否启动沙箱 | 预编译包接入一般 true |
cache_path | 持久化缓存目录 | 设为工作目录下的子目录 |
log_file | 日志输出路径 | 建议显式指定,别用默认 debug.log |
log_severity | 日志级别 | 上线用 ERROR,排查用 VERBOSE |
remote_debugging_port | 开启 DevTools 端口 | 0 关闭,9222 是常见调试端口 |
locale | Chromium 界面语言 | zh-CN配合 locales/zh-CN.pak |
CefSettings settings; settings.log_severity = LOGSEVERITY_VERBOSE; settings.remote_debugging_port = 9222; CefString(&settings.locale).FromString("zh-CN"); CefString(&settings.accept_language_list).FromString("zh-CN,en-US,en");log_severity默认值是LOGSEVERITY_DEFAULT,实际输出量不大,真遇到崩溃或白屏时我会直接调成LOGSEVERITY_VERBOSE,日志里会带出viz、gpu和渲染线程的详细信息。accept_language_list影响的是网页发出去的Accept-Language请求头,做中文站时最好显式写zh-CN,否则后端根据语言分流时可能走到英文版。
3.2 用 OnBeforeCommandLineProcessing 追加启动开关
CefSettings负责的是初始化参数,而 Chromium 命令行开关要用CefApp::OnBeforeCommandLineProcessing在启动早期追加。这个回调里process_type区分主进程和子进程,空字符串表示浏览器主进程,"renderer"、"gpu-process"等表示子进程。
void OnBeforeCommandLineProcessing( const CefString& process_type, CefRefPtr<CefCommandLine> command_line) override { if (process_type.empty()) { command_line->AppendSwitch("disable-gpu"); command_line->AppendSwitch("autoplay-policy", "no-user-gesture-required"); command_line->AppendSwitch("lang", "zh-CN"); } }disable-gpu在远程桌面、虚拟机和无独显的工控机上很重要,不关 GPU 合成时会出现画面撕裂甚至直接黑屏,关掉后 CEF 走软件渲染反而更稳定。autoplay-policy设成no-user-gesture-required,页面里的视频就能在无用户手势的情况下自动播放,这一点在监控大屏和广告机项目里几乎是必开项。需要说明的是,OnBeforeCommandLineProcessing对主进程追加的开关会由 CEF 负责传给子进程,不要在回调里自己CreateProcess。
3.3 初始化前赋值与运行期调整的边界
很多参数是“初始化前赋值,运行期改无效”的,尤其是CefSettings里所有字段。但remote_debugging_port有一点特殊:你可以在运行后用CefBrowserHost::SetRemoteDebuggingPort重新设置端口,无需重启进程。这个特性在现网排查时非常有用,启动时不开调试端口,遇到问题再通过某种隐藏入口打开 9222,就可以不中断业务直接看页面状态。
cache_path和log_file则是绝对不能在运行期改的。如果程序里做了“设置项切换缓存目录”的功能,必须在重启进程后生效,否则旧进程还在用旧目录,新写入的 Cookie 会出现错乱。另一个常见的误用是以为CefBrowserSettings里的字段也能控制全局行为,实际上它只作用于CreateBrowser创建的这一个浏览器实例,比如background_color和javascript开关都只影响当前 Browser 对象。
4. 进程模型、browser_subprocess_path 与 Sandbox:白屏和随机退出的定位思路
4.1 CEF 90 的多进程结构:一个宿主进程,多个子进程
CEF 90 沿用了 Chromium 的多进程架构,程序启动后会看到浏览器主进程、渲染子进程、GPU 进程、网络进程和存储进程同时存在。任务管理器里如果只看到主程序一个进程,说明子进程根本没有被正确拉起,这是白屏问的常见根源。
主进程负责窗口管理、UI 和进程调度;渲染进程负责 HTML/CSS/JS 的执行,彼此之间通过 IPC 通信。子进程出问题不会把主进程带崩,所以表现为“页面消失但程序还活着”——看到这种现场,优先怀疑子进程崩溃而不是主程序死循环。
4.2 让宿主 exe 自己充当子进程:CefExecuteProcess 的标准写法
CEF 90 允许不单独放一个subprocess.exe,而是复用宿主程序自身。做法是在wWinMain的最前面调用CefExecuteProcess,根据返回值判断当前进程到底是主进程还是子进程。
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) { CefMainArgs args(hInstance); CefRefPtr<DemoApp> app(new DemoApp()); int exit_code = CefExecuteProcess(args, app.get(), nullptr); if (exit_code >= 0) { // 当前进程是子进程,使命结束后直接退出 return exit_code; } // 走到这里说明是浏览器主进程,继续初始化 CefSettings settings; settings.multi_threaded_message_loop = true; settings.no_sandbox = true; CefString(&settings.browser_subprocess_path) .FromWString(L"C:/path/to/cef_demo.exe"); CefInitialize(args, settings, app.get(), nullptr); // ... 进入主消息循环 }CefExecuteProcess会根据当前进程的启动参数判断自己是哪种角色,如果是 renderer 或 gpu 进程就执行对应逻辑并返回,返回值大于等于 0 时主函数直接退出。注意这里作为参数传入的app必须是同一个CefApp实例,不能在CefInitialize里 new 另一个。browser_subprocess_path如果不设置,CEF 默认会去找主程序可执行文件自身,但显式指出来更安全,尤其是宿主程序安装了多个版本时,防止子进程加载到旧版 DLL。
4.3 Sandbox 的取舍:预编译包默认关掉,别硬开
CEF 的 Windows Sandbox 需要链接cef_sandbox.lib,而且要求宿主程序的运行时库和链接选项与沙箱库匹配。预编译包里带的cef_sandbox.lib通常是用/MT编的,你如果项目是/MD,强开沙箱的结果就是启动即崩,日志里报 “Failed to initialize sandbox”。
我的做法是默认在CefSettings里no_sandbox = true,把安全重心放到宿主本身的进程隔离和资源管理上。如果产品确实需要沙箱,那就不要用预编译包,而是自己在源码构建时统一runtime_library选项,这在 CEF 90 里是件费时费力的事,收益远低于风险。
4.4 子进程崩溃后看哪份日志
子进程崩溃时不会有 Windows 错误弹窗,CEF 会写日志到log_file指定的路径。用LOGSEVERITY_VERBOSE跑一遍复现场景,重点搜FATAL、Check failed和Renderer process字样。GPU 进程崩溃通常后面会跟着GPU process launch failed的提示,先按disable-gpu验证;渲染进程崩溃则经常与页面里的 WebGL 或特定显卡驱动有关。
如果日志里看到Unable to load libcef.dll,检查当前进程位宽和 DLL 位宽是否一致。Windows 32 位包必须在 x86 进程里加载,C# 宿主项目如果是 AnyCPU,在 64 位系统上默认跑成 64 位进程,加载 32 位libcef.dll会直接报0xC000007B,这种错误看日志没用,先改平台目标为 x86。
5. 接入真实页面时的四个硬问题:H.264、海康页面、中文渲染和位宽混用
5.1 H.264/AAC 支持:CEF 90 的预编译包没有专有解码器
Chromium 90 内核本身支持 H.264/AAC,但 CEF 预编译包是否包含这些专有解码器,取决于编译时是否打开了proprietary_codecs。官方常规预编译构建默认不包含,所以你用这个 90.5.9 包接 video 标签,很可能遇到“有声音无画面”或直接黑屏。这不是 CEF 初始化错了,而是解码器根本没编进去。
验证方法可以借 DevTools 执行这段代码,这一步在去掉调试端口前就要做:
MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"'); // 返回 false 说明当前包不支持 H.264 解码如果是 false,常见做法是让服务端把视频转成video/webm; codecs="vp9"输出,或者自己带着CEF_USE_PROPRIETARY_CODECS=ON去源码构建一份 90 的包。这个决定要早做,因为换包会影响整个发布路径,不能到上线前才改。
5.2 海康摄像头页面白屏的排查顺序
很多设备页面在 IE 里正常,切到 CEF 后白屏,海康页面就是典型。Chromium 内核不支持 ActiveX,老页面引用的 ocx 控件直接加载失败;新页面如果走的是厂商提供的 Chrome 扩展通道,CEF 90 默认没有扩展系统,同样无解。遇到白屏时按这个顺序排查:
- 打开
remote-debugging-port,用 DevTools 看 Console 里有没有控件加载失败的报错。 - 如果页面里有视频流,先执行
MediaSource.isTypeSupported检查解码器。 - 页面地址如果是
http://192.168.x.x这类内网地址,检查是不是被 Mixed Content 或 Private Network Access 拦截,尝试在命令行加--disable-features=BlockInsecurePrivateNetworkRequests验证。 - 如果最终确认是 ActiveX 依赖,CEF 这条路走不通,只能换方案:后端做转码输出给 CEF,或者用厂商提供的 Web SDK 重新开发页面。
5.3 中文乱码和方块字:先查资源文件,再查字体渲染
乱码分两种。一种是 Chromium UI 本身的菜单和提示变成英文或乱码,这通常是locales/zh-CN.pak缺失,或者settings.locale没有设置成zh-CN;另一种是页面里的中文变成方框,这是字体 fallback 失败。
CefString(&settings.locale).FromString("zh-CN");如果locales目录完整,但字符还是渲染成方块,可以在OnBeforeCommandLineProcessing里追加--disable-direct-write,强制 Chromium 回退到 GDI 字体渲染。这个方法在 Windows Server 精简版和某些国产触摸一体机上经常能解决问题,副作用是字体抗锯齿会差一些。C# 宿主项目要注意默认没有特殊字体,CEF 90 在 Windows 上主要依赖Microsoft YaHei,部署机器缺字体会出现全局换字体的问题。
5.4 32 位包和宿主位宽:0xC000007B 的三种成因
这个 zip 是windows32,那么宿主程序必须是 x86 进程。0xC000007B这个错误码几乎成了 CEF 接入新手村的招牌问题,成因通常是三种。第一个是混合架构:x86 程序加载了 x64 的依赖库;第二个是缺少 VC++ 运行库,CEF 90 依赖 Visual C++ 2015-2019 Redistributable;第三个是 DLL 搜索路径不对,libcef.dll不在 exe 同目录也没在系统 PATH 里。C# 的 WinForms/WPF 项目里,把“平台目标”从 AnyCPU 改成 x86,再把libcef.dll和Resources复制到输出目录,基本能绕开前两种。
关于 ARM64,如果项目是跑在 Windows on ARM 设备上,32 位包可以通过模拟层运行,但性能会打折;热词里提到的 cef arm64 构建需要专门找 ARM64 版本,不要把 x64 或 arm64 的libcef.dll混到 32 位宿主里。判断位宽最直接的办法是用 Dependencies 打开 DLL,看目标架构。
6. 收尾技巧:用 remote-debugging-port 查 target、抓 console,不用重新编译
CEF 90 的remote_debugging_port是现网排查最重要的入口,启动时不加这个参数,等出问题再重现场景往往很难。建议在CefSettings里默认打开 9222,并只在内网环境暴露,这样每次启动浏览器就自动具备 DevTools 能力。程序跑起来后,用 PowerShell 请求本地的/json端点就能拿到当前所有页面、子窗口和扩展进程的列表。
Invoke-RestMethod -Uri "http://localhost:9222/json" | ConvertTo-Json -Depth 3返回内容里每个 target 都有title、url和webSocketDebuggerUrl。用它验证“页面是否真的加载出来”,比看截图更可靠:如果列表里 target 存在但页面白屏,说明渲染进程活着、页面 DOM 没画出来;如果列表里根本没有对应 url 的 target,说明CreateBrowser的请求根本没发出去,问题在初始化阶段而不是页面本身。
用 DevTools 协议连上 WebSocket 后,最常用的两个调试命令是Runtime.evaluate和Log.enable。前者可以在页面上下文里执行 JavaScript,比如直接读取某个全局变量、触发按钮点击,用来验证业务逻辑;后者会实时把页面 console 日志推送到调试端,搭配log_severity的VERBOSE级别,能定位到具体是哪个 JS 文件抛异常。
最后一个具体技巧:如果程序已经跑在用户机器上、不能重启进程,可以在产品里留一个隐藏入口,比如双击某个特定区域或者检测cef_debug.flag文件存在,然后运行时调用CefBrowserHost::SetRemoteDebuggingPort(9222)现场打开调试端口。这个做法不要求预先把端口暴露在生产环境,安全性和可排查性都能兼顾。连上http://localhost:9222后在另一台机器用 Chrome 打开这个地址,就能像普通浏览器一样检查 CEF 页面里的网络请求、元素和 Console,完全不需要为排查问题重新编译一份带调试开关的版本。
本文还有配套的精品资源,点击获取