Cef功能开发经验总结

这是我开发Cef功能时对踩过的坑,进行的总结,话说Cef坑真的不少。好在踩完后用起来还是挺爽的。最终的代码可以下载网易云信PC Demo点我跳转

资料准备

这是我集成过程中查到的一些资料,包括了Cef开发的各方面资料

在调试Cef时需要Cef的pdb和源码:

Cef基本结构

CefApp接口

CefApp接口提供了不同进程的可定制回调函数,每一个进程对应一个CefApp接口。CefBrowserProcessHandler对应浏览器进程的回调,CefRenderProcessHandler对应渲染进程的回调。我们应该继承CefApp、CefBrowserProcessHandler、CefRenderProcessHandler接口。如果完全使用多进程模式,可以分别在浏览器进程和渲染进程里分开继承接口

CefApp::OnBeforeCommandLineProcessing方法里可以附加传入给Cef的命令行参数,这里可以附加很多控制参数

CefRenderProcessHandler::OnWebKitInitialized方法可以在渲染进程初始化时用来注册JS扩展代码,实现C++与JS交互

CefRenderProcessHandler::OnFocusedNodeChanged方法可以检测当前获取到焦点html元素,获取到一些元素信息可以通过进程通信发送给浏览器进程来辅助做进一步的判断

CefRenderProcessHandler::OnProcessMessageReceived方法用于接收浏览器进程发来的消息,在做C++与JS交互时会用到

CefClient接口

每一个CefBrowser对象会对应一个CefClient接口,用于处理浏览器页面的各种回调信息,包括了Browser的生命周期,右键菜单,对话框,状态通知显示,下载事件,拖曳事件,焦点事件,键盘事件,离屏渲染事件。随着Cef版本的更新这些接口也会扩展和更新,多数对Cef进行行为控制的方法都集中在这些接口,如果对Cef有新的功能需求,一般都可以先翻翻这些接口中有没有提供相关功能

CefClient::OnProcessMessageReceived方法用于接收渲染进程发到的消息,在做C++与JS交互时会用到

CefSettings结构体

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消息循环的代码如下:

  1.  
    {
  2.  
    MainThread thread; // 创建主线程
  3.  
    thread.RunOnCurrentThreadWithLoop(nbase::MessageLoop::kUIMessageLoop); // 执行主线程循环
  4.  
    }

在RunOnCurrentThreadWithLoop方法的第二个参数里可以指定一个消息分派器指针dispatcher,dispatcher继承自nbase::Dispatcher。base库中的UI消息循环代码如下:

  1.  
    PreProcessMessage(msg);
  2.  
     
  3.  
    if (state_->dispatcher)
  4.  
    {
  5.  
    if (!state_->dispatcher->Dispatch(msg))
  6.  
    state_->should_quit = true;
  7.  
    }
  8.  
    else
  9.  
    {
  10.  
    TranslateMessage(&msg);
  11.  
    DispatchMessage(&msg);
  12.  
    }
  13.  
     
  14.  
    PostProcessMessage(msg);

如果我们指定了RunOnCurrentThreadWithLoop方法的第二个参数,就不会调用原本的消息循环了,所以可以在这个dispatcher里定制消息循环。我实现CefMessageLoopDispatcher类并重写Dispatch接口。

  1.  
    BOOL CefMessageLoopDispatcher::IsIdleMessage(const MSG* pMsg)
  2.  
    {
  3.  
    switch (pMsg->message)
  4.  
    {
  5.  
    case WM_MOUSEMOVE:
  6.  
    case WM_NCMOUSEMOVE:
  7.  
    case WM_PAINT:
  8.  
    return FALSE;
  9.  
    }
  10.  
     
  11.  
    return TRUE;
  12.  
    }
  13.  
     
  14.  
    bool CefMessageLoopDispatcher::Dispatch(const MSG &msg)
  15.  
    {
  16.  
    static BOOL bDoIdle = TRUE;
  17.  
     
  18.  
    TranslateMessage(&msg);
  19.  
    DispatchMessage(&msg);
  20.  
     
  21.  
    if (IsIdleMessage(&msg))
  22.  
    {
  23.  
    bDoIdle = TRUE;
  24.  
    }
  25.  
     
  26.  
    while (bDoIdle && !::PeekMessage(const_cast<MSG*>(&msg), NULL, 0, 0, PM_NOREMOVE))
  27.  
    {
  28.  
    CefDoMessageLoopWork();
  29.  
    bDoIdle = FALSE;
  30.  
    }
  31.  
     
  32.  
    return true;
  33.  
    }

在定制消息循环里,如果判断当前消息队列为空并且刚才处理的消息不会指定的几个消息,就去调用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线程,切记!

CefClient接口介绍

CefLifeSpanHandler

CefBrowser对象的生命周期事件的回调接口。

  • OnAfterCreated:当调用CreateBrowser函数创建浏览器对象后会立马触发这个回调,在这里可以保存浏览器对象的指针
  • DoClose:当调用CloseBrowser函数后触发这个回调
  • OnBeforeClose:当浏览器对象即将销毁时会触发这个回调,在这里一定要释放所有对CefBrowser对象的引用,否则会导致程序无法退出。切记这个坑。
  • OnBeforePopup:当单击了网页中会弹出新窗口的链接时,会触发这个回调。我们的项目里应该禁止新窗口的弹出,而在原控件中跳转链接

CefRenderHandler

要使用离屏渲染功能,就必须要实现这个接口类。因为项目中使用的duilib库,目前使用分层窗体机制实现异形窗体效果,不支持显示子窗口只能自绘控件,所以没法使用比较简单的子窗口的形式显示Cef浏览器对象。只能使用离屏渲染方法,离屏渲染的数据会通过CefRenderHandler接口回调。

首先必须要开启CefSettings结构体的windowless_rendering_enabled字段

  • GetRootScreenRect:浏览器对象创建后触发的回调,返回最外层窗体在屏幕中的位置
  • GetViewRect:在浏览器对象初始化后,或者浏览器大小改变时,触发这个回调来获取浏览器对象的位置。因为浏览器对象会平铺满整个控件,所以这里返回控件的位置。其中返回的左上位置要准确,否则Cef在处理一些坐标信息时会出错
  • GetScreenPoint:在这里把传入的坐标值,由客户区坐标转换为屏幕坐标
  • OnCursorChange:当需要修改鼠标光标时触发这个回调
  • OnPaint:当浏览器对象有新的渲染数据后,会触发这个回调,包含了脏区和渲染数据。应该保存这些数据,然后在适当的时候贴到目标窗体上
  • OnPopupShow:当浏览器中要弹出内部对话框时(比如弹出一个下拉菜单),触发这个回调,通知要显示或者隐藏弹出框
  • OnPopupSize:当浏览器中要弹出内部对话框时,触发这个回调,通知弹出框的位置和大小

离屏渲染的实现

离屏渲染的效率不如真窗口渲染,如果不是必须要离屏渲染的情况,还是用真窗口比较好。CefControl控件实现了duilib嵌入Cef浏览器对象。

在控件初始化触发Init函数时,调用CreateBrowser函数创建CefBrowser对象,这会触发CefRenderHandler::GetViewRect回调,在这个回调里返回控件的位置。随后网页第一次渲染时触发CefRenderHandler::OnPaint回调。

绘制渲染数据的流程

  1. 当网页渲染数据改变、或者我们主动调用了CefBrowser对象的Invalidate方法时,会触发CefRenderHandler::OnPaint回调。
  2. 我写了一个内存位图缓冲类MemoryDC来保存Cef传来的渲染数据,在CefControl控件中dc_cef_成员变量负责保存渲染数据。在CefRenderHandler::OnPaint回调里,根据渲染数据初始化dc_cef_,然后根据脏区把渲染数据拷贝到dc_cef_中。
  3. 数据拷贝完之后,调用CefControl控件的Invalidate方法通知窗体重绘控件
  4. 在CefControl控件的Paint方法里,把dc_cef_的位图数据拷贝到duilib传入的HDC中

CefControl对事件的处理

修改CefBrowser尺寸 
在离屏渲染模式下,无法直接修改CefBrowser对象的尺寸。CefControl控件重写SetPos函数,在这里调用CefBrowser对象的WasResized接口通知CefBrowser对象需要改变尺寸,之后GetViewRect接口会被触发,这时依然是返回CefControl控件的位置就可以了。之后OnPaint接口会被自动触发,按照前一节的流程进行一次渲染数据的刷新

设置CefBrowser隐藏(显示) 
CefControl控件重写SetVisible函数和SetInternVisible函数,在这里调用CefBrowser对象的WasHidden接口通知CefBrowser对象隐藏或显示

对系统消息的处理

  1. 在控件初始化触发Init函数时,调用窗体类的AddMessageFilter函数把自己注册到窗体的消息过滤队列里。CefControl控件继承IUIMessageFilter接口类并重写MessageHandler函数。当系统消息进入窗体后会依次调用消息过滤队列指针来过滤消息。在MessageHandler函数里处理我们感兴趣的消息,其他消息并不过滤
  2. 处理各种鼠标类消息时,判断如果鼠标不在控件范围内则不处理相关消息。获取当前鼠标的坐标,因为CefBrowser的坐标值是以自身左上角作为原点的,所以获取的鼠标坐标要减去CefControl控件的左上角坐标值。其中处理ButtonDown、ButtonUp、MouseMove消息时,不会中断消息继续传递给窗体,这里需要让duilib窗体类处理SetCapture、ReleaseCapture等函数
  3. 处理键盘消息时,判断当前控件是否获取焦点,只处理有焦点的情况
  4. WM_SETCURSOR消息处理,在MessageHandler函数拦截WM_SETCURSOR消息,直接调用窗体类的默认消息处理函数,不让duilib处理这个消息。CefRenderHandler::OnCursorChange接口会修改鼠标光标并修改窗体的默认光标样式,而duilib处理WM_SETCURSOR消息时会另外修改光标,所以需要拦截

渲染Popup弹出框

  1. 浏览器中,弹出框的渲染数据是需要自己额外处理的。如下拉菜单等弹出框,否则浏览器中不会显示出弹出框
  2. 当需要显示弹出框时,CefRenderHandler::OnPopupSize接口会传入弹出框的位置和尺寸等数据,在这里把数据保存到rect_popup_成员变量
  3. 之后会触发CefRenderHandler::OnPaint回调,并且渲染类型会被指定为弹出框类型PET_POPUP。这时把弹出框的渲染数据保存到MemoryDC类型的dc_cef_popup_成员变量中
  4. 数据拷贝完之后,调用CefControl控件的Invalidate方法通知窗体重绘控件
  5. 在CefControl控件的Paint方法里,把dc_cef_popup_的位图数据按照rect_popup_的信息拷贝到duilib传入的HDC中
  6. 当弹出框消失时,触发CefRenderHandler::OnPopupShow接口,这里重置rect_popup_的信息,并且通知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");

C++与JS交互

C++调用JS

在browser进程和render进程都可以直接执行JS代码,直接调用CefFrame对象的ExecuteJavaScript方法就可以

JS调用C++

网页中的一些JS回调和对网页的JS扩展,都必须在渲染进程操作。让JS调用C++的方法有三个,http://www.cnblogs.com/guolixiucai/p/4943748.html里面介绍了两种,https://github.com/fanfeilong/cefutil里面是更复杂更强的第三种。

我们项目里,只需要给JS开放一个函数接口,而且接口并不复杂,所以直接采用JS扩展的方法注册JS回调函数就可以。

在CefRenderProcessHandler::OnWebKitInitialized接口里,注册JS扩展代码

  1.  
    std::string extensionCode =
  2.  
    "(function() {"
  3.  
    " CefTestWebFunction = function(param) {"
  4.  
    " native function CefTestWebFunction(param);"
  5.  
    " return CefTestWebFunction(param);"
  6.  
    " };"
  7.  
    "})();";
  8.  
     
  9.  
    CefRefPtr<CefV8Handler> handler = new CefJSHandler();
  10.  
    CefRegisterExtension("v8/extern", extensionCode, handler);

CefRegisterExtension函数会执行扩展代码。网上例子都是创建一个全局对象,然后把JS函数和变量绑定到这个对象上。这里直接申明一个FunExternal的全局函数。当JS代码中调用FunExternal函数时,会根据native关键字后的函数名,去通知C++代码调用对应的native函数

CefJSHandler类继承CefV8Handler接口并实现Execute方法,在CefRegisterExtension传入CefJSHandle指针,当JS代码需要调用native函数时会,会主动触发CefJSHandler::Execute方法

  1.  
    bool CefJSHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception)
  2.  
    {
  3.  
    if (name == "CefTestWebFunction" && arguments.size() == 1)
  4.  
    {
  5.  
    for (auto &it : arguments)
  6.  
    {
  7.  
    if (it->IsString())
  8.  
    {
  9.  
    CefString param = it->GetStringValue();
  10.  
     
  11.  
    CefRefPtr<CefBrowser> browser = CefV8Context::GetCurrentContext()->GetBrowser();
  12.  
    CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kJsCallbackMessage);
  13.  
     
  14.  
    message->GetArgumentList()->SetString(0, name);
  15.  
    message->GetArgumentList()->SetString(1, param);
  16.  
    browser->SendProcessMessage(PID_BROWSER, message);
  17.  
     
  18.  
    retval = CefV8Value::CreateBool(true);
  19.  
    }
  20.  
    }
  21.  
    return true;
  22.  
    }
  23.  
     
  24.  
    // Function does not exist.
  25.  
    return false;
  26.  
    }

在这里可以获取到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中的不一样,所以采用了不同的关闭流程

CefControl控件的销毁流程

  1. 在CefControl控件的析构函数里调用CloseBrowser(true)方法通知浏览器对象要关闭
  2. BrowserHandler::DoClose接口被触发,这里不需要做额外处理,直接返回就可以
  3. 之后BrowserHandler::OnBeforeClose接口被触发,在这里一定要释放所有对CefBrowser对象的引用,否则会导致程序无法退出。

进程退出流程

  1. 用户单击右下角托盘的退出菜单项
  2. 触发到LoginCallback::DoLogout函数,在这里会调用到代码nim_comp:: WindowsManager::GetInstance()->DestroyAllWindows();,这里销毁所有的窗体,所有控件被销毁,自然就会触发所有CefControl控件的销毁流程,所有浏览器对象被关闭
  3. LoginCallback::DoLogout函数里之后会调用到UILogoutCallback函数,这里原本会调用PostQuitMessage(0)函数结束消息循环,但是我们应该等待所有浏览器对象关闭后在结束消息循环,否则会发生错误。而CefBrowser的关闭是异步的,所以无法保证调用UILogoutCallback函数时所有CefBrowser被关闭
  4. 我在CefManager类实现PostQuitMessage函数,在这里等待所有CefBrowser关闭后再结束消息循环
  5. 程序正常结束

CefManager::PostQuitMessage函数里判断当前浏览器对象的数量来决定是否退出消息循环,如果还有浏览器对象没有关闭就等待500毫秒后再检测:

  1.  
    void CefManager::PostQuitMessage(int nExitCode)
  2.  
    {
  3.  
    if (browser_count_ == 0)
  4.  
    {
  5.  
    Post2UI([nExitCode]()
  6.  
    {
  7.  
    ::PostQuitMessage(nExitCode);
  8.  
    });
  9.  
    }
  10.  
    else
  11.  
    {
  12.  
    auto cb = [nExitCode]()
  13.  
    {
  14.  
    CefManager::GetInstance()->PostQuitMessage(nExitCode);
  15.  
    };
  16.  
     
  17.  
    nbase::ThreadManager::PostDelayedTask(kThreadUI, cb, nbase::TimeDelta::FromMilliseconds(500));
  18.  
    }
  19.  
    }

把Cef组件集成到云信NIM项目

  1. 把tool_kits\cef目录中写好的Cef模块组件拷贝到自己项目的对应目录,并且添加到解决方案中。其中cef_render项目是Cef渲染子进程,是一个独立的exe;libcef_dll_wrapper项目是Cef导出的C语言接口的C++包装类;cef_module项目是核心封装代码,把cef功能封装为可以在nim demo中直接使用的类,其中包含了对cef功能进行管理的CefManager和CefControl、CefNativeControl两个控件等。
  2. 把nim_win_demo\gui\cef目录的源文件拷贝都自己项目的某个目录,这里面CefControl、CefNativeControl两个控件的测试窗口代码,可有可无
  3. 进入libs目录,解压cef_sandbox.rar压缩包并把cef_sandbox.lib、cef_sandbox_d.lib文件放到libs目录;进入libs\x64目录,解压cef_sandbox.rar压缩包并把cef_sandbox.lib、cef_sandbox_d.lib文件放到libs\x64目录。这里面是编译cef组件时,为cef模块增加sandbox功能的静态库。bin\cef目录是cef模块依赖的cef相关dll。主程序初始化时会从bin\cef目录加载cef所需dll
  4. 配置nim_demo项目属性,在链接器\输入\延迟加载的DLL中加入libcef.dll;libEGL.dll;libGLESv2.dll(因为我们把cef所需的dll都放到bin\cef目录了,这样不会导致目录混乱,但是为了顺利加载cef dll,需要延迟加载;如果不想用延迟加载,就把bin\cef目录的dll都直接放到bin目录)
  5. 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();

  6. 找到原项目中调用::PostQuitMessage函数的地方,修改为nim_cef:: CefManager::GetInstance()->PostQuitMessage(0);

  7. 其他配置如果有疑问可以参见Cef Nim Demo的配置

让Cef支持Flash、mp3、mp4

现在附带的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模块功能

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),同时程序将无法通过附加参数指定渲染子进程(此时必须用主进程exe来做渲染子进程),这时也就不需要cef_render项目编译的render.exe了。如果禁用Flash功能,则会让render.exe来作为渲染子进程

集成过程中遇到的其他坑

通过这些时间用Cef,发现坑其实不少,而且各个版本的坑不一样。

  1. multi_threaded_message_loop导致中断:在2623、2526版本,Debug模式中,如果开启了multi_threaded_message_loop,当程序退出时,必定会触发中断。这个属于Cef的bug,在官方demo中也有这个问题,但是在Release模式中是没有问题的。
  2. 2357版本在程序处理重定向信息后,会导致渲染进程崩溃,这个版本无法用于项目
  3. 1916版本各个功能使用正常。但是在在Debug模式下某些网页打开时会出中断警告(但并不是错误),可能是因为对新html标准支持不够;Debug模式下单进程模式在退出时会触发中断。但是在Release模式下都正常使用
  4. 设置CefSettings的cache_path字段(也就是LocalStorage),一定要注意不要在路径末尾添加”\\”,否则会触发中断
  5. 在多进程模式下,必须设置子进程的程序名(不管是使用原程序作为子进程,还是单独一个程序作为子进程)。当Cef调用LoadUrl函数加载网页时,会查找子进程的绝对路径去启动渲染进程,如果不设置子进程名字,会导致查路径找发生错误,导致VS在Debug模式下卡死
  6. 如果开发者不负责Cef相关功能的开发,可以修改CefManager::AddCefDllToPath函数的代码,让Cef不管在Debug模式还是Release模式下都使用Release版本的Cef Dll文件。这样做不会发生错误,而且上面提到的多数坑都不会被触发
  7. 如果在使用Cef模块中遇到一些崩溃或者其他异常现象,请先使用release模式+开启多进程模式再运行一次,很多问题都是debug模式或者单进程模式导致的
  8. 如果使用flash功能,就需要在编译时加入cef_sandbox.lib等静态库,否则在使用flash功能时会有一个黑框弹出(这输入cef的bug)。而加入sandbox功能后,在某些电脑上离屏渲染功能就无法顺利创建子进程,导致没有画面。这时就要在子进程创建前检查命令行参数,发现不是flash进程就增加no-sandbox参数来关闭sandbox功能

总结

离屏渲染+异形窗体

CefWindowInfo::SetAsWindowless的transparent参数必须设置为true! 

posted @ 2019-05-31 11:11  赵二冬  阅读(5194)  评论(0编辑  收藏  举报