菜鸟与 cef 的邂逅之旅(四):Soui 离屏渲染封装 Cef3 细节分析

来源:互联网 发布:windows平板电脑2017 编辑:程序博客网 时间:2024/06/05 19:18

一、引言

最近因为项目组涉及到了 Cef3 的任务,临时来学习,经过了两个周的时间,加上各位大神和 Demo 的帮助,已经有了一点点感悟,希望在自己尚未遗忘之际,详细的记录下来,以飨各位同样在 Cef3 初学之际迷惘的人。

这篇博客的背景,是我已经成功编译运行了蓝先生(Soui 界面群里面的一位对我帮助很大的大神)的 Demo,这个 Demo 里已经集成了这篇博客里所有要讲述的内容。但是我并不满足于现成的 Demo,我想要模仿着大神的足迹,一步一步从零开始搭建 Cef3 环境并且封装 Cef3 浏览器控件,并且实现 C++ 与 JavaScript 的互相调用。于是又花了四五天,才有了这篇博客。

首先声明,这篇博客里面的代码大多都不是我写的,是在参考了蓝先生(Soui 群大神)和启程(Soui 作者)的两份有关 Soui 界面库中封装 Cef3 的项目之后,在多次尝试之后,自己写出来的代码。

其次,这篇博客假定:

1. 你对 Soui 界面库有一定了解:想要了解 Soui 界面库的同学可以点击这里
SOUI 是什么?

2. 你对 Cef3 有一定了解,至少有过成功编译运行 Cef3 官方提供的二进制文件的经历:如果没有这种经历,可以参考我之前的系列博客
菜鸟与 cef 的邂逅之旅(一):cef 源码获取与编译
菜鸟与 cef 的邂逅之旅(二):Soui 中接入 Cef3 的实现
菜鸟与 cef 的邂逅之旅(三):Cef3 中 C++ 与 JavaScript 的互相调用

3. 你至少已经粗略阅读了 Cef3 的官方文档:如果没有,我建议你先看看这一篇
tutorial/simple
然后再看看详细的以 cefclient 项目为基础讲解的教程
CEF General Usage(CEF3预览)

这里首先贴下本篇博客的实验代码,方便参考:
wangying2016/Soui-Cef-By-CefClient

那么,现在,让我们开始吧,虽然我仍然还是一只学习的菜鸟,我仍然尽力以自己的最大努力讲解清楚每一个我能讲解的细节。

二、封装模式:OSR 还是真窗口

我们想要将 Cef3 封装到我们现有的界面库中去,那么我们想象中的画面一定是这样的:

这是一个伟大的控件,我们只需要在界面上放置它即可,我们输入网址,它就可以加载网页,可以看到界面上这个控件区域就变成了网页界面了

上面这些想象,已经触发了我们的第一个问题:

究竟如何封装?换句话说,你是想要在界面库的原有真窗口上绘制这个控件呢,还是直接使用 Cef3 示例程序中的那个真窗口,直接调用真窗口呢?

这并不是一个简单的问题,引用一下网络上的原话:

所谓的 OSR,就是不创建真窗口,将整个页面渲染到一张位图上面。当然不只是渲染,还有一系列的 API 来处理鼠标、键盘事件,处理输入法事件等。

也就是说,如果我们直接调用真窗口,那么我们不需要思考界面的渲染,不需要考虑鼠标、键盘事件,对于你来说,这就是一个简简单单调用 exe 的事情。

但是,如果你想要在当前的真窗口上渲染出一个浏览器界面出来,就需要用到 Cef3 的离屏渲染(Off Screen Rendering)功能。

那么,离屏渲染的模式下,我们该如何封装我们的浏览器控件呢?

不用害怕,这个问题在 Cef3 的官方文档中已经有了详尽的(对小白不够友好 T_T)的阐述,大家可以点击下列网址查看:
Cef 官方文档:离屏渲染

在 CefClient 的子类实现中继承 CefRenderHandler 接口,其中比较重要的几个函数如下:

1. CefRenderHandler::GetViewRect:我们通过这个获取到所需要的可视区域

2. CefRenderHandler::OnPaint: 这个方法很重要!我们就是在这个回调函数中将我们的网页渲染绘制出来的

3. CefBrowserHost:WasResized(): 这个方法故名思意,就是在窗口大小变化的时候使得我们的浏览器画面也能适应窗口大小变化,这个方法会在我们的浏览器控件的 OnSize 事件中调用

这里,处理渲染,还有鼠标、键盘事件的响应等等(我是照着大神的代码敲了一遍,太过于繁琐,不过可以深入学习下),都属于离屏渲染封装的内容。

具体的逻辑可以参看代码,其中有关绘制的模块我也不是很清楚,官方文档只是指导思想,具体实现还是界面库的那些大神实现的,代码中有着他们深邃的思考,在下不敢妄自解读:)

三、消息循环:集成方式

在前面我们已经分析了两种封装方式,在 cefsimple 这个官方示例中,我们可以看到 CefRunMessageLoop 的消息循环,但是这只是一个真窗口方式调用的消息循环,想要在程序中嵌入 Cef3 显然不能使用这种模式的消息循环,那怎么办呢?

Cef3 已经为我们考虑好了两个方法来集成当前程序的消息环境:

  1. 周期性执行CefDoMessageLoopWork()函数,替代调用CefRunMessageLoop()
  2. 设置CefSettings.multi_threaded_message_loop=true(Windows平台下有效)

具体的可以看官方文档:
Cef3 官方文档:集成消息循环

消息循环很重要,于是乎,在 Soui 界面库中,我们书写了如下代码来封装了 Cef3 的 CefDoMesssageLoopWork 集成消息循环:

namespace SOUI{    class SMsgLoopCef : public SMessageLoop    {    public:        virtual BOOL OnIdle(int nIdleCount)        {               Cef3Loader::DoMessageLoop();            return __super::OnIdle(nIdleCount);        }    };    class SMsgLoopFactory : public TObjRefImpl<IMsgLoopFactory>    {    public:        virtual SMessageLoop *CreateMsgLoop()        {            return new SMsgLoopCef;        }        virtual void DestoryMsgLoop(SMessageLoop *pMsgLoop)        {            delete pMsgLoop;        }    };}

并且在程序的 _tWinMain 函数中使用该消息环境:

// Cef3: MessageLoopIMsgLoopFactory *pMsgLoopFac = new SMsgLoopFactory;theApp->SetMsgLoopFactory(pMsgLoopFac);pMsgLoopFac->Release();

其中,Cef3Loader::DoMessageLoop(); 封装了 CefDoMessageLoopWork 函数,Cef3Loader 类是一个用来管理 Cef3 机制的类,我们会在下一节中讲到。

四、代码结构:CefApp、CefClient 、Cef3Loader

说完了界面库该如何封装 Cef3,我们接下来就要考虑,如何在 Soui 中嵌入我们的 Cef3 环境。具体的库的支持文件该如何编译放置,比如 libcef_dll_wrapper 项目的编译等等,都在我的前几篇博客中有过讲解,因此这里重点以代码结构的角度好好阐述下。

其实有关代码结构的问题,官方文档说的非常清晰,在此摘录如下:

每个 Cef3 应用程序都是相同的结构
1. 提供入口函数,用于初始化 Cef、运行子进程执行逻辑或者 Cef 消息循环
2. 提供 CefApp 实现,用于处理进程相关的回调
3. 提供 CefClient 实现,用于处理 Browser 实例相关的回调
4. 执行 CefBrowserHost::CreateBrowser() 创建一个实例,使用 CefLifeSpanHandler 管理 Browser 对象生命周期

上面这短短的几句话,看起来很短很容易理解,但是实实在在的深入理解,还是得自己深入项目代码中探索(我也是经过了多次重新建立 Cef3 项目才感悟到的)。

在本篇博客中,对于上述的各个步骤有了如下的对应封装:

1. Cef3Loader: 这是一个用于管理 Cef3 机制的一个类,其中包含了 Cef3 的初始化、集成消息循环、结束的控制调用,以下是 Cef3Loader 类的定义:

namespace SOUI{    class Cef3Loader    {    public:        static void Initialize();        static void DoMessageLoop();        static void ShutDown();        static bool bInitialized;    };}

2. CefApp: 这个类是用来响应进程间消息处理的,这一块与后面的 JavaScript 调用 C++ 有关,而且实现有些复杂,我这里暂时不讲,待后面讲解

3. Browser: 继承自 CefClient 类,用来处理 Browser 实例的回调。这个类是编写难度最大的一个类,其中包含了页面渲染、鼠标键盘信息转化、Browser 实例生命周期管理、转发 JavaScript 调用请求、转发 C++ 调用请求等等内容,有关这个类的实现,可以参看代码

4. 创建 Browser: 这个方法是封装在 Browser 类的 open 函数中,而这个方法又会在浏览器控件类 SCef3Window 中的 OnCreate 方法中调用,也就是达到了控件初始化就自动创建一个 Browser 实例的功能

处理上述说到的 Cef3Loader 的 Cef3 机制管理类、Browser 的浏览器实例回调管理类,还有 SCef3Window 浏览器封装控件类(其中包含一个 Browser 类的实例对象)、ExtendEvents 类(用来封装 JavaScript 请求调用 C++ 代码的窗口消息类)等等,具体可以参看代码,相信我这么详细的阐述,已经能够让你比较清晰的熟悉代码结构了。

五、JavaScript 与 C++ 的互相调用

其实这方面的内容我已经在上一篇博客中讲述过了,这里之所以还要在讲述一下,是因为我自己在重新建立 Cef3 项目进行编写的时候遇到了比较难走的坑,这里记录下来。

首先说 C++ 调用 JavaScript,这个的实现实在是很简单,为什么呢?因为我们可以拿着 CefBrowser 的实例直接调用 ExecuteJavaScript 函数呀:

void Browser::executeJS(const wchar_t *code, const wchar_t *url, int start_line){    if (!code)        return;    CefRefPtr<CefFrame> frame = mainFrame();    if (!frame)        return;    frame->ExecuteJavaScript(code, url, start_line);}

其中 mainFrame 函数是由 CefRefPtr<CefBrowser> 获取 frame 的实现。

那么接下来,由 JavaScript 调用 C++ 就不简单了。

首先来说说上面代码结构中没有讲述的 CefApp 类的实现:要记住 Cef3 的项目大多都是多进程的,多进程的关键在于,你要意识到当前的进程在做什么。

比如程序当前运行的显示界面执行浏览器回调任务的,这就是 UI 进程,也就是 Cef3 官方定义的 TID_UI 进程;而我们用来渲染界面执行 JavaScript 行为的,则是在我们的 Render 进程上执行的,代码中将其定义为 PID_RENDERER。

那么 CefApp 实现是干什么的呢?是用来处理多进程信息交互的。也就是说,只要你当前的进程不需要处理其他进程的回调信息,那就不用实现它。

因此,在 UI 进程中,我们只需要程序运行,显示显示界面,完全不需要理会 Render 进程的回调信息,就不需要实现这个 CefApp 实现。

那么,在 Render 进程中,我们需要约定绑定的 C++ 和 JavaScript 都共同承认的一个调用约定,就得需要实现一个 CefApp 实现来管理它。

那么 Render 进程的代码工程在哪里呢?

在你的子进程中,也就是你在使用 CefInitialize 函数之前设置的 CefSettings 的 browser_subprocess_path 的路径的那个执行程序:

void Cef3Loader::Initialize()    {        SASSERT(!bInitialized);        CefMainArgs args(GetModuleHandle(NULL));        // cef settings        CefSettings settings;        settings.no_sandbox = true;        settings.multi_threaded_message_loop = false;        // cef sub process path        SStringW strAppPath;        strAppPath = _T("cefclient.exe");        CefString(&settings.browser_subprocess_path) = strAppPath;        settings.windowless_rendering_enabled = true;        settings.single_process = false;        // cef locate         CefString(&settings.locale) = "zh-CN";        // cef initialize        BOOL bOK = CefInitialize(args, settings, NULL, NULL);        bInitialized = true;    }

所以,这里我们使用的是官方的实例 cefclient 生成的 exe 进程,那么我们要让 cefclient 这个 Render 进程能够使用 JavaScript 调用我们 UI 进程的 C++ 代码,就得在 cefclient 项目中去修改(要认识到这一点很不容易,你要对 Cef3 的多进程有一定的认识,不然像我一样一直在 UI 进程找原因始终找不到)。

于是,打开 cef.sln,在 cefclient 中找到继承了 ClientApp 和 CefRenderProcessHandler 类的子类(我的是 Cef3071 版本,其中是 ClientAppRenderer 类),在其中的 ClientAppRenderer::OnContextCreated 函数中添加我们的注册函数即可,这里代码如下:

void ClientAppRenderer::OnContextCreated(CefRefPtr<CefBrowser> browser,                                         CefRefPtr<CefFrame> frame,                                         CefRefPtr<CefV8Context> context) {    (*it)->OnContextCreated(this, browser, frame, context);*/  CefRefPtr<CefV8Value> object = context->GetGlobal();  CefRefPtr<CefV8Handler> handler = new HtmlEventHandler();  CefRefPtr<CefV8Value> func = CefV8Value::CreateFunction("HandleEvent", handler);  object->SetValue("HandleEvent", func, V8_PROPERTY_ATTRIBUTE_NONE);}

在这个函数中注册了全局的函数 HandleEvent ,我们可以在 JavaScript 中调用,其中 HtmlEventHandler 封装了继承自 CefV8Handler 的方法 Excute,也就是每个 JavaScript 函数的执行都需要经过这里。我们在其中筛选我们需要的函数 HandleEvent 进行 UI 进程的消息发送:

bool HtmlEventHandler::Execute(const CefString& name,                                CefRefPtr<CefV8Value> object,                                const CefV8ValueList& arguments,                                CefRefPtr<CefV8Value>& retval,                                CefString& exception) {    if (name != "HandleEvent" || arguments.size() == 0)         return true;    CefRefPtr<CefBrowser> browser =        CefV8Context::GetCurrentContext()->GetBrowser();    CefRefPtr<CefProcessMessage> message =        CefProcessMessage::Create(arguments[0]->GetStringValue());    message->GetArgumentList()->SetSize(arguments.size() - 1);    for (size_t i = 1; i < arguments.size(); ++i)        message->GetArgumentList()->SetString(i - 1, arguments[i]->GetStringValue());    browser->SendProcessMessage(PID_BROWSER, message);    return false;}

然后再回到我们的 UI 进程中,在 Browser 类中的继承自 CefClient 类的 OnProcessMessageReceived 方法中,将我们收到的 Render 进程的消息转发给 UI 管理类显示即可。

整体的 JavaScript 调用 C++ 的逻辑就是上述这些,还有很多细节,比如如何将 Browser 类中收到的消息转发给 UI 管理类,不过这都是 Soui 界面库的相关知识了,也就不再赘述了。

六、其他问题:修改大神 Demo 里的 bug

然而大神的 Demo 并非十全十美,在解决大神的 Demo 里面的 bug 的过程中,我还是学到了不少,这里总结一下:

问题1. 网页链接鼠标点击无法跳转

这个问题其实比较好定位,肯定是在 Browser 类中处理鼠标信息没有处理好,经过查找,发现原来在这里:

void Browser::sendMouseClickEvent(UINT uMsg, WPARAM wp, LPARAM lp){    ...    evt.modifiers = GetCefMouseModifiers(wp);    host->SendMouseClickEvent(evt, mt, bMouseUp, nClickCount);}

其中 host->SendMouseClickEvent 函数传入的第四个参数,nClickCount 传入为当前鼠标按钮的点击次数,大神这里默认传入了 0,导致 Cef3 底层认为点击次数为 0,因此出了点击网页链接不能跳转的问题。

问题2. 关闭界面崩溃

这个问题实在是太常见了,不过这里引用蓝先生的一句话:

(关闭崩溃的问题也就如下两个原因)没调用关闭浏览器的接口,或者调用了,还没等到关闭就调用CefShutdown

蓝先生总结的很好,其实就是这两个原因,好好检查下打下断点就可以解决了。

2017-8-23 更新:
有一个网友与我联系,发现他在使用 Cef2623 版本的时候,怎么都会发生退出中断的情况。我拿着他的代码反复调试,甚至将其代码全部更换为我的代码之后,发现还是有这个问题,于是研究了很久,初步有了以下结论:

1. 如果在你确保你在调用 CefShutdown 之前调用了关闭浏览器的函数,并且之后成功调用了 CefShutdown 函数,那么可以推测,这也许是 Cef2623 版本在关闭浏览器的时候,在之后调用 CefShutdown 函数之前浏览器还没有被关闭完毕。

此时,你可以在父窗口的 OnClose() 函数处加一个定时器,查询到浏览器关闭完毕后再摧毁窗口,从而调用 CefShutdown 函数。那么,怎么实现当前打开的浏览器的计数呢?很简单,在 OnAfterCreated() 处增加计数,在 OnBeforeClose() 处减少计数即可。(关于 Cef2623 解决关闭崩溃问题的具体方式,可以参看我的 Demo wangying2016/Cef3-Soui-Demo)

2. 通过我的测试, Cef3071 版本就没有这个问题,可以放心调用关闭浏览器的函数之后调用 CefShutdown 安全结束进程(这一块也就是本篇博客的实验代码中显示的效果)。

问题3. JavaScript 调用 C++,发现 OnContextCreated 未被调用

OnContextCreated 这个函数是注册 JavaScript 绑定函数的关键,这个函数没调用可是大问题。

不过,排除你已经继承了 CefRenderProcessHandler 并且实现了 CefApp 的方法 GetRenderProcessHandler (其中 return this)之后,还有问题的话,那大多是因为你没认识到什么是多进程。

之前说过,Cef3 的运行大多是多进程的,程序运行是 UI 进程,JavaScript 等等执行则是在 Render 进程,你想要在 UI 进程在 Debug 环境下打 Render 进程的断点,那是不可能命中的,这就犹如你想要去北京,却买了张去深圳的火车票一样。

想要调试可以在 Render 进程(我们这个项目则是 cefclient 官方示例项目)中使用 MessageBox 或者打日志等等。

七、总结

接触 Cef3 也有两个周了,从编译运行官方代码,到现在自行从零建立一个 Cef3 Demo,着实遇到了不少的坑。不过虽然过程是痛苦的,结果和收获却是美好的。

这里附上本篇博客的实验代码:
wangying2016/Soui-Cef-By-CefClient

代码可能不会成功编译,你需要参看我之前的博客,自行编译 libcef_dll_wrapper,使用 CMake 生成适合自己的运行环境的代码,这些坑都需要你一步一步的踩(T_T),其中 JavaScript 调用 C++ 功能实现中,与官方示例代码 cefclient 联系紧密,你需要自己编译 cefclient,并且在运行的时候将 cefclient 中 Debug 文件夹下的所有文件拷贝到当前项目的运行环境中(Debug/Release)。

最后,当然得好好感谢下 Soui 群里的蓝先生和启程,这两位大神在我的痛苦探索之路中,给了我莫大的帮助!

Cef3 的内容很多,想要深入了解还需要很多很多时间:

To be Stronger!

原创粉丝点击