简介:面向仍需在 Windows 32 位环境下嵌入浏览器内核、并依赖 NPAPI 插件能力的 C++ 开发者,这份备份包保留了 CEF 2357 分支中支持 NPAPI 的最高官方版本,适合旧项目维护、兼容性验证与源码级排错。压缩包共 580 个文件,约 120.41MB,以 310 个 .h 头文件和 156 个 .cc 实现文件为主体,辅以 pak 资源、dll/lib/bin 运行时与库文件,以及 html、txt 示例和说明,覆盖头文件、封装层、示例程序与运行资源等层次。已有 516 人学习或下载,说明该版本在旧版 CEF 迁移与 NPAPI 兼容场景中仍有稳定需求。其价值在于官方渠道已难以获取,包内保留 C++ 封装层、消息路由、离屏窗口与客户端处理等模块,可用于编译集成、接口对照、行为调试与旧工程复现,帮助读者减少版本缺失带来的环境搭建与排错成本。
1. 32 位 CEF 3.2357 这份包,为什么还有人专门回头找它
手上有个只出 32 位版本的 NPAPI 插件,宿主是早年用 C++ 写的桌面客户端,现在要把内嵌的旧浏览器控件换掉——这类需求一搜,最后基本都会落到cef_binary_3.2357.1271.g8e0674e_windows32上。它是 CEF(Chromium Embedded Framework)的 Windows 32 位二进制分发包,构建号 1271,落在 Chromium 45 那条线上,也正好卡在 NPAPI 从内核里退场的前一站。包里没有 Chromium 源码树,只有现成的libcef.dll、完整的 C++ 头文件、libcef_dll_wrapper源码和一套 CMake 工程,解压就能编。
找它的人通常分两类:一类是必须让老插件继续跑的客户端维护者,另一类是想把 Web 界面塞进 C++ 程序、又不想花几个小时编译 Chromium 的开发者。它给的不是“最新最好”,而是“这条老链路到底还能不能跑通”。
2. 解压到第一个窗口:CMake 出 VS 工程、编 wrapper、跑通 main
拿到 zip 的第一件事不是急着打开 sln,而是先认清目录分工。这份二进制包的布局和源码包差别很大,直接照网上“从 depot_tools 开始”的教程走会卡在第一步,因为你根本没有chromium/src那棵树。
2.1 解压后先认清五个目录,别乱动
把 zip 解到一个短路径下,比如D:\cef\cef32,路径里不要有中文和空格——CMake 生成 VS 工程时对空格的处理在这个年代并不算好,血泪经验是路径带空格会在cef_variables.cmake里拼出一串奇怪的引号问题。
| 目录/文件 | 里面是什么 | 要不要动 |
|---|---|---|
Release/、Debug/ | libcef.dll、libcef.lib、cef.pak、icudtl.dat、locales/等运行期文件 | 不动,最终要拷到 exe 旁边 |
include/ | cef_app.h、cef_browser.h、cef_client.h、cef_render_handler.h等头文件 | 不动,加进包含路径 |
libcef_dll/ | wrapper 源码,把 CEF 的 C 接口包成 C++ 类 | 要自己编译成静态库 |
cmake/、CMakeLists.txt | 变量定义和工程生成脚本 | 不动 |
tests/cefclient、tests/cefsimple | 官方示例,最可靠的参照物 | 可以先编一个跑跑看 |
为什么一定要选 32 位这一份?因为 Windows 上进程位数是硬约束:64 位宿主进程LoadLibrary一个 32 位 DLL,系统直接返回失败,这不是兼容性设置能绕过去的。你的插件只有 32 位版本,宿主、libcef.dll、插件 DLL 三者就必须全部对齐到 x86。反过来,如果你的机器是 64 位 Windows,32 位进程照样能跑,只是吃不满内存——这一点后面会变成真实的坑。
2.2 用 CMake 生成 VS 工程并单独编 wrapper
CEF 的 C++ 封装不是 header-only,必须先把libcef_dll_wrapper编出来,否则你写多少代码都停在 LNK2019。这个包对应的年代主推 VS2013(v120),用 VS2015 也能编,但要保证运行期 CRT 一致。
:: 设置 CEF 根目录,注意用绝对路径 set CEF_ROOT=D:\cef\cef32\cef_binary_3.2357.1271.g8e0674e_windows32 cd /d %CEF_ROOT% mkdir build cd build :: -G 指定 VS2013 生成器,-A Win32 明确 32 位平台 cmake -G "Visual Studio 12 2013" -A Win32 -DCMAKE_BUILD_TYPE=Release .. :: 只编 wrapper,先验证工具链没问题 cmake --build . --config Release --target libcef_dll_wrapper三个参数值得说清楚:-G决定生成哪一版.sln,写错版本后面打开工程会提示升级;-A Win32是 CMake 3.1 之后才有的平台选择,如果你的 CMake 太老没有-A,就得改用-G "Visual Studio 12 2013 Win32"这种老写法;--target libcef_dll_wrapper只编这一个目标,比全量 build 快得多,也更容易定位错误。编完在build\libcef_dll_wrapper\Release\下应该能看到libcef_dll_wrapper.lib。
提示:CMake 配置阶段如果报找不到
CEF_ROOT,不是环境变量没生效,而是 CMake 缓存里存了旧路径,删掉build目录重来一次最省事。
2.3 最小可跑骨架:CefExecuteProcess 必须在最前面
有了 wrapper,写一个能弹出窗口的最小程序大约七十行。核心结构是CefApp管进程级回调,CefClient管浏览器实例回调,CefLifeSpanHandler管生命周期。下面这段可以直接抄,放进一个空的 Win32 项目里。
#include "include/cef_app.h" #include "include/cef_browser.h" #include "include/cef_client.h" #include "include/wrapper/cef_helpers.h" // 生命周期回调:拿到 browser 对象,关闭时退出消息循环 class SimpleHandler : public CefClient, public CefLifeSpanHandler { public: CefRefPtr<CefBrowser> browser_; CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; } void OnAfterCreated(CefRefPtr<CefBrowser> browser) override { CEF_REQUIRE_UI_THREAD(); browser_ = browser; // 必须持有引用,否则对象被回收 } void OnBeforeClose(CefRefPtr<CefBrowser> browser) override { CEF_REQUIRE_UI_THREAD(); browser_ = nullptr; CefQuitMessageLoop(); // 最后一个窗口关闭才退出 } IMPLEMENT_REFCOUNTING(SimpleHandler); }; // 应用级:这里先空着,后面挂插件开关会用到 class SimpleApp : public CefApp, public CefBrowserProcessHandler { public: CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; } IMPLEMENT_REFCOUNTING(SimpleApp); }; int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) { CefMainArgs main_args(hInstance); CefRefPtr<SimpleApp> app(new SimpleApp); // 关键:先跑子进程逻辑,返回 >=0 说明当前进程是子进程,直接退出 int exit_code = CefExecuteProcess(main_args, app.get(), nullptr); if (exit_code >= 0) return exit_code; CefSettings settings; settings.no_sandbox = true; // 调试期先关沙箱,避免权限问题干扰 CefString(&settings.log_file).FromASCII("debug.log"); settings.log_severity = LOGSEVERITY_INFO; if (!CefInitialize(main_args, settings, app.get(), nullptr)) return 1; CefWindowInfo info; info.SetAsChild(::GetConsoleWindow(), CefRect(0, 0, 800, 600)); // 替换成自己的 HWND CefBrowserSettings browser_settings; CefRefPtr<SimpleHandler> handler(new SimpleHandler); CefBrowserHost::CreateBrowser(info, handler.get(), "about:blank", browser_settings); CefRunMessageLoop(); // 阻塞在这里,直到 CefQuitMessageLoop CefShutdown(); return 0; }CefMainArgs在 Windows 上接收的是HINSTANCE,不是argc/argv,这一点和另外两个平台不一样,抄跨平台示例时最容易在这里翻车。settings.no_sandbox打开是为了让子进程能正常启动,正式发布时要不要关沙箱取决于你的进程权限模型,但调试阶段先关掉能排掉一大半“启动就退”的问题。
CefExecuteProcess那一行的位置不能动。它必须在CefInitialize之前,而且如果返回值大于等于 0,说明当前进程是被拉起来的渲染子进程,此时应该立刻return,绝不能再往下创建窗口。很多人把这一行放到CefInitialize之后,结果就是浏览器能开但页面永远白屏,因为子进程被当成主进程又初始化了一遍。
注意:上面
SetAsChild的第二个参数类型以这版头文件为准。3.2357 的cef_window_info.h里用的是 Win32 的RECT,不是后来才有的CefRect,直接抄新版示例会编译报错。
3. 多进程骨架:子进程放哪、CefSettings 怎么填、浏览器怎么嵌进 HWND
能跑起来之后,真正决定这个内嵌浏览器好不好用的是两件事:进程模型和窗口嵌入方式。这两块参数填错,表现出来就是白屏、焦点乱跳、关闭后进程残留,而且日志里什么都不说,像个黑匣子。
3.1 为什么子进程入口和主进程是同一个 exe
CEF 建在 Chromium 的多进程架构上:一个 browser 进程管 UI 和网络,若干 render 进程管页面解析和 JS 执行。默认配置下,CEF 会把自己这个 exe 再拉起来一次当子进程,靠命令行参数区分角色。这就是为什么CefExecuteProcess必须放在入口最前面——它负责识别“我这次是被拉起来的渲染进程”,然后跑对应的消息循环。
常见的两种部署方式:一是单 exe,browser_subprocess_path留空,CEF 直接用当前 exe;二是拆成两个 exe,主程序叫client.exe,子进程叫client_subprocess.exe,通过CefString(&settings.browser_subprocess_path).FromASCII("client_subprocess.exe")指定。后者在需要给主 exe 加壳、签名或者做权限隔离时更常见。
单进程模式(settings.single_process = true)能省掉子进程,但代价明确:页面崩溃会带着宿主一起崩,插件行为和渲染时序也会变得难以预测,某些依赖独立渲染进程的特性直接不可用。除非你的宿主是个短生命周期的工具,否则不建议开。
3.2 CefSettings 里真正影响行为的七个字段
这个版本的CefSettings字段不少,但日常需要动的就那么几个。下表按“改错了会怎样”的顺序排。
| 字段 | 常用取值 | 影响 | 什么时候改 |
|---|---|---|---|
single_process | false | 是否单进程运行 | 只在极简工具里开,正式产品保持 false |
multi_threaded_message_loop | false | true 时 CEF 跑独立 UI 线程,不能再调CefRunMessageLoop | 宿主已有自己的消息泵时开 |
windowless_rendering_enabled | false | 开启离屏渲染,配合CefRenderHandler | 需要把画面画到自己的 D3D/纹理上时开 |
no_sandbox | 调试true | 关闭渲染进程沙箱 | 上线前评估,调试期先关 |
browser_subprocess_path | 空或子进程 exe 路径 | 子进程启动位置 | 主 exe 需要加壳/签名时填 |
log_severity | LOGSEVERITY_INFO | 日志详细程度 | 排查期调到VERBOSE,上线调回 |
command_line_args_disabled | false | 是否忽略外部命令行 | 需要自己控制启动参数时保持 false |
multi_threaded_message_loop这个开关最容易出事。设成 true 之后,CEF 的 UI 线程不再是你创建窗口的那个线程,所有涉及 browser 对象的调用都必须CefPostTask(TID_UI, ...)投递过去,否则就是随机崩溃。我一般先在单线程模式(false)下把功能跑通,确认逻辑没问题,再考虑要不要切多线程去适配宿主的消息泵。
日志那两个字段是排查期最值钱的配置。log_severity调到LOGSEVERITY_VERBOSE之后,debug.log里会记录子进程启动命令、资源文件加载路径、插件扫描结果,很多“什么都没发生”的问题在这里能看到原因。
3.3 把浏览器塞进自己的 HWND:坐标、层级和 Resize
SetAsChild的本质是把 CEF 创建的窗口设成你某个 HWND 的子窗口,然后把这块矩形区域交给它自己绘制。三个细节决定成败。
第一是坐标。传给SetAsChild的矩形必须是父窗口的客户区坐标,不是屏幕坐标,也不是父窗口在屏幕上的位置。传屏幕坐标的典型症状是:窗口移动之后浏览器内容跑到别的地方去了,或者干脆看不见。
第二是父窗口样式。父窗口需要带WS_CLIPCHILDREN,否则重绘时父窗口的背景刷子会盖住子窗口,表现为拖动窗口时浏览器区域闪烁或者变黑。
第三是尺寸变化。窗口大小变了之后,CEF 不会自动知道,必须显式通知:
// 在父窗口的 WM_SIZE 处理里调用 if (handler_ && handler_->browser_) { CefRefPtr<CefBrowserHost> host = handler_->browser_->GetHost(); host->WasResized(); // 让 CEF 重新计算视口尺寸 } // 需要主动重绘时再补一句 // host->Invalidate(PET_VIEW);WasResized()是必需项,Invalidate()只在离屏渲染或者画面卡住时才有必要。有人图省事在WM_SIZE里又是SetWindowPos又是WasResized又是Invalidate三连,结果页面疯狂重排,CPU 直接拉满。
另外,浏览器窗口本身的 z-order 和焦点需要宿主接管:点击进入浏览器区域时,焦点会被 CEF 自己的窗口吃掉,你的快捷键处理就失效了。常见做法是在CefKeyboardHandler::OnPreKeyEvent里拦截你关心的组合键返回 true,其余放行。
4. NPAPI 插件挂载:3.2357 的插件加载路径与 32 位对齐
这一章是很多人专门回头找这个版本的全部理由。但得先把预期摆正:这个分支“还留着插件加载路径”不等于“插件放进去就会跑”。默认策略已经收紧到相当保守的程度,能不能加载,取决于你有没有把该开的开关开全。
4.1 这条线为什么还有插件加载路径
Chromium 45 是 NPAPI 正式退场前的那一版,CEF 3.2357 就建在这条线上。再往后的分支,内核里已经没有这条路径了,你把插件 DLL 放在哪儿都不会被扫描。这也是为什么这个包在 2024 年之后还有人翻出来用——不是不想升级,是升级之后那条路物理上不存在了。
但退场前的版本已经做了两轮收紧:默认不加载过期插件,默认不自动授权插件运行,插件目录扫描范围也被限制。所以完整的链路是三步:让内核愿意扫描这个目录 → 让页面里的<embed>/<object>被授权执行 → 让插件 DLL 的位数和依赖都对上。三步缺一步,页面上就是一个灰块或者“该插件不受支持”。
4.2 在 OnBeforeCommandLineProcessing 里补开关
CEF 提供了CefApp::OnBeforeCommandLineProcessing,在命令行解析完成后、进程初始化之前给你一次改参数的机会。这是这个版本里挂插件最靠谱的入口。
void SimpleApp::OnBeforeCommandLineProcessing( const CefString& process_type, CefRefPtr<CefCommandLine> command_line) override { // process_type 为空表示 browser 进程;渲染进程也会走这个回调,别重复追加 if (!process_type.empty()) return; // 允许加载被标记为过期的插件 command_line->AppendSwitch("allow-outdated-plugins"); // 页面里的插件无需用户点击授权即可运行 command_line->AppendSwitch("always-authorize-plugins"); // 指定额外的插件扫描目录,注意这是目录不是单个 dll command_line->AppendSwitchWithValue("extra-plugin-dir", "C:\\MyPlugins"); }三个开关的含义依次是:绕开过期插件黑名单、跳过每次运行的授权确认、把指定目录加入扫描范围。process_type的判断不能省,渲染子进程也会触发这个回调,重复追加不会立刻出错,但会让日志里出现两份相同的启动参数,排查时很容易误判。
extra-plugin-dir的值必须是一个目录的绝对路径,插件 DLL 放在这个目录下面,不要把 DLL 路径直接写进去。另外这个目录不要放在网络盘或者带中文的路径下,插件加载失败的时候日志里只会写“找不到模块”,不会告诉你是路径编码问题。
提示:这几个开关是内核级的,改完必须整个进程重启才生效。改代码之后只刷新页面是没用的,我在这上面浪费过一个下午。
4.3 位数与依赖:用 dumpbin 把三份 DLL 摊开看
前面反复说过位数要对齐,但“对齐”这件事得能验证,不能靠猜。Windows SDK 自带的dumpbin就够用,在 VS 的开发人员命令提示符里执行。
:: 看机器类型:x86 显示 14C machine (x86),x64 显示 8664 machine (x64) dumpbin /headers C:\MyPlugins\myplugin.dll | findstr /i machine dumpbin /headers D:\cef\cef32\...\Release\libcef.dll | findstr /i machine dumpbin /headers YourClient.exe | findstr /i machine :: 看依赖:重点是 CRT 版本和系统 DLL dumpbin /dependents C:\MyPlugins\myplugin.dll三份输出都应该是 x86。如果插件的输出是 8664,那不管你怎么配开关都不会加载,唯一的办法是找插件方要 32 位版本或者换宿主架构。
依赖这块的坑更隐蔽:老插件经常链的是MSVCR71.dll、MSVCP90.dll这种上古 CRT,或者干脆静态链接。dumpbin /dependents能把这些名字列出来,缺哪个就补哪个,补的时候同样要注意 32/64 位。有些插件还依赖atl100.dll、mfc120u.dll,一并放进插件目录或者系统目录。
下图这种检查我一般做成一个批处理,每次换插件版本都跑一遍:
| 检查项 | 命令 | 期望结果 |
|---|---|---|
| 宿主位数 | dumpbin /headers client.exe | 14C machine (x86) |
| CEF 位数 | dumpbin /headers libcef.dll | 14C machine (x86) |
| 插件位数 | dumpbin /headers plugin.dll | 14C machine (x86) |
| CRT 依赖 | dumpbin /dependents plugin.dll | 列出的 DLL 都能在目录或系统里找到 |
这套检查看着笨,但它是唯一能在“页面就是不显示插件”这种情况下给出确定答案的手段。日志不会告诉你位数不匹配,它只会告诉你加载失败。
5. 避坑排查:五条血泪记录,从闪退到白屏
下面这五条是按我踩到的频率排的,每条都给出可复现的现象、根因和具体改法。如果你的症状对得上,直接照第三条和第四条改,能省掉大半时间。
5.1 启动即退出,窗口一闪而过
现象:双击 exe,进程在任务管理器里闪一下就消失,debug.log里出现Failed to load locale pack file或Could not find cef.pak。
原因:运行期资源文件没有跟着 exe 走。CEF 的 DLL 加载成功不代表资源能加载,cef.pak、icudtl.dat、locales/这些是运行期按相对路径找的,工作目录不对就全部找不到。
解决:把Release/下的运行期文件整体拷到 exe 同级目录,locales/保持目录结构。清单是:libcef.dll、cef.pak、cef_100_percent.pak、cef_200_percent.pak、cef_extensions.pak、devtools_resources.pak、icudtl.dat、natives_blob.bin、snapshot_blob.bin、locales/*.pak。VS 工程里可以用生成后事件自动拷,比手动可靠。
5.2 编译通过,链接报 LNK2019,符号都带__imp_
现象:源码编译无误,链接阶段一堆LNK2019 unresolved external symbol,指向libcef_dll_wrapper里的函数。
原因:三种可能——没把libcef_dll_wrapper.lib加进附加依赖项;加进去了但 Release 工程链了 Debug 版静态库;或者头文件路径指向了另一个版本的 include 目录。
解决:先确认附加库目录和附加依赖项同时配上,再确认配置(Debug/Release)与平台(Win32)两者都和 wrapper 的编译产物一致。混用 Debug/Release 的典型表现是链接期报_ITERATOR_DEBUG_LEVEL不匹配,看到这个错就直接去查配置,不用看代码。
5.3 浏览器区域全白或全黑,但鼠标点上去有反应
现象:窗口出来了,那块矩形是纯白或纯黑,点击、滚动都有响应,页面内容就是不显示。
原因:绝大多数是坐标或层级问题。传给SetAsChild的矩形用了屏幕坐标;父窗口缺少WS_CLIPCHILDREN;或者父窗口在WM_PAINT里把整个客户区刷了一遍背景,把子窗口盖住了。
解决:改用GetClientRect拿到的客户区坐标;父窗口样式加上WS_CLIPCHILDREN;父窗口的WM_PAINT里只刷浏览器区域之外的背景。改完在WM_SIZE里补一句WasResized(),重新跑一次。
5.4 页面提示插件不受支持,日志里没有任何插件加载记录
现象:页面里放<embed>的区域显示成灰色占位或者提示“该插件不受支持”,debug.log里搜不到插件路径,连扫描记录都没有。
原因:按可能性排序——插件 DLL 位数不对(64 位插件配 32 位宿主);extra-plugin-dir写成了 DLL 路径而不是目录;process_type判断写反导致开关只加到了渲染进程上;插件目录在带中文或空格的路径下,扫描时被跳过。
解决:先用 4.3 的dumpbin三条命令确认位数;再把extra-plugin-dir改成纯英文无空格的目录,把插件 DLL 放进去;在OnBeforeCommandLineProcessing里加一行日志打印command_line->GetCommandLineString(),确认开关真的进了 browser 进程。这三步走完基本都能定位。
5.5 关闭窗口后进程残留,或者退出时崩在 CefShutdown
现象:主窗口关了,任务管理器里还有进程;或者进程退出时崩在CefShutdown()里面,堆栈看不出所以然。
原因:还有CefRefPtr<CefBrowser>没释放,或者消息循环还在跑就调了CefShutdown()。CEF 要求所有 browser 对象引用归零之后才能 shutdown,顺序错了就是随机崩溃。
解决:严格按OnBeforeClose里置空引用并CefQuitMessageLoop()、CefRunMessageLoop()返回之后再CefShutdown()的顺序走。如果你的宿主自己管消息泵,用CefDoMessageLoopWork()的轮询模式,在每次轮询里检查 browser 引用是否已清空,清空后再 shutdown。
注意:32 位进程还有一条硬约束——用户态地址空间只有 2GB 左右。页面里塞了大量高清图或者同时开五六个 browser 实例,很容易在没有任何异常日志的情况下直接崩掉。遇到这种“无缘无故就没了”的情况,先去任务管理器看内存峰值。
6. 离屏渲染验证:用 OnPaint 确认页面真的画出来了
到了这一步,你已经有一个能加载页面、能挂插件的壳子。真正难缠的是“页面加载完了但没有画面”这类问题——OnLoadEnd回调正常触发,DOM 也拿到了,就是看不到像素。这时候光看日志没用,得直接检查渲染管线。
做法是开离屏渲染:settings.windowless_rendering_enabled = true,CefWindowInfo改用SetAsWindowless(0),然后在 client 里实现CefRenderHandler。
class OffscreenHandler : public CefClient, public CefRenderHandler, public CefLoadHandler { public: int frames_ = 0; // 累计收到的帧数 int nonzero_frames_ = 0; // 其中非全零的帧数 CefRefPtr<CefRenderHandler> GetRenderHandler() override { return this; } // CEF 问你要视口多大,必须返回真实尺寸,否则不产生绘制 bool GetViewRect(CefRefPtr<CefBrowser> browser, CefRect& rect) override { rect = CefRect(0, 0, 1024, 768); return true; } void OnPaint(CefRefPtr<CefBrowser> browser, PaintElementType type, const RectList& dirtyRects, const void* buffer, int width, int height) override { if (type != PET_VIEW) return; // 只关心主视图,别把弹窗也混进来 frames_++; const unsigned char* p = static_cast<const unsigned char*>(buffer); size_t total = static_cast<size_t>(width) * height * 4; for (size_t i = 0; i < total; i += 4) { if (p[i] | p[i + 1] | p[i + 2]) { nonzero_frames_++; break; } } // 这里可以顺手把 buffer 转成 BGRA 位图存盘,肉眼确认 } void OnLoadEnd(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, int httpStatusCode) override { if (!frame->IsMain()) return; // 加载完主动要求重绘一次,避免首帧丢失 browser->GetHost()->Invalidate(PET_VIEW); } IMPLEMENT_REFCOUNTING(OffscreenHandler); };判断逻辑很简单:frames_应该在你Invalidate之后几帧内增长,nonzero_frames_应该大于零。如果帧数在涨但全是零,说明 buffer 是空的,问题在渲染层——通常是GetViewRect返回了 0 尺寸,或者WasHidden状态一直是 true,页面以为自己在后台就不画了。如果帧数压根不涨,问题在进程侧——子进程没起来或者windowless_rendering_enabled没真正生效,去debug.log里搜子进程的启动命令行。
把 buffer 存成.bmp是最快的肉眼验证:32 位 BGRA,四个字节一个像素,把 alpha 填成 255 直接写文件头就能打开。这招我在排查“插件画出来的区域是空白”时用过一次,发现插件确实画了,但只画在了右上角 200×100 的区域,GetViewRect给的尺寸和插件内部期望的尺寸差了四倍。这个问题在日志里完全看不出来。
一个补充技巧:调试期把windowless_frame_rate调到 15,帧率低一点没关系,能显著降低 32 位进程的内存压力,也不容易因为渲染太快把日志刷爆。上线再调回去。
后面再碰这类内嵌浏览器的活,我都会先把离屏渲染这条路走通,哪怕最终交付的是窗口模式。因为它能把“页面有没有画出来”这件事变成一个可观测的数字,而不是靠肉眼盯着白屏猜。希望帮到你。
本文还有配套的精品资源,点击获取