Android WebView简要介绍和学习计划

千金一刻莫空度,老大无成空自伤。这篇文章主要讲述Android WebView简要介绍和学习计划相关的知识,希望能为你提供帮助。
我们通常会在App的UI中嵌入WebView, 用来实现某些功能的动态更新。在4.4版本之前, android WebView基于WebKit实现。不过, 在4.4版本之后, Android WebView就换成基于Chromium的实现了。基于Chromium实现, 使得WebView可以更快更流畅地显示网页。本文接下来就介绍Android WebView基于Chromium的实现原理, 以及制定学习计划。
老罗的新浪微博: http://weibo.com/shengyangluo, 欢迎关注!

《Android系统源代码情景分析》一书正在进击的程序员网( http://0xcc0xcd.com) 中连载, 点击进入!

通过前面几个系列文章的学习, 我们知道, Chromium的实现是相当复杂的。这种复杂可以体现在编译出来的动态库的大小上, 带符号版本1.3G左右, 去掉符号后还有27M左右。编译过AOSP源码的都知道, 在整个编译过程中, Chromium库占用了大部分的时间, 尤其是在生成上述1.3G文件时, 电脑几乎卡住了。因此, 在使用Chromium实现WebView时, 第一个要解决的问题就是它的动态库加载问题。
Android系统的动态库是ELF文件结构。ELF文件由ELF Header、Program Header Table、Section以及Section Header Table组成。ELF Header位于文件的开头, 它同时描述了Program Header Table和Section Header Table在文件中的偏移位置。Section Header Table描述了动态库由哪些Section组成, 典型的Section有.text、.data、.bss和.rodata等。这些Section描述的要么是程序代码, 要么是程序数据。Program Header Table描述了动态库由哪些Segment组成。一个Segment又是由一个或者若干个Section组成的。Section信息是在程序链接期间用到的, 而Segment信息是在程序加载期间用到的。关于ELF文件格式的更详细描述, 可以参考Executable and Linkable Format。
对于动态库来说, 它的程序代码是只读的。这意味着一个动态库不管被多少个进程加载, 它的程序代码都是只有一份。但是, 动态库的程序数据, 链接器在加载的时候, 会为每一个进程都独立加载一份。对于Chromium动态库来说, 它的程序代码占据了95%左右的大小, 也就是25.65M左右。剩下的5%, 也就是1.35M, 是程序数据。假设有N个App使用了WebView, 并且这个N个App的进程都存在系统中, 那么系统就需要为Chromium动态库分配( 25.65 + 1.35 × N) M内存。这意味着, N越大, Chromium动态库就占用越多的系统内存。
在Chromium动态库1.35M的程序数据中, 大概有1.28M在加载后经过一次重定位操作之后就不会发生变化。这些数据包含C+ + 虚函数表, 以及所有指针类型的常量。它们会被链接器放在一个称为GNU_RELRO Section中, 如图1所示:

Android WebView简要介绍和学习计划

文章图片


图1 App进程间不共享GNU_RELRO Section
如果我们将Chromium动态库的GNU_RELRO Section看成是普通的程序数据, 那么Android系统就需要为每一个使用了WebView的App进程都分配1.28M的内存。这将会造成内存浪费。如果我们能App进程之间共享Chromium动态库的GNU_RELRO Section, 那么不管有多少个App进程使用了WebView, 它占用的内存大小都是1.28M, 如图2所示:
Android WebView简要介绍和学习计划

文章图片


图2 App进程间共享GNU_RELRO Section
这是有可能做到的, 毕竟它与程序代码类似, 在运行期间都是只读的。不同的地方在于, 程序代码在加载后自始至终都不用修改, 而GNU_RELRO Section的内容在重定位期间, 是需要进行一次修改的, 之后才是只读的。但是修改的时候, Linux的COW( Copy On Write) 机制就会使得它不能再在各个App进程之间实现共享。
为了使得Chromium动态库能在不同的App进程之间共享, Android系统执行了以下操作:
1. Zygote进程在启动的过程中, 为Chromium动态库保留了一段足够加载它的虚拟地址内间。我们假设这个虚拟地址空间的起始地址为gReservedAddress, 大小为gReservedSize。
2. System进程在启动的过程中, 会请求Zygote进程fork一个子进程, 并且在上述保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)中加载Chromium动态库。Chromium动态库的Program Header指定了它的GNU_RELRO Section的加载位置。这个位置是相对基地址gReservedAddress的一个偏移量。我们假设这个偏移量为gRelroOffset。上述子进程完成Chromium动态库的GNU_RELRO Section的重定位操作之后, 会将它的内容写入到一个RELRO文件中去。
3. App进程在创建WebView的时候, Android系统也会在上述保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)中加载Chromium动态库, 并且会直接将第2步得到的RELRO文件内存映射到虚拟地址空间[gReservedAddress + gRelroOffset,gReservedAddress + gRelroOffset + 1.28)去。
关于Zygote进程、System进程和App进程的启动过程, 可以参考Android系统进程Zygote启动过程的源代码分析和Android应用程序进程启动过程的源代码分析这两篇文章。
Android 5.0的Linker提供了一个新的动态库加载函数android_dlopen_ext。这个函数不仅可以将一个动态库加载在指定的虚拟地址空间中, 还可以在该动态库重定位操作完成后, 将其GNU_RELRO Section的内容写入到指定的RELRO文件中去, 同时还可以在加载一个动态库时, 使用指定的RELRO文件内存映射为它的GNU_RELRO Section。因此, 上述的第2步和第3步可以实现。函数android_dlopen_ext还有另外一个强大的功能, 它可以从一个指定的文件描述符中读入要加载的动态库内容。通常我们是通过文件路径指定要加载的动态库, 有了函数android_dlopen_ext之后, 我们就不再受限于从本地文件加载动态库了。
接下来, 我们进一步分析为什么上述3个操作可以使得Chromium动态库能在不同的App进程之间共享。
首先, 第2步的子进程、第3步的App进程都是由第1步的Zygote进程fork出来的, 因此它们都具有一段保留的虚拟地址空间[gReservedAddress, gReservedAddress + gReservedSize)。
其次, 第2步的子进程和第3步的App进程, 都是将Chromium动态库加载在相同的虚拟地址空间中, 因此, 可以使用前者生成的RELRO文件内存映射为后者的GNU_RELRO Section。
第三, 所有使用了WebView的App进程, 都将相同的RELRO文件内存映射为自己加载的Chromium动态库的GNU_RELRO Section, 因此就实现了共享。
App进程加载了Chromium动态库之后, 就可以启动Chromium渲染引擎了。Chromium渲染引擎实际上是由Browser进程、Render进程和GPU进程组成的。其中, Browser进程负责将网页的UI合成在屏幕上, Render进程负责加载和渲染网页的UI, GPU进程负责执行Browser进程和Render进程发出的GPU命令。由于Chromium也支持单进程架构( 在这种情况下, 它的Browser进程、Render进程和GPU进程都是通过线程模拟的) , 因此接下来我们将它的Browser进程、Render进程和GPU进程统称为Browser端、Render端和GPU端。相应地, 启动Chromium渲染引擎, 就是要启动它的Browser端、Render端和GPU端。
对于Android WebView来说, 它启动Chromium渲染引擎的过程如图3所示:
Android WebView简要介绍和学习计划

文章图片


图3 Android WebView启动Chromium渲染引擎的过程
当我们在App的UI中嵌入一个WebView时, WebView内部会创建一个WebViewChromium对象。从名字就可以看出, WebViewChromium是基于Chromium实现的WebView。Chromium里面有一个android_webview模块。这个模块提供了两个类AwBrowserProcess和AwContents, 分别用来封装Chromium的Content层提供的两个类BrowserStartupController和ContentViewCore。
从前面Chromium硬件加速渲染的OpenGL上下文绘图表面创建过程分析一文可以知道, 通过Chromium的Content层提供的BrowserStartupController类, 可以启动Chromium的Browser端。不过, 对于Android WebView来说, 它并没有单独的Browser进程, 它的Browser端是通过App进程的主线程( 即UI线程) 实现的。
Chromium的Content层会通过BrowserStartupController类在App进程的UI线程中启动一个Browser Main Loop。以后需要请求Chromium的Browser端执行某一个操作时, 就向这个Browser Main Loop发送一个Task即可。这个Browser Main Loop最终会在App进程的UI线程中执行请求的Task。
从前面Chromium网页Frame Tree创建过程分析一文可以知道, 通过Chromium的Content层提供的ContentViewCore类, 可以创建一个Render进程, 并且在这个Render进程中加载指定的网页。不过, 对于Android WebView来说, 它是一个单进程架构, 也就是它没有单独的Render进程用来加载网页。这时候ContentViewCore类将会创建一个线程来模拟Render进程。也就是说, Android WebView的Render端是通过在App进程中创建的一个线程实现的。这个线程称为In-Process Renderer Thread。
现在, Android WebView具有Browser端和Render端了, 它还需要有一个GPU端。从前面Chromium的GPU进程启动过程分析一文可以知道, 在Android平台上, Chromium的GPU端是通过在Browser进程中创建一个GPU线程实现的。不过, 对于Android WebView来说, 它并没有一个单独的GPU线程。那么, Chromium的GPU命令由谁来执行呢?
我们知道, 从Android 3.0开始, App的UI就支持硬件加速渲染了, 也就是支持通过GPU来渲染。到了Android 4.0, App的UI默认就是硬件加速渲染了。这时候App的UI线程就是一个OpenGL线程, 也就是它可以用来执行GPU命令。再到Android 5.0, App的UI线程只负责收集UI渲染操作, 也就是构建一个Display List。保存在这个Display List中的操作, 最终会交给一个Render Thread执行。Render Thread又是通过GPU来执行这些渲染操作的。这时候App的UI线程不再是一个OpenGL线程, Render Thread才是。Android WebView的GPU命令就是由这个Render Thread执行的。当然, 在Android 4.4时, Android WebView的GPU命令还是由App的UI线程执行的。这里我们只讨论Android 5.0的情况, 具体可以参考Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章。
Chromium在android_webview模块中提供了一个DeferredGpuCommandService服务。当Android WebView的Render端需要通过GPU命令绘制网页UI时, 它就会通过DeferredGpuCommandService服务提供的RequestProcessGL接口向App的Display List增加一个类型为DrawFunctorOp的操作。当Display List从UI线程同步给Render Thread的时候, 它里面包含的DrawFunctorOp操作就会被执行。这时候Android WebView的Render端请求的GPU命令就会在App的Render Thread中得到执行了。
Android WebView的Browser端在合成网页UI时, 是运行在App的Render Thread中的。这时候它仍然是通过DeferredGpuCommandService服务提供的RequestProcessGL接口请求执行GPU命令。不过, DeferredGpuCommandService会检测到它当前运行在App的Render Thread中, 因此, 就会直接执行它请求的GPU命令。
Chromium为Android WebView独特的GPU命令执行方式( 既不是在单独的GPU进程中执行, 也不是在单独的GPU线程中执行) 提供了一个称为In-Process Command Buffer GL的接口。顾名思义, In-Process Command Buffer GL与Command Buffer GL接口一样, 要执行的GPU命令都是先写入到一个Command Buffer中。然而, 这个Command Buffer会提交给App的Render Thread进程执行, 过程如图4所示:
Android WebView简要介绍和学习计划

文章图片


图4 Android WebView通过In-Process Command Buffer GL接口执行GPU命令的过程
In-Process Command Buffer GL接口与前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文分析的Command Buffer GL接口一样, 都是通过GLES2Implementation类描述的, 区别在于前者通过一个InProcessCommandBuffer对象执行GPU命令, 而后者通过一个CommandBufferProxyImpl对象执行GPU命令。更具体来说, 就是CommandBufferProxyImpl对象会将要执行的GPU命令提交给Chromium的GPU进程/线程处理, 而InProcessCommandBuffer对象会将要执行的GPU命令提交给App的Render Thread处理。
GLES2Implementation类是通过InProcessCommandBuffer类的成员函数Flush请求它处理已经写入在Command Buffer中的GPU命令的。InProcessCommandBuffer类的成员函数Flush又会请求前面提到的DeferredGpuCommandService服务调度一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread, 意思就是说要在App的Render Thread线程中调用InProcessCommandBuffer类的成员函数FlushOnGpuThread。InProcessCommandBuffer类的成员函数FlushOnGpuThread在调用的过程中, 就会执行已经写入在Command Buffer中的GPU命令。
DeferredGpuCommandService服务接收到调度Task的请求之后, 会判断当前线程是否允许执行GL操作, 实际上就是判断当前线程是否就是App的Render Thread。如果是的话, 那么就会直接调用请求调度的Task绑定的InProcessCommandBuffer类的成员函数FlushOnGpuThread。这种情况发生在Browser端合成网页UI的过程中。Browser端合成网页UI的操作是由App的Render Thread主动发起的, 因此这个合成操作就可以在当前线程中直接执行。
另一方面, DeferredGpuCommandService服务接收到调度Task的请求之后, 如果发现当前不允许执行GL操作, 那么它就会将该Task保存在内部一个Task队列中, 并且通过调用java层的DrawGLFunctor类的成员函数requestDrawGL请求App的UI线程调度执行一个GL操作。这种情况发生在两个场景中。
第一个场景发生在前面描述的Render端绘制网页UI的过程中。绘制网页UI相当于就是绘制Android WebView的UI。Android WebView属于App的UI里面一部分, 因此它的绘制操作是由App的UI线程发起的。Android WebView在App的UI线程中接收到绘制的请求之后, 它就会要求Render端的Compositor线程绘制网页的UI。这个Compositor线程不是一个OpenGL线程, 它不能直接执行GL的操作, 因此就要请求App的UI线程调度执行GL操作。
从前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章可以知道, App的UI线程主要是负责收集当前UI的渲染操作, 并且通过一个DisplayListRenderer将这些渲染操作写入到一个Display List中去。这个Display List最终会同步到App的Render Thread中去。App的Render Thread获得了这个Display List之后, 就会通过调用相应的OpenGL函数执行这些渲染操作, 也就是通过GPU来执行这些渲染操作。这个过程称为Replay( 重放) Display List。重放完成之后, 就可以生成App的UI了。
DrawGLFunctor类的成员函数requestDrawGL请求App的UI线程调度执行的GL操作是一个特殊的渲染操作, 它对应的是一个GL函数, 而一般的渲染操作是指绘制一个Circle、Rect或者Bitmap等基本操作。GL函数在执行期间, 可以执行任意的GPU命令。这个GL操作封装在一个DrawGLFunctor对象。App的UI线程又会进一步将这个rawGLFunctor对象封装成一个DrawFunctionOp操作。这个DrawFunctionOp操作会通过DisplayListRenderer类的成员函数callDrawGLFunction写入到App UI的Display List中。当这个Display List被App的Render Thread重放时, 它里面包含的DrawFunctionOp操作就会被OpenGLRenderer类的成员函数callDrawGLFunction执行。
OpenGLRenderer类的成员函数callDrawGLFunction在执行DrawFunctionOp操作时, 就会通知与它关联的DrawGLFunctor对象。这个DrawGLFunctor对象又会调用Chromium的android_webview模块提供的一个GL函数。这个GL函数又会找到一个HardwareRenderer对象。这个HardwareRenderer对象是Android WebView的Browser端用来合成网页的UI的。有了这个HardwareRenderer对象之后, 上述GL函数就会调用它的成员函数DrawGL, 以便请求前面提到的DeferredGpuCommandService服务执行保存在它内部的Task, 这是通过调用它的成员函数PerformIdleTask实现的。
DeferredGpuCommandService类的成员函数PerformIdleTask会依次执行保存在内部Task队列的每一个Task。从前面的分析可以知道, 这时候Task队列中存在一个Task。这个Task绑定了InProcessCommandBuffer类的成员函数FlushOnGpuThread。因此, 这时候InProcessCommandBuffer类的成员函数FlushOnGpuThread就会在当前线程中被调用, 也就是在App的Render Thread中被调用。
前面提到, InProcessCommandBuffer类的成员函数FlushOnGpuThread在调用的过程中, 会执行之前通过In-Process Command Buffer GL接口写入在Command Buffer中的GPU命令。InProcessCommandBuffer类的成员函数FlushOnGpuThread又是通过内部的一个GpuScheduler对象和一个GLES2Decoder对象执行这些GPU命令的。其中, GpuScheduler对象负责将GPU命令调度在它们对应的OpenGL上下文中执行, 而GLES2Decoder对象负责将GPU命令翻译成OpenGL函数执行。关于GpuScheduler类和GLES2Decoder类执行GPU命令的过程, 可以参考前面前面Chromium硬件加速渲染的OpenGL命令执行过程分析一文。
第二个场景发生Render端光栅化网页UI的过程中。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道, Render端通过GPU光栅化网页UI的操作也是发生在Compositor线程中的。这时候它请求App的Render Thread执行GPU命令的大概流程相同, 也是将要执行的GPU操作封装在一个DrawGLFunctor对象中。不过, 这个DrawGLFunctor对象不会进一步被封装成DrawFunctionOp操作写入到App UI的Display List中, 而是直接被App的UI线程放入到App的Render Thread中, App的Render Thread再通知它执行所请求的GPU操作。DrawGLFunctor对象获得执行GPU操作的通知后, 后流的流程就与前面描述的第一个场景一致了。
通过以上描述的方式, Android WebView的Browser端和Render端就可以执行任意的GPU命令了。其中, Render端执行GPU命令是为了绘制网页UI, 而Browser端执行GPU命令是为了将Render端渲染出来的网页UI合成在App的UI中, 以便最后可以显示在屏幕中。这个过程就是Android WebView硬件加速渲染网页的过程, 如图5所示:
Android WebView简要介绍和学习计划

文章图片


图5 Android WebView硬件加速渲染网页过程
从前面Android应用程序UI硬件加速渲染技术简要介绍和学习计划这个系列的文章可以知道, App从接收到VSync信号开始渲染一帧UI。两个VSync信号时间间隔由屏幕刷新频率决定。一般的屏幕刷新频率是60fps, 这意味着每一个VSync信号到来时, App有16ms的时间绘制自己的UI。
这16ms时间又划分为三个阶段。第一阶段用来构造App UI的Display List。这个构造工作由App的UI线程执行, 表现为各个需要更新的View的成员函数onDraw会被调用。第二阶段App的UI线程会将前面构造的Display List同步给Render Thread。这个同步操作由App的Render Thread执行, 同时App的UI线程会被阻塞, 直到同步操作完成为止。第三阶段是绘制App UI的Display List。这个绘制操作由App的Render Thread执行。
由于Android WebView是嵌入在App的UI里面的, 因此它每一帧的渲染也是按照上述三个阶段进行的。从前面Chromium网页渲染机制简要介绍和学习计划这个系列的文章可以知道, 网页的UI最终是通过一个CC Layer Tree描述的, 也就是Android WebView的Render端会创建一个CC Layer Tree来描述它正在加载的网页的UI。与此同时, Android WebView还会为Render端创建一个Synchronous Compositor, 用来将网页的UI渲染在一个Synchronous Compositor Output Surface上。渲染得到的最终结果通过一个Compositor Frame描述。这意味网页的每一帧都是通过一个Compositor Frame描述的。
Android WebView的Browser端负责合成Render端渲染出来的UI。为了完成这个合成操作, 它同样会创建一个CC Layer Tree。这个CC Layer Tree只有两个节点, 一个是根节点, 另外一个是根节点的子节点, 称为Delegated Renderer Layer。这个Delegated Renderer Layer的内容来自于Render端的渲染结果, 也就是一个Compositor Frame。与此同时, Android WebView会为Browser端创建一个Hardware Renderer, 用来将它的CC Layer Tree渲染在一个Parent Output Surface上, 实际上就是将网页的UI合成在App的窗口上。
在第一阶段, Android WebView的成员函数onDraw会在App的UI线程中被调用。在调用期间, 它又会调用为Render端创建的Synchronous Compositor的成员函数DemandDrawHw, 用来渲染Render端的CC Layer Tree。渲染完成后, 为Render端创建的Synchronous Compositor Output Surface的成员函数SwapBuffers就会被调用, 并且交给它一个Compositor Frame。这个Compositor Frame描述的就是网页当前的UI, 它最终会被保存在一个SharedRendererState对象中。这个SharedRendererState对象是用来描述网页的状态的。
在第二阶段, 保存在上述SharedRendererState对象中的Compositor Frame会被提取出来, 并且同步给Browser端的CC Layer Tree, 也就是设置为该CC Layer Tree的Delegated Renderer Layer的内容。
在第三阶段, Android WebView为Browser端创建的Hardware Renderer的成员函数DrawGL会在App的Render Thread中被调用, 用来渲染Browser端的CC Layer Tree。渲染完成后, 为Browser端创建的Parent Output Surface的成员函数SwapBuffers就会被调用, 这时候它就会将得到的网页UI绘制在App的窗口上。
以上就是Android WebView以硬件加速方式将网页UI渲染在App窗口的过程。当然, Android WebView也可用软件方式将网页UI渲染在App窗口。不过, 在这个系列的文章中, 我们只要关注硬件加速渲染方式, 因为这种渲染方式效率会更高, 而且Android系统从4.0版本之后, 默认已经是使用硬件加速方式渲染App的UI了。
接下来, 我们就结合源码, 按照以下四个情景, 详细分析Android WebView硬件加速渲染网页UI的过程:
1. Android WebView加载Chromium动态库的过程;
2. Android WebView启动Chromium渲染引擎的过程;
3. Android WebView执行OpenGL命令的过程;
4. Android WebView硬件加速渲染网页UI的过程。
【Android WebView简要介绍和学习计划】分析了这四个情景之后, 我们就可以对Android WebView的实现有深刻的认识了。敬请关注! 更多的信息也可以关注老罗的新浪微博: http://weibo.com/shengyangluo。

    推荐阅读