跨进程使用浏览器渲染


发布于 2020-12-13


现在基于浏览器开发的桌面客户端越来越多了。最早的vc++里面的WebBrowser控件就不提了。优点是系统自带,小巧灵便。缺点是太古老了,IE内核。目前桌面客户端最流行的使用浏览器开发应该是CEF(Chromium Embedded Framework ()、Electron,或者是直接基于Chromium开发。

以CEF来说,开发者的客户端必须跟CEF的浏览器编译到同一个进程里。浏览器本身很庞大,这导致使用者的客户端因为链接加载了浏览器也变得很大,导致客户端的启动速度变慢,内存占用也变多了。此外把使用的客户端和浏览器编译链接到一起,也影响了客户端的稳定性。

有时候客户端也不是一直都需要浏览器的功能,最好能够按需的开启关闭浏览器,这样也能提升性能。但是CEF默认跟客户端编译在一起,不太容易做到灵活使用。

根据我的经验,有4种跨进程使用浏览器渲染的办法。

CEF离屏渲染

之前写文章介绍过CEF离屏渲染。把客户端进程可以与CEF进程分开,客户端提供一个宿主的窗口,并把这个窗口的鼠标键盘消息传递给CEF主进程,然后CEF离屏渲染之后,客户端进程在接收渲染的结果,绘制到自己的窗口是。

但是CEF离屏渲染时通过软件渲染的,性能是比较落后的。在大画面更新的情况下,CPU占用和延迟是有一些影响的。

跨进程设置父子窗口

正常情况下使用GPU合成渲染,Chromium的窗口层次从底到上如下:

Chrome_WidgetWin_1
  Chrome_RenderWidgetHostHWND
  Intermediate D3D Window

Chrome_WidgetWin_1是主进程最底层的窗口,Chrome_RenderWidgetHostHWND是主进程里面页面webview的伪窗口,不是用于UI渲染的,不起作用。Intermediate D3D Window是GPU进程里接收绘制结果的窗口。

我们可以在客户端进程创建一个宿主窗口,然后在浏览器进程创建webview,再把webview所在的窗口设置成宿主窗口的子窗口。

跨进程设置父子窗口细分成2种技术方案。

禁用GPU合成

不禁用GPU的情况下,页面的webview其实是没有真正对应的窗口的,也就无从设置窗口窗口关系了。只有禁用GPU合成,才会把伪的Chrome_RenderWidgetHostHWND窗口转换成真的窗口,用于设置父子关系。

一般通过--disable-gpu或者--disable-gpu-compositing来禁用GPU合成。

这种技术方案的优点是禁用之后对渲染的性能有影响,缺点是实现起来比较简单快捷。

阿里巴巴的千牛客户端就是使用此种技术方案。

3层窗口

还有种入侵的技术方案,是直接把外部的窗口ExternalHostWnd设置成浏览器最底层窗口的父窗口,这样形成了一种3层窗口层次,如下所示:

ExternalHostWnd
  Chrome_WidgetWin_1
    Chrome_RenderWidgetHostHWND
    Intermediate D3D Window

这种方案的优点是浏览器继续可以使用硬件加速合成,渲染的性能依旧不错。缺点是需要修改Chromium内部代码,做一些UI兼容。

拼多多的商家客户端就是使用此种技术方案。

跨进程设置父子窗口的缺点

以上两种跨进程设置父子窗口技术方案都已一个大的缺点,就是跨进程设置父子窗口关系,会导致跨进程的父子窗口UI线程强制消息循环强制同步,会导致一些卡顿。

引用吕毅的文章使用 SetParent 跨进程设置父子窗口时的一些问题描述:

一般来说,每个创建了窗口的线程都有自己独立的消息循环,且不会互相影响。然而一旦这些窗口之间建立了父子关系之后就会变得麻烦起来。

Windows 会让具有父子关系的所有窗口的消息循环强制同步。具体指的是,所有具有父子关系的窗口消息循环,其消息循环会串联成一个队列(这样才可以避免消息循环的并发)。

根据我自己使用经验,跨进程设置父子窗口的时间开销有时候超乎想象,仅仅调用一个SetParent API可能耗时几百毫秒,改变窗口的大小也很耗时。

外部底层窗口

最后一个外部底层窗口的方案,最复杂,但是技能使用上GPU硬件加速,又能规避跨进程设置父子窗口的缺点。

这个技术方案还是客户端进程创建一个宿主窗口,然后把这个窗口句柄传递给浏览器主进程,当浏览器创建wevview的时,使用外部底层窗口做它的底层窗口,而不是浏览器主进程本身去创建。这样就形成了这样的2层的窗口层次

ExternalHostWnd
    Intermediate D3D Window

另外客户端进程的宿主窗口还需要把自己鼠标键盘消息通过IPC传递给浏览器主进程,把用户的交互转发给wevview。

看起来Intermediate D3D Window还是ExternalHostWnd的子窗口,还是跨进程的父子窗口,存在跨进程设置父子窗口的缺点?

ExternalHostWnd是存在于客户端的进程种,Intermediate D3D Window存在于GPU进程种。Intermediate D3D WindowWS_EX_LAYEREDWS_EX_TRANSPARENT,不接收处理窗口消息,仅仅是用来接收绘制的UI结果,所以就没有消息循环强制同步的问题。

参考