@rogeryi
2015-01-28T14:59:07.000000Z
字数 28414
阅读 13041
Android
WebView
Compositor
Rendering
作者: 易旭昕 (@roger2yi)
说明: 访问 Cmd Markdown 版本可以获得最佳阅读体验
Chrome Android WebView 研究系列文章
本文主要描述 Chromium Android WebView (下文简称CAW)的渲染架构,在阅读本文之前,需要先阅读 Threading of Chromium Android WebView,了解 CAW 的线程架构。
Chromium 的代码非常复杂,除了一个现代浏览器本身所需要的复杂度以外,其它一些因素也导致了额外的代码复杂度:
- Chromium 是跨平台的,它需要同时支持多个不同的 Platform Configurations,从一个 Embedded WebView (Android WebView) 到一个完整的 OS (Chrome OS),不同的 Platform Configurations 使用的进程/线程架构和渲染架构都不尽相同;
- 多进程架构和 Threaded Compositing 架构导致大量间接层的产生,同样用途的对象同时存在由不同进程/线程所拥有的特定版本;
- Chromium 为了把自己的代码和 WebKit 内核的代码更好地进行隔离,避免一方发生变化对另一方的影响,它在两者之间建立了一道防火墙,人为地加入了一个间接层;
- Chromium 的设计习惯通常采用一个中心对象,周围围绕着若干关联附属对象,附属对象为中心对象承担部分职责的方式。这种方式有利于不同功能的分离和代码维护,但也间接造成了对象数量的膨胀;
因为 Chromium 的代码是这样的复杂,所以先了解它的系统层次结构,还有对象命名规律是十分有必要的。
我们把一个基于 CAW 的浏览器应用的系统层次结构划分如下:
Blink (WebKit)
WebKit 内核,现在改成 Blink,但是基本上还是那些我们熟悉的对象,Frame,FrameView, Document,Node,RenderObject,RenderLayer,GraphicsLayer,RenderLayerCompositor 等等。Chromium 需要为 WebKit 提供一些平台相关的适配模块,比如 2D 绘图(Skia),网络(Chromium Net)等。
代码主要位于目录 - external/chromium_org/third_party/WebKit。
Blink Public
早期 WebKit Glue 是 Chromium 为了更好地隔离 WebKit 和 自己的代码所建立的一道防火墙,而自从 Blink 独立后,它逐渐演变成为 Blink Public,变成是 Blink 对外公开的 API 层。它包括的对象类型通常跟 WebKit 里面的对象类型相对应(一般使用 WebXXX 前缀,比如 WebFrame 对应 WebCore::Frame),它把 WebKit 里面的对象重新封装,然后再供 Chromium 的其它模块调用,主要对象包括 WebView,WebWidget,WebFrame 等。
代码主要位于目录 - external/chromium_org/third_party/WebKit/public。
Content
Content 层代码对 WebKit 内核进行封装,对外提供一个支持多进程架构的 WebContents 对象,可供进一步再被封装成可以嵌入浏览器 UI 界面的 UI WebView。
Content 层的多进程架构可以分为 Renderer 端和 Renderer Host 端,Renderer 端位于 Render 进程的 Render 线程,包括 RenderProcess[Impl],RenderThread[Impl],RenderView[Impl],RenderWidget,RenderWidgetCompositor 等,而 Renderer Host 端位于 Browser 进程的 UI 线程,包括的对象基本跟 Renderer 端相对应 - RenderProcessHost[Impl],RenderThreadHost[Impl],RenderViewHost[Impl],RenderWidgetHost[Impl],Renderer 端和对应的 Host 端对象之间通过各自进程的 IO 线程进行 IPC 通讯,它们最后由 WebContents 封装,对外提供一个可以展现网页内容和处理事件的完整的网页封装对象,可供不同的平台再包装成一个适合该平台的 UI WebView。
代码主要位于目录 - external/chromium_org/content。
Chromium Compositor
Chromium Compositor(下文简称为 cc)可以看作是属于 Content 层,但是本身足够庞大和复杂成为一个独立的模块。cc 实现了一套基于 cc::Layer[Impl] 的 Layer Content Record,Layer Content Rasterize,Layer Composite 的图层渲染架构,支持同步或异步的渲染模式,支持硬件和软件的合成输出模式等。cc 的一部分对象位于 Render 线程(一般称为 main-side),包括 Layer, PictureLayer,LayerTreeHost 等,一部分对象位于 Compositor 线程(一般称为 impl-side),包括 LayerImpl,PictureLayerImpl,LayerTreeImpl,LayerTreeHostImpl,GLRenderer 等,还有一部分对象负责两个线程之间的通讯和主要任务的调度决策,包括 ThreadProxy,Scheduler 等。
代码主要位于目录 - external/chromium_org/cc。
Android WebView
对 Content 层代码进行封装,对外提供一个可供第三方应用使用,或者可包装成一个完整浏览器应用的 Android WebView。主要对象包括 AwContents,AwContents.java,ContentViewCore.java,WebViewChromium.java,WebView.java。
代码主要位于目录 - external/chromium_org/android_webview,frameworks/webview/chromium,frameworks/base/core/java/android/webkit。
Shell (Android Browser App)
将 CAW 包装成一个完整的浏览器应用所需的代码。
Chromium 主要对象的命名遵循一定的规律,了解这些规律有助于我们更快更好地理解代码。
修饰名称 | 说明 |
---|---|
Web | 带 Web 前缀的对象一般是一个 WebKit Glue 模块对象,通常跟一个 WebKit 内核的对象相对应,比如 WebFrame 对应 WebCore::Frame |
Impl | Impl 后缀一般由两种用途 - 1. 表示某个接口具体的实现类,比如 RenderViewImpl 实现 RenderView 接口; 2. 如果在 cc 模块里面,通常表示某个 main-side 对象在 impl-side 的对应对象,比如 cc::LayerImpl 对应 cc:Layer |
Host | Host 修饰一般由两种用途 - 1. 表示某个位于 Renderer 端对象在 Host 端的对应对象,比如 RenderProcessHostImpl 对应 RenderProcessImpl;2. 如果在 cc 模块里面,通常表示一棵 Layer 树的管理者对象,比如 LayerTreeHost 管理一棵 cc::Layer 树 |
Client | Client 后缀一般用于某个对象定义一个回调接口,供其它对象实现,然后自己通过这个接口调用对方,比如 GraphicsLayer 定义了一个 GraphicsLayerClient 接口,CompositedLayerMapping 实现了该接口,可被 GraphicsLayer 通过这个接口调用 |
Delegate | 跟 Client 有些类似,用于某个对象通过定义的一个 Delegate 接口分配特定的职责到其它对象(delegator and delegatee) |
cc 模块提供了一套基于 cc::Layer 的图层渲染架构,它是 Chromium 渲染架构的核心,作用类似 Apple Cocoa 里面的 CoreAnimation。cc 可以支持同步/异步的合成模式和硬件/软件的渲染模式。在 CAW 中使用的是同步合成模式,UI 线程和 Compositor 线程是同一个线程,如果 UI 线程中 WebView Viewport 的大小/位置,网页的滚动位置,WebView 的 Transform 矩阵等属性发生改变,这些新的属性值会马上传送到 Compositor 里面,用于接下来的这一帧的绘制,简单说合成属性的改变和使用这些新的属性值合成新的一帧是连续的调用,在同一个 Draw Loop 里面。另外,CAW 同时支持硬件和软件的渲染模式,跟旧的 Android WebView 一样,使用硬件还是软件的渲染模式取决于传递给 WebView 的 Canvas 是一个硬件加速的 Canvas 还是软件绘制的 Canvas。
Chromium Layer 的种类之多简直令人发指,粗略来说,它可以分为 3 大类:
包括 RenderLayer 和它的 Composited Layer - GraphicsLayer,跟原来差别不大,还是原来的配方,还是熟悉的味道。不过 RenderLayer 到 GraphicsLayer 的映射关系跟原来 WebKit 有所不同,并且原来的 RenderLayerBacking 也变成了 CompositedLayerMapping,它负责管理 RenderLayer 到 GraphicsLayer 的映射。
包括 WebLayer[Impl] 和 WebContentLayer[Impl],其中 WebLayer 是 GraphicsLayer 的 Platform Layer,它们的实际用途主要是 GraphicsLayer 跟 cc:Layer 之间的桥接。
cc::Layer 是 cc 模块 Layer 最基本的接口,cc::LayerImpl 是 cc::Layer 在 impl-side 的对应对象,cc::Layer[Impl] 跟旧的 Android WebView 中的 LayerAndroid 基本类似。cc::Layer 最常见的实现类是 PictureLayer,它被用于普通的网页元素,顾名思义,它可以将 Layer 的内容纪录到内部的 PicturePile 对象里面。cc::Layer 所构成的 Layer 树由 LayerTreeHost 进行管理,LayerTreeHost 在 impl-side 对应的对象是 LayerTreeHostImpl。LayerTreeHostImpl 实际需要管理两棵 impl-side 的 Layer 树(LayerTreeImpl 可以看作是一棵树的管理者),一棵 active 的 Layer 树是正在绘制的,一棵 pending 的 Layer 树是在后台进行预绘制的,当它 ready 的时候就会变成 active 的 Layer 树,这个跟旧的 Android WebView 里面 SurfaceCollectionManager 管理两个 SurfaceCollection 是类似的。
当一个 WebView 被创建时,它需要先创建 RenderViewHostImpl(同时还会创建 InProcessViewRenderer 和 SynchronousCompositorImpl),当它第一次开始加载一个 URL 的时候,它需要先初始化 Compositor(如果是第一个 WebView 第一次加载 URL,还要先启动 Render 线程,创建和初始化 RenderProcessHostImpl,RenderProcessImpl 和 RenderThreadImpl 等对象)。Compositor 的初始化过程会导致大量的对象在 UI 线程(同时也是 Compositor 线程)和 Render 线程被创建。
初始化开始时,UI 线程会发送一个消息给 Render 线程要求创建 RenderViewImpl,而 RenderViewImpl 的创建和初始化会导致 FrameView,RenderLayerCompositor,WebViewImpl,ChromeClientImpl,RenderWidgetCompositor,LayerTreeHost 等相继在 Render 线程被创建,接着 LayerTreeHost 创建了 SynchronousCompositorOutputSurface,并使用它创建和初始化 ThreadProxy,然后通过 ThreadProxy 发送一个同步消息给 Compositor 线程(也就是 UI 线程),要求创建 LayerTreeHostImpl,最后 Compositor 线程 会从 ThreadProxy 里把 SynchronousCompositorOutputSurface 取出来传递给 SynchronousCompositorImpl。
cc 主要任务的运行都需要通过 Scheduler 来调度,Scheduler 内部包含一个 SchedulerStateMachine 的状态机用于纪录 cc 当前的状态,通过这些状态来决定下一步要做什么。
网页内容的变化,包括元素内容的变化,DOM 树结构,Layer 树结构发生改变等,这些导致 cc::Layer 树的结构或者内容的变化最终都会在 Render 线程通过 ThreadProxy::SetNeedsCommit 发送消息 SetNeedsCommitOnImplThread 给 Compositor 线程处理,Compositor 线程在 SchedulerStateMachine 中会纪录下需要 Commit 的状态,然后在下一帧绘制(BeginImplFrame)的接近结束时,通过 ThreadProxy::ScheduledActionSendBeginMainFrame 发送消息 BeginMainFrame 给 Render 线程,让 Render 线程开始 cc::Layer 树的更新。
ThreadProxy::BeginMainFrame 负责在 Render 线程中更新 cc::Layer 树,它会通过 WebViewImpl 要求 WebKit 内核先进行重排版,然后再通过 LayerTreeHost 更新 cc::Layer 树的结构,内容和属性,跟旧的 Android WebView 一样,所谓内容更新,就是把网页元素的绘制通过 SkPicture 纪录下绘制指令,保存在 PictureLayer (cc::Layer 主要的实现类)里面。
当 ThreadProxy::BeginMainFrame 更新完 cc::Layer 树后,它会发送一个消息 StartCommitOnImplThread 到 Compositor 线程要求 Commit 这棵更新后的 cc::Layer 树,然后 Render 线程进入阻塞状态等待 Compositor 线程处理。最终 ThreadProxy::ScheduledActionCommit 会在 Compositor 线程被调用到,它会复制 cc::Layer 树生成 cc::LayerImpl 树,PictureLayer 所拥有的 PicturePile 会复制到它在 impl-side 的对应对象 PictureLayerImpl 中 (TreeSynchronizer::SynchronizeTrees),这棵 cc::LayerImpl 树一开始会先处于 pending 状态,等待光栅化,只有当它可见区域的部分完成光栅化和纹理上传后,才会取代当前 active 的 cc::LayerImpl 树,用于下一帧的绘制。
LayerTreeHostImpl 管理两棵 LayerTreeImpl 树,其中 pending LayerTreeImpl 的 dirty region,和 active LayerTreeImpl 在网页滚动过程中出现纹理缺失的部分都需要通过光栅化生成相应区域的纹理。
跟其它浏览器一样,cc::Layer 的光栅化也是分块的,PictureLayerImpl 会通过 PictureLayerTilingSet 对象管理一个或者多个 PictureLayerTiling 对象,每个 PictureLayerTiling 对象定义了在某个缩放比例下的分块策略(通常有两个 PictureLayerTiling,一个是正常缩放比例的分块,一个是缩略图比例的分块),这个跟旧的 Android WebView 里面一个 SurfaceBacking 管理多个 TileGrid 是类似的。
CAW 的分块策略是固定大小和可变大小两种策略的结合,具体规则如下(PictureLayerImpl::CalculateTileSize):
- 当前的固定分块大小是 384,最大分块大小是 500(实际上会被对齐到512);
- 如果 Layer Content Size (Layer Size * Scale)长宽都大于 384 并且任意一边大于 500,则用 384x384 进行分块;
- 其它情况下分块大小为长宽和 500 的最小值;
- 最终所有分块大小都需要对齐到 64 的倍数;
相比旧的 Android WebView 只采用固定 256x256 的分块大小,这样的分块策略对于较小的 Layer 或者狭长形状的 Layer,减少了浪费,比如 1000 x 12 大小的 Layer 只需要两个 512 x 64 的分块而不需要四个 256 x 256 的分块,但是可变大小的分块使得纹理的回收重用变得更复杂,需要维持一个较大的纹理缓存池来避免频繁地分配和释放纹理。
另外,为什么采用 384 的固定分块大小,个人的猜测如下:
- 384 是 64 的倍数,这个大小对于 Android 的内存分配器 ION 应该是比较友好的,不会产生浪费,GPU 贴图时的兼容性和性能也不会有问题,这也是非 384 的分块大小需要进行 64 对齐的原因;
- 在主流的 720p 和 1080p 设备上,384 相比 256 的分块大小,对于宽度为屏幕宽度的 Layer,比如说 Base Layer,同样面积下需要的分块数目更少。更大的分块可以减少合成时的 GL Draw Call 次数,从而提升合成性能;
个人的看法在 480p 设备上采用 256,2k 设备上采用 512 的分块大小应该会更好,不过目前没有看到 CAW 上有类似的处理;
PictureLayerTiling 所创建的 Tile 对象都会注册到一个全局的 TileManager 对象里面,TileManager 会将所有注册的 Tile 按一定的规则计算优先级,分成几个类别,然后根据当前的可用纹理缓存状况(GPU Memory budget)选择出若干优先级最高的 Tile 准备光栅化,准备光栅化的 Tile 会被分配 Resource,然后生成 RasterTask,所有 RasterTask 会被放入一个队列里面,这个队列会发送给 ImageRasterWorkerPool 等待光栅化。
上述的 Resource 对象负责了一块 GPU Buffer 的分配,在 CAW 上它实际分配的是 Android GraphicBuffer,GraphicBuffer 可以 map/unmap 到应用进程空间供 CPU 访问,也可以通过 EGLImage 跟一个 GL Texture 绑定供 GPU 访问,简而言之,GraphicBuffer 提供了一块可供 CPU/GPU 共同访问的内存,它是由 Android 的 ION 内存分配器分配出来的。
使用 GraphicBuffer 可以避免在光栅化前先分配临时的位图,在光栅化结束后再将位图上传成纹理的这种做法,实现了 zero copy texture upload,不过 ION 实际上是由芯片厂商提供的驱动,在早期的 Android 版本上兼容性问题不少,另外 map/unmap 的时间开销在不同芯片上也不尽相同,所以使用 GraphicBuffer 是否能够带来性能提升还要取决于芯片和驱动,在 Nexus 4 上看到的结果还不错,map/unmap的开销基本在 0.1 毫秒左右。
Resource 对象在分配一个 GraphicBuffer 的时候,都会同时创建一个关联的 GLImageEGL 对象,GLImageEGL 会使用 GraphicBuffer 的 Native Handler 创建一个 EGLImageKHR,这个 EGLImageKHR 在合成时会绑定到一个 GL Texture ID,从而将 GraphicBuffer 跟一个 GL Texture 绑定在一起,作为这个 Texture 的 buffer。
ImageRasterWorkerPool 的基类 WorkerPool 内部会创建一个线程池,Tile 的光栅化是由线程池里面的光栅化线程负责执行的,不过在当前的 CAW 实现上,只使用了一个光栅化线程,并且它的优先级被降低到 kThreadPriority_Background 的优先级。
光栅化线程除了光栅化任务外,还负责位图的解码,如果这块 Tile 上面有位图需要先解码,那解码任务会作为光栅化任务的前置任务先被执行。至于解码任务为什么会独立出来作为光栅化任务的前置任务执行,而不是直接在光栅化过程中解码,官方文档并没有说明,不过跟其它同事讨论后,大致猜测的原因是为了更好地支持多个光栅化线程并发运行,因为目前 Skia 的解码器是不支持多线程的,把解码的部分单独抽出来,就可以让多个 Tile 的光栅化任务分别在多个线程真正并发运行变成了可能(不需要加锁)。
WebView 绘制流程
上图显示了一个 WebView Draw Path 的调用流程图,跟旧的 Android WebView 一样,CAW 也是利用了 Android 的 GLFunctor 机制,在 WebView.onDraw 被 Android 调用生成 DisplayList 的时候,CAW 将一个 GLFunctor 置入 DisplayList 中,然后这个 GLFunctor 会在 Android drawDisplayList 的时候被回调。
合成相关的主要对象
上图展示了跟合成相关的一些主要对象,其中黄色标识为 Chromium 在 Android WebView 上的适配对象,蓝色标识为 Content 模块对象,绿色标识是 cc 模块对象,红色标识是 GPU 相关的功能类和需要由外部适配的抽象接口。
InProcessViewRenderer 对象封装了一个 WebView 跟渲染相关的部分,它是 WebView draw path 的入口,另外还处理例如 WebView 可见/不可见变化,attach/detach Window,大小变化,scroll position 变化等。InProcessViewRenderer 提供了 AwGLSurface 对象,代表当前窗口的 Draw Surface,当然,对于 CAW 来说,WebView 所在窗口的 Surface Swap 是由 Android 本身控制的。
SynchronousCompositor 和 SynchronousCompositorOutputSurface 分别代表了合成器和合成器的 OutputSurface,其中 OutputSurface 需要为绘图指令的执行提供一个绘图上下文,这个就是 GLInProcessContext,它实际是一个 Command Buffer 的封装,也就是说通过它执行的绘图指令会先缓存到内部的 Command Buffer 里面,然后再一次性输出。
cc 模块的对象为实际绘制提供了内容,要绘制的内容首先由一棵 Layer 树表示,Layer 树上的 Layer 和它的分块最终会转化为一个个 DrawQuad Command,然后 DrawQuad Command 再被 GLRenderer 转化为 GL Draw Call 通过 OutputSurface 提供的绘图上下文输出。
整个合成的过程大概可以分成三步:
- 首先 LayerTreeHostImpl::CalculateRenderPasses 遍历要合成的 Layer 在可见区域的 Tile,生成 RenderPass,里面包含根据 Tile 生成的 DrawQuad Commands(PictureLayerImpl::AppendQuads),CAW 会根据不同的渲染模式产生不同的 DrawQuad Command,包括硬件渲染模式,软件渲染模式(使用位图缓存),软件渲染模式(不使用位图缓存);
- 然后,如果是硬件渲染模式,LayerTreeHostImpl::DrawLayers 会使用 GLRenderer 将 RenderPass 里面的 Tile DrawQuad Commands 转换成 GL Draw Calls 放入 GPU Command Buffer 里面。如果是软件渲染模式,则使用 SoftwareRenderer 直接绘制 DrawQuad;
- 最后 GLRenderer::SwapBuffers 的时候,就会 flush 这个 Command Buffer,真正执行 GL Draw Calls。
Tile DrawQuad Command 被转换成 GL Draw Call 时,它相关联的 Resource 此时需要分配一个 GL Texture ID(如果已经分配可以忽略),然后在这个 GL Draw Call 真正执行前,相关联的 EGLImageKHR 需要跟这个 Texture ID 进行绑定(如果已经绑定可以忽略),然后我们就可以使用这个 GL Texture 进行贴图操作了。
Scheuler 本质上是一个状态机,它根据内部的状态纪录决定 cc 下一个要执行动作,它基本的运作方式是 -
- 接收到一个外部调用或者通知,比如 Scheduler::SetNeedsCommit;
- 改变内部状态机对象的某些状态,比如 SchedulerStateMachine::SetNeedsCommit;
- 调用 Scheduler::ProcessScheduledActions 进行任务调度,Scheuler 根据状态机当前的状态集,决定下一个要执行的动作是什么,然后根据该动作再次更新状态机的当前状态,并调度或执行相应的任务;
- 上面的步骤会一直循环直到没有下一个要执行的动作为止,如果已经没有要执行的动作,则看是否需要请求下一个 BeginImplFrame;
void Scheduler::ProcessScheduledActions() {
SchedulerStateMachine::Action action;
do {
action = state_machine_.NextAction();
state_machine_.UpdateState(action);
switch (action) {
case SchedulerStateMachine::ACTION_NONE:
break;
case SchedulerStateMachine::ACTION_SEND_BEGIN_MAIN_FRAME:
client_->ScheduledActionSendBeginMainFrame();
break;
case SchedulerStateMachine::ACTION_COMMIT:
client_->ScheduledActionCommit();
break;
...
}
} while (action != SchedulerStateMachine::ACTION_NONE);
SetupNextBeginImplFrameIfNeeded();
...
}
Scheduler 可能的动作包括 -
enum Action {
ACTION_NONE,
ACTION_SEND_BEGIN_MAIN_FRAME,
ACTION_COMMIT,
ACTION_UPDATE_VISIBLE_TILES,
ACTION_ACTIVATE_PENDING_TREE,
ACTION_DRAW_AND_SWAP_IF_POSSIBLE,
ACTION_DRAW_AND_SWAP_FORCED,
ACTION_DRAW_AND_SWAP_ABORT,
ACTION_DRAW_AND_READBACK,
ACTION_BEGIN_OUTPUT_SURFACE_CREATION,
ACTION_ACQUIRE_LAYER_TEXTURES_FOR_MAIN_THREAD,
ACTION_MANAGE_TILES,
};
因为 CAW 使用的是同步合成器,Scheduler 的调度逻辑在 CAW 上相对简单一些,比如说它如果接收到 BeginImplFrame 的通知,DrawAndSwap 的动作就一定会被调用(ACTION_DRAW_AND_SWAP_IF_POSSIBLE),真正需要的动作也只是上面其中的一部分,下面列出一些比较重要的动作的调度说明。
Commit 调用流程图
ACTION_SEND_BEGIN_MAIN_FRAME 和 ACTION_COMMIT
SendBeginMainFrame 这个动作的指通知 WebKit 线程开始 BeginMainFrame,它包括重排版,更新 cc::Layer 树的结构,内容和属性,然后等待 Compositor 线程执行 Commit。这个动作是由 WebKit 线程向 Compositor 线程发送了一个 NeedsCommit 的通知所引起,它告诉 Compositor 线程,网页的内容发生了变化,或者是由于在加载过程中网页不断进行排版,或者是由于网页加载完成后,JavaScript 又改变了 DOM 树结构或者元素的 CSS 属性。
WebKit 线程在 BeginMainFrame 的最后会向 Compositor 请求 Commit 并阻塞自己,它最终会触发 Scheduler 在 Compositor 线程进行执行 Commit 动作。
但是 Scheduler 接收到一个 NeedsCommit 请求时,并不会马上发送 BeginMainFrame 消息给 WebKit 线程,它会尽量让 BeginMainFrame 和 BeginImplFrame 保持同步,避免不必要的内核更新,所以一般来说它会先通过 Invalidate WebView 来请求下一次的 BeginImplFrame,然后在 BeginImplFrame 接近结束后再发送 BeginMainFrame,并且在已经有 pending 的 LayerTreeImpl 的时候,SendBeginMainFrame 会被一直延迟到 ActivatePendingTree 为止。
整个 Commit 的流程如下图:
ACTION_ACTIVATE_PENDING_TREE
所谓 ActivatePendingTree 就是 Pending 的 LayerTreeImpl 树在可见区域内的分块都已经完成光栅化和纹理上传,然后 Scheduler 就会请求 ActivatePendingTree,将 Pending 的 LayerTreeImpl 树取代当前 Active 的 LayerTreeImpl 树成为新的 Active LayerTreeImpl。
ACTION_DRAW_AND_SWAP_IF_POSSIBLE
DrawAndSwap 就是为 WebView 绘制新的一帧,因为在 CAW 里面,Compositor 线程跟 UI 线程是同一个线程的关系,它使用的是同步合成器,所以当 WebView 的 DrawGL 被 Android 在 drawDisplayList 的过程中调用到时,Scheduler 会接收到 BeginImplFrame 的通知,它会马上去调用 OnBeginImplFrameDeadLine,设置 BEGIN_IMPL_FRAME_STATE_INSIDE_DEADLINE 的状态,这样保证 DrawAndSwap 马上被 ProcessScheduledActions 调用到。
void Scheduler::OnBeginImplFrameDeadline() {
begin_impl_frame_deadline_closure_.Cancel();
state_machine_.OnBeginImplFrameDeadline();
ProcessScheduledActions();
...
}
ACTION_MANAGE_TILES
一般是因为 Commit 完成后,Compositor 有了一棵新的 LayerTreeImpl 树,新的分块加入 TileManager,旧的分块被移除;或者因为网页滚动或者其它原因导致分块的优先级发生变化,需要重新更新 TileManager 里面的每个 Tile 的优先级,重新进行分组,并安排光栅化任务。
Resource 的创建和使用
Resources 相关的主要对象
上图显示了 Resources 相关的主要对象,黄色标识的为 cc::Resources 模块,红色标识为 GPU 模块。TileManager 在分派 Tile 的光栅化任务前,需要通过 ResourcePool 为 Tile 分配一个 Resource,如果 ResourcePool 没有可用的 Resource,它需要通过 ResourceProvider 创建。Tile 关联的 Resource 实际上只是一个 resource id,它用于映射到 ResourceProvider 内部管理的 ResourceProvider::Resource 对象。
gfx::GpuMemoryBuffer* GpuControlService::CreateGpuMemoryBuffer(
size_t width,
size_t height,
unsigned internalformat,
int32* id) {
linked_ptr<gfx::GpuMemoryBuffer> buffer = make_linked_ptr(
gpu_memory_buffer_factory_->CreateGpuMemoryBuffer(width,
height,
internalformat));
static int32 next_id = 1;
*id = next_id++;
if (!RegisterGpuMemoryBuffer(*id,
buffer->GetHandle(),
width,
height,
internalformat)) {
return NULL;
}
}
当真正光栅化前,我们需要为 Resource 分配一块缓冲区,在 CAW 上这块缓冲区的类型是 GpuMemoryBuffer[Impl],它实际上是一块 Android GraphicBuffer 的封装,GpuMemoryBuffer 由 GpuControlService 创建和管理,它会为每个 GpuMemoryBuffer 分配一个 image id,ResourceProvider::Resource 通过获得的 image id 来映射到它的 GpuMemoryBuffer。GpuMemoryBuffer 分配后,通过它的 MapImage 操作就可以获得一个可以被 CPU 访问的用户进程空间地址,使用它创建一个 SkBitmap 就可以通过 Skia 使用 CPU 来执行光栅化操作。
bool GLImageEGL::Initialize(gfx::GpuMemoryBufferHandle buffer) {
DCHECK(buffer.native_buffer);
EGLint attrs[] = {
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
EGL_NONE,
};
egl_image_ = eglCreateImageKHR(
GLSurfaceEGL::GetHardwareDisplay(),
EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID,
buffer.native_buffer,
attrs);
return true;
}
GpuControlService 创建 GpuMemoryBuffer 后需要通过接口 GpuMemoryBufferManagerInterface 进行注册,GpuMemoryBufferManagerInterface 的实现类 ImageManager 需要为 GpuMemoryBuffer 分配一个关联的 GLImage 对象,它的 ID 就是 GpuMemoryBuffer 的 image id,GLImage 在 CAW 上的具体实现是 GLImageEGL,GLImageEGL 实际上是 EGLImageKHR 的封装,它在初始化时使用 GpuMemoryBuffer 的 Native Buffer 句柄创建了 EGLImageKHR。
void GLImageEGL::WillUseTexImage() {
in_use_ = true;
glEGLImageTargetTexture2DOES(target_, egl_image_);
}
GLImage 的作用是将 GpuMemoryBuffer 跟一个 GL Texture 绑定,让 GpuMemoryBuffer 作为 Texture 的纹理缓存,从而可以被 GPU 通过贴图操作访问。所以当我们真正试图使用 GL 去绘制 Tile 的时候,我们需要为 Tile 关联的 Resource 分配一个 Texture ID,并将这个 Texture 跟 Resource 关联的 GLImage 进行绑定,从而将跟 GLImage 相关联的 GpuMemoryBuffer 作为 Texture 的纹理缓存。
TileManager and ResourcePool
enum PriorityCutoff {
// Allow allocations that are not strictly needed for correct rendering, but
// are nice to have for performance. For compositors, this includes textures
// that are a few screens away from being visible.
CUTOFF_ALLOW_NICE_TO_HAVE,
};
enum TileMemoryLimitPolicy {
// You're being interacted with, but we're low on memory.
ALLOW_PREPAINT_ONLY = 2, // Grande.
};
// Tile manager classifying tiles into a few basic bins:
enum ManagedTileBin {
NOW_AND_READY_TO_DRAW_BIN = 0, // Ready to draw and within viewport.
NOW_BIN = 1, // Needed ASAP.
SOON_BIN = 2, // Impl-side version of prepainting.
EVENTUALLY_AND_ACTIVE_BIN = 3, // Nice to have, and has a task or resource.
EVENTUALLY_BIN = 4, // Nice to have, if we've got memory and time.
AT_LAST_AND_ACTIVE_BIN = 5, // Only do this after all other bins.
AT_LAST_BIN = 6, // Only do this after all other bins.
NEVER_BIN = 7, // Dont bother.
NUM_BINS = 8
// NOTE: Be sure to update ManagedTileBinAsValue and kBinPolicyMap when adding
// or reordering fields.
};
CAW 使用的 memory policy 是 gpu::MemoryAllocation::CUTOFF_ALLOW_NICE_TO_HAVE,它对应 TileManager 的 TileMemoryLimitPolicy 是 ALLOW_PREPAINT_ONLY。TileManager 在每次 ManageTiles 时,会根据需要重新计算 Tile 的优先级,并根据优先级将 Tile 归类到不同的 BIN,如果 Tile BIN 为 NEVER_BIN 时则释放 Tile 的 Resource。
// Determine bin based on three categories of tiles: things we need now,
// things we need soon, and eventually.
inline ManagedTileBin BinFromTilePriority(const TilePriority& prio) {
// The amount of time/pixels for which we want to have prepainting coverage.
// Note: All very arbitrary constants: metric-based tuning is welcome!
const float kPrepaintingWindowTimeSeconds = 1.0f;
const float kBackflingGuardDistancePixels = 314.0f;
// Note: The max distances here assume that SOON_BIN will never help overcome
// raster being too slow (only caching in advance will do that), so we just
// need enough padding to handle some latency and per-tile variability.
const float kMaxPrepaintingDistancePixelsHighRes = 2000.0f;
const float kMaxPrepaintingDistancePixelsLowRes = 4000.0f;
if (prio.distance_to_visible_in_pixels ==
std::numeric_limits<float>::infinity())
return NEVER_BIN;
if (prio.time_to_visible_in_seconds == 0)
return NOW_BIN;
if (prio.resolution == NON_IDEAL_RESOLUTION)
return EVENTUALLY_BIN;
float max_prepainting_distance_pixels =
(prio.resolution == HIGH_RESOLUTION)
? kMaxPrepaintingDistancePixelsHighRes
: kMaxPrepaintingDistancePixelsLowRes;
// Soon bin if we are within backfling-guard, or under both the time window
// and the max distance window.
if (prio.distance_to_visible_in_pixels < kBackflingGuardDistancePixels ||
(prio.time_to_visible_in_seconds < kPrepaintingWindowTimeSeconds &&
prio.distance_to_visible_in_pixels <= max_prepainting_distance_pixels))
return SOON_BIN;
return EVENTUALLY_BIN;
}
Tile 初始 BIN 的计算是由函数 BinFromTilePriority 根据优先级决定的,其中虽然现在不可见,但是马上就变成可见的类别(SOON_BIN),是由下面两个因素决定的:
- Tile 离可见区域的距离在 314 像素以内。
- Tile 离可见区域的距离在 4000 像素以内(低分辨率的 Tile 距离 2000 像素),并且按照当前的惯性滑动速度预估 1 秒后就会进入显示区域。
ALLOW_PREPAINT_ONLY 的 TileMemoryLimitPolicy 会让 TileManager 只保留类别为 NOW_AND_READY_TO_DRAW_BIN,NOW_BIN 和 SOON_BIN 的 Tile 的 Resource,其它 Tile 都被归类到 NEVER_BIN,它们的 Resource 会被释放。
ResourcePool 是 Rosource 的回收池,当 TileManager 释放 Tile 的 Resource 时,会把它放入 ResourcePool 里面。而当 TileManager 为一个新的 Tile 分配 Resource 时,ResourcePool 会试图先重用一个被回收的 Resource,如果没有可用的 Resource,它才去请求 ResourceProvider 创建一个新的。
在 CAW 上,对 ResourcePool 的限制是:
- 里面容纳的 Resource 个数不超过150个;
- 里面容纳的 Resource 总内存大小不超过 ViewWidth x ViewHeight x 4 x 10,并且以 5M 大小对齐,大概相当于屏幕像素的 10 倍左右;
如果超过以上限制,ResourcePool 就会销毁被回收的 Resource 直到满足限制为止,并且当一个 WebView 被应用从当前 Window 上移除时,ResourcePool 会销毁全部回收的 Resource。
Chromium 目前一共有三种不同的硬件加速合成器,分别介绍如下:
多进程架构下旧的合成器
Render 进程将网页合成到一个离屏的缓存(Texture),Browser 进程再将它合成到窗口帧缓存。主要的缺点是需要两次 Render Pass,额外的离屏缓存开销,渲染性能比较差,耗电严重;
多进程架构下新的合成器 —— Uber Compositor
Render 进程生成 CompositorFrame,它包含一系列 Draw Quad 命令,通过 DelegatingRenderer 传给 Browser 进程,由 Browser 进程执行命令完成合成。
比起上面的合成器,只需要一个 Render Pass,不需要额外的离屏缓存,渲染性能有所提升(特别是在内存带宽受限的设备上),耗电减少;缺点是仍然需要将 Draw Quad 命令序列化/反序列化跨进程传递(Texture 在进程间共享)。
Uber Compositor 目前使用开关 “ --enable-delegated-renderer” 开启。
单进程架构下 Android WebView 使用的同步合成器 —— Synchronous Compositor
Browser 进程生成 Draw Quad 命令后直接执行,绘制到当前的窗口帧缓,不需要任何跨进程通讯的开销,理论上性能是最好的。另外,因为 Uber Compositor 和 Synchronous Compositor 使用了 Draw Quad 命令的方式,所以这两者是能够做到同时支持软件合成。
从 M33 开始,Android WebView 的合成器就一直在为 Android 5.0 做适配和优化,从 M37 开始引入了 Uber Compositor 的架构,更好地适配 Android 5.0 多线程的渲染架构。这一章的内容根据最新 M39 的代码进行撰写,描述 Android WebView 当前合成器的架构。
Android 5.0 的渲染架构
无论是 2.x 时代的纯 CPU 绘制(Skia),还是从 3.x 开始引入 GPU 加速绘制(GLRenderer),Android 一直都是单线程的渲染架构。不过从 5.0 开始,Android 会演变成多线程的渲染方式,更好地支持 CPU/GPU 并发,实现更流畅的动画效果。
在单线程硬件加速的渲染模式下,Android 的 UI 线程本身就是 GPU 线程,系统会先遍历 View Hierachy 更新每个 View 的 DisplayList,然后再调用 GLRenderer 执行 DisplayList,使用 GPU 绘制 View 的内容。对 WebView 来说,更新 DisplayList 时是 onDraw 被调用,执行 DiaplayList 时是 DrawGL 被调用。
多线程硬件加速的渲染模式下,UI 线程不再是 GPU 线程,Android 会创建另外一个子线程 Render 线程,而 Render 线程才是 GPU 线程(这个 Render 线程跟 Chromium 的 Render 线程没有关系)。Android 系统会在 UI 线程更新 DisplayList,在 Render 线程执行 DisplayList,并且两者很有可能是异步的,也就是说当 Render 线程执行第 N 帧的 DisplayList 的时候,UI 线程可以去执行其它任务比如处理 Touch 事件,甚至可以生成第 N + 1 帧的 DisplayList(从目前的 L Preview 来看,Render 线程和 UI 线程是同步的,不过这很有可能只是一个临时的处理方式,最终的发布版本应该是异步的)。在这种渲染模式下,WebView 的 onDraw 会在 UI 线程被调用,而 DrawGL 会在 Render 线程被调用,这两个方法需要可以并发运行,并且 WebView 只能在 DrawGL 里面调用 GL API 使用 GPU。
Uber Compositor for WebView
M39 合成相关的主要对象
上图展示了 M39 代码里面合成相关的一些主要对象,其中黄色标识为 Chromium 在 Android WebView 上的适配对象,蓝色标识为 Content 模块对象,绿色标识是 cc 模块对象,红色标识是 GPU 相关的功能类和需要由外部适配的抽象接口。
WebView Compositor 为了对 L 的多线程渲染架构做适配和优化,引入了 Uber Compositor 的架构,从上图我们可以看出新的合成器的架构比起 M33 来说变得复杂了许多。在 Uber Compositor 的架构下,UI 线程拥有的是所谓的 Child Compositor,它的主要任务是在 WebView.onDraw 的调用中使用 cc::Layer[Impl] 树生成一个 CompositorFrame,而 Render 线程拥有的是所谓的 Parent Compositor,它的主要任务是在 WebView.DrawGL 的调用中使用 CompositorFrame 生成 GLFrame,调用 GL API 进行真正的绘制,Child/Parent Compositor 之间通过 SharedRendererState 传递和共享数据,而 AwContents 是两者的调度中枢。
WebView.onDraw, and WebView.DrawGL
更详细的说明如下:
Child Compositor 包括的主要对象是 BrowserViewRenderer,SynchronousCompositorImpl 和 SynchronousCompositorOutputSurface,整体架构和运行机制跟 M33 时差不多,最大的区别在于 Child Compositor 所关联的 cc::Layer[Impl] 树的管理对象 LayerTreeHostImpl 创建的不是之前的 GLRenderer(DirectRenderer 的子类),而是 DelegatingRenderer,如前所述 GLRenderer 会直接执行生成的 RenderPass,生成 GLFrame 进行真正的绘制,而 DelegatingRenderer 则不然,如其名所示,DelegatingRenderer 会将 RenderPass 包装成一个 CompositorFrame,并将其传递给 SynchronousCompositorOutputSurface ,而 BrowserViewRenderer 最后会将 CompositorFrame 取出来交由 SharedRendererState 持有;
Parent Compositor 包括的主要对象是 HardwareRenderer 和 ParentOutputSurface,它也会持有自行创建的 cc::Layer[Impl] 树和相关的管理对象 LayerTreeHost[Impl],不过跟 Child Compositor 不一样,它们跟 Blink 里面的 RenderLayer/GraphicsLayer 并无关系,只是用来包纳一个 Root Layer —— DelegatedRenderLayer,仅仅起到一个 Wrapper 的作用,而 Parent Compositor 的 LayerTreeHostImpl 创建的 Renderer 是 GLRenderer。在 WebView.DrawGL 调用 时,Parent Compositor 从 SharedRendererState 取出 CompositorFrame 数据,交由 DelegatedRenderLayer 持有,然后进行合成,最终由 LayerTreeHostImpl 的 GLRenderer 使用 CompositorFrame 生成 GLFrame 进行真正的绘制;
因为 Child/Parent Compositor 只在 SharedRendererState 中共享一帧的数据,所以 WebView.OnDraw 和 DrawGL 之间实际上并不能真正完全并发地运行。从当前的 WebView Compositor 的代码可以逆推出 Android 5.0 发布版本的一些可能的渲染机制,Android 5.0 应该会在 UI 线程生成 DisplayList 后,立即向 Render 线程发出一个 sync mode 的 DrawGL 请求,如其名所示,这个请求是同步的,也就是说 UI 线程会被阻塞直到 Render 线程处理完毕为止;接着 UI 线程再真正请求执行 DisplayList (draw mode DrawGL),这个请求就是异步的了,UI 线程不再阻塞。从实际的代码上看,Parent Compositor 会在 sync mode 的 DrawGL 调用中从 SharedRendererState 取走 CompositorFrame 数据交由 HardwareRenderer 持有,然后直接返回,不进行真正的绘制;然后在真正的 draw mode 的 DrawGL 调用时才通过 HardwareRenderer 使用它当前持有的 CompositorFrame 数据进行真正的绘制。所以 WebView.onDraw 和 DrawGL 之间最多余留了一帧的异步处理空间,也就是说最多允许绘制第 N 帧时同时生成第 N + 1 帧,如果第 N + 1 帧生成完毕后,第 N 帧还没有绘制完毕,Android 5.0 则会强制进行同步,这种机制避免了生产者生产速度太快,消费者来不及处理,导致的跳帧现象;大概的流程如下图所示:
Chromium 从 M37 开始在 Android 上逐步推进 GPU 光栅化的使用,目前是否在 Android 5.0 WebView 上开启还没有确定,不过当前主干 M39 上的 WebView 默认是开启的。
在当前 CPU 光栅化,纹理上传,GPU 合成的渲染架构下,一些不会改变图层内容的 CSS/JS 动画,比如 Transform 和 Opacity,效率是足够高的,比较容易达到 60fps。但是对于一些会改变图层内容的 CSS/JS 动画,因为每一帧都需要重新光栅化的原因,效率反而更低了,难以达到 60fps。而使用 GPU 光栅化(via Skia Ganesh),理论上在这些方面会有较大的优势,包括性能和耗电。
目前 Chromium 在 GPU 光栅化的进度上仍然处于第一阶段,大致状况如下:
总的来说,Chromium 仍然在不断地改进 GPU 光栅化的性能,使之能够覆盖更多不同类型的网页。不过不太确定的是,未来是否能够覆盖更多的机型,包括一些系统版本较老或者硬件配置较低的设备。
Slimming Paint (a.k.a. Redesigning Painting and Compositing) 看起来是一项非常具有野心的项目,它试图对 Apple 引入 WebKit 的图层合成加速的渲染架构进行重新设计,再结合其它改进比如 GPU Rasterization,来大幅度提升网页 JS/CSS(非加速)动画的性能,并且让更多的 CSS 动画变成是可加速的(单独由合成器驱动)。
目前 WebKit or Blink 的图层合成加速渲染架构,JS/CSS(非加速)动画的每一帧都需要经过一个漫长的处理过程,包括内核的 BeginMainFrame 输出一棵包括 PicturePile 作为 Recording Content 的 cc::Layer 树,而合成器需要将新的 cc::Layer 树变更的部分重新光栅化,然后在 BeginImplFrame 中上传纹理,commit 新的 cc::Layer 树,输出 CompositorFrame,最后调用 GLRenderer 完成最后的合成,如果是多线程级联合成器的架构,这个过程就更加复杂了。
上图展示了一个 JS Animation 的 Blink BeginMainFrame 可能的处理流程,其中包括了 JS,DOM,CSS Style,Layout,Compositor update layers 和 Layer content re-recording,整个流程步骤很多,耗时很长,而 Slimming Painting 项目的目标就是:
总的来说就是简化 Blink 的处理,将更多的工作转移到合成器,让 Blink 和合成器之间结合的更紧密,合成器可以接受来自 Blink 更小单位的增量更新,并且相信合成器具备更多的上下文知识来给出最佳化的处理方式。
Slimming Painting 的整体设计
对当前代码可能的影响:
Chromium Android WebView
优势:
劣势:
U3 WebView
优势:
劣势:
根据以上特性分析大概可以推理出在不同设备和不同网页上,两者理论上的性能表现(真实状况需要根据具体的设备和网页特性进行实际的性能分析):
Chromium 里面负责历史页面截图的对象是 NavigationEntryScreenshotManager,它由 NavigationControllerImpl 创建并持有,后者由 WebContentsImpl 创建并持有。
截图的时机和使用方式跟我们目前的做法有些类似,就是在网页跳转到一个新的链接时,调用 NavigationEntryScreenshotManager::TakeScreenshot 方法(需要开启 overscroll navigation gesture 的支持),生成当前页面当前可见区域的截图,然后把数据放入 NavigationEntryImpl 对象里面。当后退时,就可以从后退页面对应的 NavigationEntryImpl 对象里面取出截图用于显示。
void WebContentsImpl::DidNavigate(
RenderViewHost* rvh,
const ViewHostMsg_FrameNavigate_Params& params) {
...
if (PageTransitionIsMainFrame(params.transition)) {
// When overscroll navigation gesture is enabled, a screenshot of the page
// in its current state is taken so that it can be used during the
// nav-gesture. It is necessary to take the screenshot here, before calling
// RenderFrameHostManager::DidNavigateMainFrame, because that can change
// WebContents::GetRenderViewHost to return the new host, instead of the one
// that may have just been swapped out.
if (delegate_ && delegate_->CanOverscrollContent())
controller_.TakeScreenshot();
GetRenderManager()->DidNavigateMainFrame(rvh);
}
...
}
void NavigationEntryScreenshotManager::OnScreenshotEncodeComplete(
int unique_id,
scoped_refptr<ScreenshotData> screenshot) {
NavigationEntryImpl* entry = NULL;
int entry_count = owner_->GetEntryCount();
for (int i = 0; i < entry_count; ++i) {
NavigationEntry* iter = owner_->GetEntryAtIndex(i);
if (iter->GetUniqueID() == unique_id) {
entry = NavigationEntryImpl::FromNavigationEntry(iter);
break;
}
}
if (!entry)
return;
entry->SetScreenshotPNGData(screenshot->data());
OnScreenshotSet(entry);
}
生成截图的方式一样是走 Compositor 的软件绘制路径,不过具体实现方式有些还是不太一样,包括:
void RenderWidgetHostViewAndroid::SynchronousCopyContents(
const gfx::Rect& src_subrect_in_pixel,
const gfx::Size& dst_size_in_pixel,
const base::Callback<void(bool, const SkBitmap&)>& callback) {
SynchronousCompositor* compositor =
SynchronousCompositorImpl::FromID(host_->GetProcess()->GetID(),
host_->GetRoutingID());
if (!compositor) {
callback.Run(false, SkBitmap());
return;
}
SkBitmap bitmap;
bitmap.setConfig(SkBitmap::kARGB_8888_Config,
dst_size_in_pixel.width(),
dst_size_in_pixel.height());
bitmap.allocPixels();
SkCanvas canvas(bitmap);
canvas.scale(
(float)dst_size_in_pixel.width() / (float)src_subrect_in_pixel.width(),
(float)dst_size_in_pixel.height() / (float)src_subrect_in_pixel.height());
compositor->DemandDrawSw(&canvas);
callback.Run(true, bitmap);
}
- Threading
- GPU Architecture Roadmap
- How Chromium Displays Web Pages
- Multi-process Architecture
- Compositing in Blink / WebCore: From WebCore::RenderLayer to cc:Layer
- Compositor Thread Architecture
- Impl-side Painting
- Synchronous compositing for WebView
- GPU Command Buffer
- Organization of code for Android WebView
- Scheduler improve
- Ubercomp in Android WebView
- Blink/Chrome GPU Rasterization: Phase 1
- GPU Accelerated Rasterization Phase 2