
简介CEFChromium 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.9gd330790chromium-90.0.4430.85_windows32.zip这个名字看着像一串版本号实际说明你已经绕过了“自己编译 Chromium”这条最重的路你要用的是别人编好的 CEFChromium Embedded Framework90.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.9gd330790chromium-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.9gd330790chromium-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_SANDBOX0)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; CefRefPtrCefClient client new DemoClient(); CefBrowserHost::CreateBrowser(info, client, https://example.com, settings, nullptr, nullptr); } CefRefPtrCefBrowserProcessHandler 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(Lcef.log); CefRefPtrDemoApp 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_castint(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 循环时设 trueno_sandbox是否启动沙箱预编译包接入一般 truecache_path持久化缓存目录设为工作目录下的子目录log_file日志输出路径建议显式指定别用默认 debug.loglog_severity日志级别上线用 ERROR排查用 VERBOSEremote_debugging_port开启 DevTools 端口0 关闭9222 是常见调试端口localeChromium 界面语言zh-CN配合 locales/zh-CN.pakCefSettings 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, CefRefPtrCefCommandLine 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); CefRefPtrDemoApp 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(LC:/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; codecsavc1.42E01E); // 返回 false 说明当前包不支持 H.264 解码如果是 false常见做法是让服务端把视频转成video/webm; codecsvp9输出或者自己带着CEF_USE_PROPRIETARY_CODECSON去源码构建一份 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-featuresBlockInsecurePrivateNetworkRequests验证。如果最终确认是 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完全不需要为排查问题重新编译一份带调试开关的版本。本文还有配套的精品资源点击获取