转载请说明原出处,谢谢~~:http://blog.csdn.net/zhuhongshu/article/details/70159672
这是我开发Cef功能时对踩过的坑,进行的总结,话说Cef坑真的不少。好在踩完后用起来还是挺爽的。最终的代码可以下载网易云信PC Demo C++源码
点我跳转
这是我集成过程中查到的一些资料,包括了Cef开发的各方面资料
在调试Cef时需要Cef的pdb和源码:
CefApp接口提供了不同进程的可定制回调函数,每一个进程对应一个CefApp接口。CefBrowserProcessHandler对应浏览器进程的回调,CefRenderProcessHandler对应渲染进程的回调。我们应该继承CefApp、CefBrowserProcessHandler、CefRenderProcessHandler接口。如果完全使用多进程模式,可以分别在浏览器进程和渲染进程里分开继承接口
CefApp::OnBeforeCommandLineProcessing方法里可以附加传入给Cef的命令行参数,这里可以附加很多控制参数
CefRenderProcessHandler::OnWebKitInitialized方法可以在渲染进程初始化时用来注册JS扩展代码,实现C++与JS交互
CefRenderProcessHandler::OnFocusedNodeChanged方法可以检测当前获取到焦点html元素,获取到一些元素信息可以通过进程通信发送给浏览器进程来辅助做进一步的判断
CefRenderProcessHandler::OnProcessMessageReceived方法用于接收浏览器进程发来的消息,在做C++与JS交互时会用到
每一个CefBrowser对象会对应一个CefClient接口,用于处理浏览器页面的各种回调信息,包括了Browser的生命周期,右键菜单,对话框,状态通知显示,下载事件,拖曳事件,焦点事件,键盘事件,离屏渲染事件。随着Cef版本的更新这些接口也会扩展和更新,多数对Cef进行行为控制的方法都集中在这些接口,如果对Cef有新的功能需求,一般都可以先翻翻这些接口中有没有提供相关功能
CefClient::OnProcessMessageReceived方法用于接收渲染进程发到的消息,在做C++与JS交互时会用到
CefSettings结构体定义了Cef的全局配置信息,比如指定单进程模式、指定渲染子进程路径、设置localstorage路径、设置日志等级、Cef资源文件路径。其中对于项目最重要的字段是single_process、multi_threaded_message_loop、windowless_rendering_enabled,分别用于指定单进程模式、多线程渲染模式、离屏渲染模式。
如果是UI线程消息循环构架较简单的项目,可以直接调用CefRunMessageLoop来使用Cef自带的消息循环,它会阻塞线程直到调用了CefQuitMessageLoop函数,CefRunMessageLoop是兼容传统的Win32消息循环的。
不过NIM项目底层是使用谷歌base库的多线程构架,所以没法直接使用CefRunMessageLoop。(PS:实际上Cef的底层消息循环也是谷歌的base库)
要让NIM的消息循环兼容Cef消息循环,有两种方法。
第一种方法是使用CefDoMessageLoopWork函数代替CefRunMessageLoop来完全消息消息循环。CefDoMessageLoopWork函数的作用是让Cef执行一次消息循环,这个函数不会阻塞线程,所以需要在我们现有的消息循环里的适当情况下主动去调用CefDoMessageLoopWork函数,如果调用的太频繁会很消耗CPU,如果调用频率太低会导致Cef来不及处理内部消息,让Cef界面反映变慢,所以这个函数的调用时机很重要。
因为CefDoMessageLoopWork函数应该在原本的消息循环中调用,而base库的UI线程消息循环是封装好的。这里首先说一下定制base库消息循环的方法。在WinMain入口函数里调用UI消息循环的代码如下:
{
MainThread thread; // 创建主线程
thread.RunOnCurrentThreadWithLoop(nbase::MessageLoop::kUIMessageLoop); // 执行主线程循环
}
在RunOnCurrentThreadWithLoop方法的第二个参数里可以指定一个消息分派器指针dispatcher,dispatcher继承自nbase::Dispatcher。base库中的UI消息循环代码如下:
PreProcessMessage(msg);
if (state_->dispatcher)
{
if (!state_->dispatcher->Dispatch(msg))
state_->should_quit = true;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
PostProcessMessage(msg);
如果我们指定了RunOnCurrentThreadWithLoop方法的第二个参数,就不会调用原本的消息循环了,所以可以在这个dispatcher里定制消息循环。我实现CefMessageLoopDispatcher类并重写Dispatch接口。
BOOL CefMessageLoopDispatcher::IsIdleMessage(const MSG* pMsg)
{
switch (pMsg->message)
{
case WM_MOUSEMOVE:
case WM_NCMOUSEMOVE:
case WM_PAINT:
return FALSE;
}
return TRUE;
}
bool CefMessageLoopDispatcher::Dispatch(const MSG &msg)
{
static BOOL bDoIdle = TRUE;
TranslateMessage(&msg);
DispatchMessage(&msg);
if (IsIdleMessage(&msg))
{
bDoIdle = TRUE;
}
while (bDoIdle && !::PeekMessage(const_cast(&msg), NULL, 0, 0, PM_NOREMOVE))
{
CefDoMessageLoopWork();
bDoIdle = FALSE;
}
return true;
}
在定制消息循环里,如果判断当前消息队列为空并且刚才处理的消息不会指定的几个消息,就去调用CefDoMessageLoopWork函数。WM_PAINT、WM_MOUSEMOVE等消息的处理比较复杂,所以不在这里调用CefDoMessageLoopWork函数
这个方法基本可以使用,但是还存在一些问题,这里CefDoMessageLoopWork函数的调用机制还不够好,Cef界面不够顺畅,而且因为Cef与项目base库的冲突,导致在程序结束时有些问题。这个方法有待优化
CefSettings结构体的multi_threaded_message_loop(多线程消息循环)为false时,可以调用CefRunMessageLoop或者CefDoMessageLoopWork函数来触发Cef消息循环,这时浏览器进程的UI线程就是调用CefRunMessageLoop或者CefDoMessageLoopWork函数的线程。如果CefSettings结构体的multi_threaded_message_loop为true时。浏览器进程的UI线程是另外的线程。设置multi_threaded_message_loop为true则使用多线程消息循环。
通过对比Cef Demo的多线程消息循环代码,可以确定在NIM项目中直接开启多线程消息循环,不需要修改现有消息循环代码就可以正常使用Cef了。不过需要注意的是,使用多线程消息循环后某些函数就无法使用了,比如CreateBrowserSync,这种函数要求必须在Cef的UI线程调用。
另外,在很多版本的Cef里,如果开启了多线程消息循环,会导致程序在结束时触发中断,这属于Cef的bug,不过在release版本的Cef中没有问题。应该在项目中使用这个方法。不过使用了多线程消息循环后,很多Cef对象触发的回调函数,都是在Cef的UI线程而不是我们的UI线程,所以这时操作我们的UI线程就比较麻烦,要注意一些多线程问题,尽量把操作转发到我们的UI线程,不转发的话必须确定所操作的代码不会影响我们的UI线程,切记!
CefBrowser对象的生命周期事件的回调接口。
要使用离屏渲染功能,就必须要实现这个接口类。因为项目中使用的duilib库,目前使用分层窗体机制实现异形窗体效果,不支持显示子窗口只能自绘控件,所以没法使用比较简单的子窗口的形式显示Cef浏览器对象。只能使用离屏渲染方法,离屏渲染的数据会通过CefRenderHandler接口回调。
首先必须要开启CefSettings结构体的windowless_rendering_enabled字段
离屏渲染的效率不如真窗口渲染,如果不是必须要离屏渲染的情况,还是用真窗口比较好。CefControl控件实现了duilib嵌入Cef浏览器对象。
在控件初始化触发Init函数时,调用CreateBrowser函数创建CefBrowser对象,这会触发CefRenderHandler::GetViewRect回调,在这个回调里返回控件的位置。随后网页第一次渲染时触发CefRenderHandler::OnPaint回调。
修改CefBrowser尺寸
在离屏渲染模式下,无法直接修改CefBrowser对象的尺寸。CefControl控件重写SetPos函数,在这里调用CefBrowser对象的WasResized接口通知CefBrowser对象需要改变尺寸,之后GetViewRect接口会被触发,这时依然是返回CefControl控件的位置就可以了。之后OnPaint接口会被自动触发,按照前一节的流程进行一次渲染数据的刷新
设置CefBrowser隐藏(显示)
CefControl控件重写SetVisible函数和SetInternVisible函数,在这里调用CefBrowser对象的WasHidden接口通知CefBrowser对象隐藏或显示
Cef3支持多进程和单进程渲染,但是单进程渲染不够稳定,只应该在Debug模式下作为调试目的使用。在Cef3.1916等好几个版本中,调试状态下使用单进程模式,当程序初始化或者退出时,会触发中断。但是在多进程模式下没有问题。官方也明确说明不推荐使用单进程模式
CefManager类实现了Cef3的初始化和销毁功能。初始化函数Initialize里调用的CefExecuteProcess函数会检测当前的进程类型,如果是浏览器进程则函数会直接返回,在其他进程的话这个函数会阻塞直接进程销毁。
ClientApp类继承CefBrowserProcessHandler和CefRenderProcessHandler,可以同时处理浏览器进程和渲染进程的消息。原本多进程模式中,浏览器进程和渲染进程可以同用一个程序。但是由于我们的主程序的代码比较复杂,如果让主程序多开进程的话,会占用较多的内存和CPU,同时触发不必要的问题。所以专门另写了一个cef_render项目来作为渲染子进程
cef_render项目代码比较简单,主要代码都是继承CefRenderProcessHandler接口的CefRenderProcessHandler类。考虑到代码周全,以后可以在cef_render项目补充一些崩溃Dump处理等代码。务必要保证主程序的CefRenderProcessHandler接口实现代码与cef_render程序的CefRenderProcessHandler接口实现代一致。否则单进程和多进程模式下会出现不同的处理结果
在浏览器进程启动时,通过附加参数可以指定渲染子进程的路径
command_line->AppendSwitchWithValue("browser-subprocess-path", "render.exe");
在browser进程和render进程都可以直接执行JS代码,直接调用CefFrame对象的ExecuteJavaScript方法就可以
网页中的一些JS回调和对网页的JS扩展,都必须在渲染进程操作。让JS调用C++的方法有三个,http://www.cnblogs.com/guolixiucai/p/4943748.html里面介绍了两种,https://github.com/fanfeilong/cefutil里面是更复杂更强的第三种。
我们项目里,只需要给JS开放一个函数接口,而且接口并不复杂,所以直接采用JS扩展的方法注册JS回调函数就可以。
在CefRenderProcessHandler::OnWebKitInitialized接口里,注册JS扩展代码
std::string extensionCode =
"(function() {"
" CefTestWebFunction = function(param) {"
" native function CefTestWebFunction(param);"
" return CefTestWebFunction(param);"
" };"
"})();";
CefRefPtr handler = new CefJSHandler();
CefRegisterExtension("v8/extern", extensionCode, handler);
CefRegisterExtension函数会执行扩展代码。网上例子都是创建一个全局对象,然后把JS函数和变量绑定到这个对象上。这里直接申明一个FunExternal的全局函数。当JS代码中调用FunExternal函数时,会根据native关键字后的函数名,去通知C++代码调用对应的native函数
CefJSHandler类继承CefV8Handler接口并实现Execute方法,在CefRegisterExtension传入CefJSHandle指针,当JS代码需要调用native函数时会,会主动触发CefJSHandler::Execute方法
bool CefJSHandler::Execute(const CefString& name, CefRefPtr object, const CefV8ValueList& arguments, CefRefPtr& retval, CefString& exception)
{
if (name == "CefTestWebFunction" && arguments.size() == 1)
{
for (auto &it : arguments)
{
if (it->IsString())
{
CefString param = it->GetStringValue();
CefRefPtr browser = CefV8Context::GetCurrentContext()->GetBrowser();
CefRefPtr message = CefProcessMessage::Create(kJsCallbackMessage);
message->GetArgumentList()->SetString(0, name);
message->GetArgumentList()->SetString(1, param);
browser->SendProcessMessage(PID_BROWSER, message);
retval = CefV8Value::CreateBool(true);
}
}
return true;
}
// Function does not exist.
return false;
}
在这里可以获取到JS要调用的函数名,以及传入的参数等信息。获取到这些信息后,把他们包装为CefProcessMessage结构,通过IPC把信息发送到Browser进程进行异步处理。调用SendProcessMessage方法把信息发送到Browser进程
浏览器进程的CefClient::OnProcessMessageReceived方法接收到Render进程发来的消息。Browser进程处理消息后,可以通过C++调用JS的方法去通知Web端消息处理结果
在https://github.com/fanfeilong/cefutil/blob/master/doc/CEF_Close.md里有完整的Cef结束流程分析和处理代码,不过由于我们的项目不单单只有Cef组件,而且使用场景和Cef Demo中的不一样,所以采用了不同的关闭流程
CefManager::PostQuitMessage函数里判断当前浏览器对象的数量来决定是否退出消息循环,如果还有浏览器对象没有关闭就等待500毫秒后再检测:
void CefManager::PostQuitMessage(int nExitCode)
{
if (browser_count_ == 0)
{
Post2UI([nExitCode]()
{
::PostQuitMessage(nExitCode);
});
}
else
{
auto cb = [nExitCode]()
{
CefManager::GetInstance()->PostQuitMessage(nExitCode);
};
nbase::ThreadManager::PostDelayedTask(kThreadUI, cb, nbase::TimeDelta::FromMilliseconds(500));
}
}
WinMain函数中第一句加入(用于延迟加载cef dll,如果不延迟加载,则不需要这句)
nim_ui:: InitManager::GetInstance()->AddCefDllToPath();
在开始云信组件初始化之前加入如下代码用于初始化cef功能(一定要在云信组件初始化之前)
if (!nim_cef::CefManager::GetInstance()->Initialize(true)
return 0;
在开始UI线程消息循环之后加入如下代码用于清理cef功能
nim_ui::InitManager::GetInstance()->CleanupUiKit();
找到原项目中调用::PostQuitMessage函数的地方,修改为nim_cef:: CefManager::GetInstance()->PostQuitMessage(0);
现在附带的nim demo中使用的cef相关dll是专门下载了cef源码增加mp3、mp4功能后重新编译的(在Windows下编译Cef3.2623并加入mp3、mp4支持(附带源码包和最终DLL) )。所以如果使用我提供的dll,可以直接支持mp3、mp4播放(官方直接下载的cef不支持)。如果对cef功能有其他需求的话请自行下载编译cef并替换demo中的dll
demo中附带的dll都是release版本,没有附带debug版本
cef_module项目中已经默认支持flash播放,bin\cef\PepperFlash目录中附带了支持flash播放所需的dll,如果不需要flash功能,可以删除这个目录
cef_module项目中提供了两个控件来展示cef浏览器,分别为CefControl、CefNativeControl,CefControl用于离屏渲染模式,CefNativeControl用于真窗口模式,根据需求来选择使用这两个控件的一个。离屏渲染模式的话控件自己控制浏览器的渲染,所以可以与nim duilib结合的更完美,支持透明异形窗体;真窗口模式因为Cef需要依托一个子窗口,由Cef自己渲染,所以无法支持透明异形窗体。对于绝大多数需求,使用离屏渲染模式的CefControl更好,因为与duilib结合更完美。但是如果网页的内容刷新非常频繁(尤其是用于播放Flash时),应该使用真窗口模式,否则Flash播放导致的频繁绘制操作会让程序的CPU占用率飙升!
我们的代码默认是开启离屏渲染模式的,如果有播放Flash的需求或者其他浏览器画面频繁的需求时,应该关闭离屏渲染模式而使用真窗口模式,关于方法时Winmain函数中初始化cef功能时参数传入false,nim_cef:: CefManager::GetInstance()->Initialize(false)。另外我们的duilib窗口默认是使用支持透明异形的分层窗口,是不支持子窗口的,所以如果使用cef的真窗口模式,那么应该关闭duilib窗口的分层窗口样式,关闭方法是创建窗口的Window::Create函数的第五个参数isLayeredWindow设置为false。demo中CefForm、CefNativeForm这两个窗体类分别用于演示离屏渲染模式(对应CefControl控件)和真窗口模式(对应CefNativeControl控件)的功能。这两个窗口的创建代码在MainForm::OnClicked中有演示代码
cef_module项目中预处理宏中增加了两个控件Cef模块功能的宏SUPPORT_CEF、SUPPORT_CEF_FLASH。SUPPORT_CEF宏控制是否启用cef功能,SUPPORT_CEF_FLASH控制cef是否支持flash播放功能(只有SUPPORT_CEF宏启用时这个宏才有效)。
如果不需要cef带来的浏览器功能,可以在cef_module项目中去掉SUPPORT_CEF宏,这样cef相关的功能就被禁用。同时*bin\cef*目录就可以删除掉而不影响程序运行。
如果需要cef功能但是并不需要flash功能,可以在cef_module项目中去掉SUPPORT_CEF_FLASH宏。同时bin\cef\PepperFlash**目录可以删除掉、**libs目录的cef_sandbox.lib、cef_sandbox_d.lib文件也可以删掉。
在开启了Flash功能后,要在编译时加入cef_sandbox.lib等静态库,否则在使用flash功能时会有一个黑框弹出(这输入cef的bug),关于这个bug的其他解决办法,详见:解决cef加载flash时弹出黑框的问题。同时程序将无法通过附加参数指定渲染子进程(此时必须用主进程exe来做渲染子进程),这时也就不需要cef_render项目编译的render.exe了。如果禁用Flash功能,则会让render.exe来作为渲染子进程
通过这些时间用Cef,发现坑其实不少,而且各个版本的坑不一样。
no-sandbox
参数来关闭sandbox功能CefWindowInfo::SetAsWindowless的transparent参数必须设置为true!