首页 > 代码库 > Chromium Graphics: 再谈Chromium WebView硬件渲染模式的演进
Chromium Graphics: 再谈Chromium WebView硬件渲染模式的演进
摘要:从Android KitKat系统第一个采用Chromium内核的WebView开始,Android WebView一直在持续演进中,自Chromium M38开始,WebView在硬件渲染模式方面发生了较大的变化,最明显的变化莫过于WebGL的支持以及ubercompositor的使用,同时为了吻合Android L的渲染模型变化,DrawGL函数是在Android系统的渲染线程中执行的。
Android 4.4系统WebView的硬件渲染
对于Chromium WebView来说,首先它是单进程模型的,至少在使用CommandBuffer时可以省去不必要的跨进程IPC开销,尽管IPC开销并没有想象中那么大(根据性能评测,大概占了不超过5%的开销)。更为重要的是,WebView必须是Android窗口中一个普通的View,与窗口其他View共享同一个Android Surface绘制表面,因此,
- WebView在UI线程上响应onDraw事件,启动页面渲染的流水线工作,将新的帧内容通过onDraw绘制到Canvas上;
- WebView使用同步合成器(SynchronousCompositor)来合成页面内容,“同步”指的是运行在UI线程上,合成操作是由UI线程的onDraw事件触发的,而不是VSYNC触发的。从线程化合成器的角度看,UI线程实际上也是Compositing线程;
- WebView提供了一个硬件加速版本的回调函数DrawGL(即AwContents::DrawGL),将Chromium的渲染流水线工作串到AndroidView的渲染路径上。在使用硬件加速的情况下,WebView.onDraw接受的参数Canvas是一个对Android SDK不可见的android.view.HardwareCanvas对象,HardwareCanvas允许客户端提供自己的GL回调函数,Android View系统在重放(playback)显示列表绘制View层次时会调用这个回调函数。(注:在AndroidFramework内部实现中,当开始绘制HardwareCanvas时,当前GL上下文会绑定HardwareCanvas的framebuffer对象,所以GL回调函数中所有的GL命令实际上在操作HardwareCanvas的framebuffer对象)。
Android 4.4系统中,在绘制View层次时,显示列表的更新记录以及重放都发生在UI线程上,相应地,WebView.onDraw事件的处理中,页面的层合成,以及通过DrawGL函数将合成的内容绘制到HardwareCanvas,都是在UI线程执行的。当WebView收到invalidate消息后,ViewRoot会遍历整个View树决定向哪些View派送onDraw事件,WebView响应onDraw事件时,会直接请求HardwareCanvas执行DrawGL回调函数,DrawGL再请求同步合成器以硬件加速方式合成新的一帧内容,并将这个新的内容通过GL命令绘制到HardwareCanvas上。这里没有特别提到Blink线程,原因是Blink线程相对来说还是比较独立的,在Blink线程启动后,会逐步执行DOM解析和排版,JS代码的执行,以及渲染层的状态信息计算等工作,这些工作运行完毕之后,如果Compositor内部状态机发现页面还需要继续更新内容,执行动画,Blink线程会通知UI线程(即Compositing线程)将WebView置为失效(invalidate)状态,继而触发onDraw事件,再把上述过程走一遍。具体过程可参考如下图示:
必要的解释:
- Blink线程启用了Impl-Side-Painting方式,在绘制页面内容时,先将绘制命令录制到一个SkPicture中,稍后再在Raster线程中重放这些绘制命令,从而尽可能的降低Blink线程的负载;
- Raster线程是通过zero-copy技术直接将rasterization绘制到GPU内存中,从而减少了一次CPU到GPU的数据拷贝开销;
- 整个渲染过程中只有一个Compositor,在UI线程将已更新的Web页面内容执行合成操作,写到HardwareCanvas后端的FBO中,然后由Android SurfaceFlinger将最后的UI呈现在屏幕上
- 步骤1,2,6和步骤3,4,5是可以同时进行的,步骤3,4,5总是在准备下一帧内容
这里还需要特别说明的是,Android 4.4系统中WebView是没有单独的GPU线程执行GL命令的,所有的一切都发生在UI线程上,导致很难再提供WebGL的支持了,就算是花些力气支持了WebGL,估计也是吃力不讨好,因为性能是个大问题,在UI线程上执行WebGL很可能导致UI根本不会有响应。
Android L系统WebView的改进
Chromium WebView的演进速度还是相当的快。从最新发布的Android L开发者预览版来看,在页面渲染的流水线和线程模型都有不少改进,如下图所示:
ubercompositor的使用
简单的来说,ubercompositor是一个大一统的合成器,不管Chromium系统中存在几个合成器,但最终的合成总是由一个称为Parent Compositor完成,其他的Compositor唯一要做的事情是将自己管理的GPU资源打包为CompositorFrame发送给Parent Compositor,由Parent Compositor根据CompositorFrame包含的RenderPass列表和元数据等,执行合成操作将最后的内容写到Native窗口上,当Parent Compositor完成合成操作,它会通知Child compositor可以执行资源的清理工作了,如下图所示:
为了吻合Android L引入的渲染线程(注:为了和Android系统的渲染线程作区分,Chromium的渲染线程称为Blink线程),将Chromium渲染流程无缝整合到Android L系统中,WebView新的架构中引入了两级合成器:
- Child Compositor:页面内容的合成器,就是前面提到的同步合成器,运行在UI线程上,是一个线程化的合成器,其中Blink线程负责生成渲染层的状态信息,而UI线程负责合成渲染层。每收到onDraw事件后,WebView会使用ChildCompositor合成新的一帧,并将内容打包到CompositorFrame结构体中,稍后Android渲染线程会访问这个结构体。ChildCompositor由android_webview::BrowserViewRenderer创建;
- Parent Compositor:类似于Chrome on Android中Browser进程端的合成器,是一个单线程版本的合成器,其作用是在Android系统的渲染线程,注意不是UI线程,通过DrawGL回调函数将已经由ChildCompositor合成好的CompositorFrame绘制到HardwareCanvas上。ParentCompositor由android_webview::HardwareRender创建;
与Android 4.4系统不同的是,在Android L系统上WebView处理onDraw事件逻辑上可以分解为两个步骤:
- 第一步,在UI线程上调用Child Compositor合成操作生成新的CompositorFrame,再向Android系统请求执行DrawGL回调函数,但此时DrawGL并不会立即执行。UI线程负责为发生invalidate操作的View生成显示列表,再将这个显示列表同步给渲染线程上的ThreadedRenderer渲染器;
- 第二步,Android View系统在渲染线程上将显示列表提交给GPU驱动,绘制View层次的内容,在绘制显示列表的过程中,DrawGL函数将会被调用,在渲染线程上请求Parent Compositor将合成的页面内容绘制到HardwareCanvas上。
注:关于DrawGL回调函数是如何分派到Android渲染线程上执行的,由于AndroidL的源代码尚未完全公开,还不能100%确定上述描述是否完全正确。从已有掌握的信息分析来看,UI线程会向HardwareCanvas发起DrawGL调用请求,此时可能不会立即执行DrawGL,而是作为drawOp先保存在HardwareCanvas中,当渲染线程开始重放HardwareCanvas显示列表时,再调用DrawGL。
上述两个步骤与Android L采用的渲染线程模型完全吻合,这也是要引入两级合成器的原因。AwContents::DrawGL是将Chromium图形系统整合到Android中的一个极其关键的方法,它是Android L渲染线程进入Chromium图形系统的唯一入口,DrawGL是在Android L在渲染线程为HardwareCanvas对象重放显示列表被调用的,而另一方面,Chromium只能被动地由渲染线程调用DrawGL操作,唯一能够获取的信息是当前HardwareCanvas后端的FBO对象,然后所有的内容都绘制到这个Canvas上,其他信息诸如消息队列,异步执行任务的能力,都难以获取,这就导致Chromium会在DrawGL中,只能一次性地将任务队列中所有的任务,例如等待SyncPoint,刷新CommandBuffer等,全部执行完毕,这也就是为什么下面提到的WebGL要在一个专门的GPU线程中执行的原因。
此外,WebView这个新型的渲染模型与Android4.4系统是不兼容的,也就是从AndroidL编译出来的libwebviewchromium.so是不能够在Android 4.4系统运行的。
WebGL和硬件加速Canvas 2D的支持
以WebGL为例,WebView为渲染WebGL还创建 了专门的GPU线程,WebGL程序中所有的GL命令都发生在这个线程上。渲染WebGL的egl上下文就是这个线程上创建的,然而,这个egl上下文是一个独立的上下文,不与WebView进程内任何其他上下文共享资源,即与AndroidView系统的上下文不在同一个share group。关于Chromium中上下文和share group相关技术细节,可参阅ChromiumGraphics: 3D上下文及其虚拟化一文。Android View系统的上下文指的是调用DrawGL绘制HardwareCanvas时的上下文,因此,要将WebGL的内容绘制到HardwareCanvas上,需要解决将WebGL上下文和AndroidView系统上下文之间的texture共享和同步问题。首先,这两个上下文都属于同一进程,因此可以利用EGLImage进行跨线程的texture共享,再次,Chromium图形栈提供的信箱texture(mailbox texture)机制有助于解决texture共享。Mailbox texture机制为每个生成的texture生成一个64字节的唯一名字,并建立名字到texture id的全局映射表,所以不管在哪个上下文创建的texture,都可以通过mailbox的名字检索到对应的texture id,mailbox机制的好处就是在一个支持多上下文的环境提供了textureid的全局别名系统,换句话说,有了这个别名系统,所有上下文看起来都属于“同一个share group”。
那么,渲染WebGL是如何使用EGLImage和mailbox texture机制实现资源共享的呢?
Chromium中WebGL的内容是通过离屏的framebuffer对象渲染到texture中的,在GPU线程的WebGL上下文创建这个texture时将其绑定一个唯一的mailbox名字,还会调用eglCreateImageKHR为这个texture id创建一个可以被其他上下文共享的EGLImage,当AndroidView系统的渲染线程在另一个上下文中执行DrawGL函数,通过mailbox名字可确定GPU线程中的EGLImage,将其绑定到该上下文的一个texture便可以直接使用WebGL上下文的texture了,至此解决了texture共享问题。这里还有一个问题需要交代一下,在多个上下文中共享texture时,必须处理好生产者和消费者之间的同步问题,即执行DrawGL函数绘制新的一帧时,要确保WebGL上下文的所有GL命令都已经执行完毕。Chromium同样提供了一套同步点(syncpoint)机制,来保证多个上下文(Chromium里也称CommandBuffer)之间GL命令的执行次序,在上下文A中插入一个同步点,然后在上下文B中等待这个同步点,可以保证上下文B中的GL命令必须等待上下文同步点A之前所有的GL命令全部执行完毕之后,才能开始执行。
所以,当WebGL将新的一帧内容生成完毕后,会插入一个同步点,而DrawGL时AndroidView系统上下文会等待这个同步点,只有当前WebGL所有GL命令都提交给GPU驱动之后,才开始使用WebGL的内容,从而保证了WebGL内容和DrawGL的同步。关于同步点机制的细节,请参阅ChromiumGraphics: GPU客户端之间同步机制的原理和实现分析一文。
AndroidWebViewShell也支持硬件渲染模式
WebView Shell构建在核心类AwContents之上,AwContents在功能基本上可以等同于android.webkit.WebView类。Chromium M30的代码库中只能编译出仅支持软件渲染模式的WebViewShell,从Chromium M38开始,WebView Shell也支持硬件加速渲染了,主要改进的地方是,使用GLSurfaceView作为最后的渲染目的地,如上所述,DrawGL回调函数可以在非UI线程上调用,而GLSurfaceView正好也内建性地支持在单独的线程中被渲染。
小结
Chromium Graphics: 再谈Chromium WebView硬件渲染模式的演进