《像素点旅行记WebView篇》

原文连接请参见BeesAndroid项目,javascript

咱们平时有没有思考过这样一个问题,当咱们打开一个H5页面时,从手指触摸到屏幕到页面被渲染出来,这期间发生了哪些事情。这是一个比较有深度的问题,它能够考验咱们对整个Android浏览器体系的掌握程度,其中不乏软件和硬件方面的知识,深刻了解这些能够帮助咱们在分析问题和设计方案时,看的更广,看的更深。

《像素点旅行记WebView篇》将以这个问题为出发点,深刻探讨H5渲染过程当中涉及到的各类理论知识。

css

the_life_of_pixel_webview_01.svg


如上图所示,咱们来思考一下这个过程可能会涉及哪些流程。

  1. 触摸反馈:首先是触摸,这个触摸事件是怎么传递到Android里面的App的。
  2. 容器建立:当触摸事件传递到App中,Android是怎么启动WebView容器来加载URL的。这里会涉及Chromium内核的启动等相关知识。
  3. 页面加载:WebView启动之后,是怎样向服务端发送主文档请求的,又是怎么接收主文档响应的。
  4. 页面渲染:WebView接收到主文档以后,是怎么样将它解析成页面的,这个是最为关键也是最为复杂的一环。


虽然简单来讲,大体是上面四个流程。可是内部的实现仍是很是复杂的,咱们来一一探究。文章较长,能够选择性的进行阅读。
html

触摸反馈


the_life_of_pixel_webview_01.svg


the_life_of_pixel_webview_01.drawio

当用户在和操做系统交互以前,他首先接触到的就是触摸屏,触摸屏是传感器的一种,目前市面上的触摸屏大都基于电容来实现的。早期还有电阻屏和表面声波屏。这几种屏幕的区别能够参考 这篇文章

电容屏是利用人体的电流感应进行工做的,用户触摸电容屏幕时,因为人体电场,用户和触摸屏表面会造成一个耦合电容。对于高频电流来讲,电容是直接导体,因而手指从接触点吸走一个很小的电流(天天都在被电击😲)。这个电流从触摸屏的四角电极流出,而且流经这四个电极的电流和手指到四角的距离成正比,电容屏控制器芯片会对这个电流的比例作精确计算,得出触摸点的位置。

当触摸屏控制芯片获得触摸点位置之后会经过总线接口(例如PC总线接口)将信号传导到CPU的引脚上,它是依赖电气信号(电压高低变化)进行通讯,这部份内容能够去《计算机体系结构》中进一步了解其中的原理,笔者也是个硬件小白,就不展开了。

CPU收到触屏控制器的电气信号之后就是触发CPU的中断机制,咱们每次触摸会产生两次中断,滑动的时候回产生上百次中断。Android系统是基于Linux内核的,对于Linux内核来讲每一个外部设备都有一个惟一的标识符,称为中断请求号(IRQ),能够在adb shell下经过cat /proc/interrupts查看,例如:

CPU0       CPU1       CPU2       CPU3       CPU4       CPU5       CPU6       CPU7       
  3:      20053      20264      12264      11228       7765       7477       7543       9172       GIC  27 Edge      arch_timer
  5:        928       1570       1080        994        483        375        505        522       GIC  80 Level     timer
  6:          0          0          0          0          0          0          0          0       GIC  81 Level     acpu osa
  7:         91          0          0          0          0          0          0          0       GIC 103 Level     acpu om_timer
  8:          0          0          0          0          0          0          0          0       GIC 104 Level     hard_timer_irq
  9:       1748          0          0          0          0          0          0          0       GIC 186 Level     ipc_irq
 10:          0          0          0          0          0          0          0          0       GIC 187 Level     ipc_sem
 39:       3803          0          0          0          0          0          0          0       GIC 152 Level     ffd73000.i2c
 40:       1890          0          0          0          0          0          0          0       GIC 113 Level     fdf0c000.i2c
 41:       2006          0          0          0          0          0          0          0       GIC 114 Level     fdf0d000.i2c
 43:        103          0          0          0          0          0          0          0       GIC 107 Level   
 48:          0          0          0          0          0          0          0          0       GIC 145 Level     pl022
 49:          0          0          0          0          0          0          0          0       GIC 112 Level     pl022
 52:     100195          0          0          0          0          0          0          0       GIC 182 Level     asp_irq_slimbus
 53:      36182      49662          0          0          0          0          0          0       GIC 169 Level     mmc0
 54:       2414          0          0          0          0          0          0          0       GIC 290 Level     gpufreq
 55:          0          0          0          0          0          0          0          0       GIC 291 Level     gpufreq
 56:       6464          0          0          0          0          0          0          0       GIC 292 Level     gpufreq
 57:       3530          0          0          0          0          0          0          0       GIC 277 Level     irq_pdp
 58:          0          0          0          0          0          0          0          0       GIC 278 Level     irq_sdp
 59:          0          0          0          0          0          0          0          0       GIC 279 Level     irq_adp
 64:         56          0          0          0          0          0          0          0       GIC 171 Level     dw-mci

复制代码


咱们在之前学习计算机原理的时候知道当中断产生之后,CPU会停下当前运行的程序,保存当前运行状态,而后跳转到相应的中断处理程序进行处理,这个处理程序通常是由三方内核驱动来实现的,例如Android input里的drivers/input/touchscreen/ektf3k.c。该驱动程序会调用它里面的input_report_abs等方法记录触摸屏下面的坐标信息,而后经过input模块将这些信息都写进/dev/input/event1设备文件中。咱们能够经过getevent工具查看这些信息。
前端

adb shell su -- getevent -lt /dev/input/event1java

[   78826.389007] EV_ABS       ABS_MT_TRACKING_ID   0000001f
[   78826.389038] EV_ABS       ABS_MT_PRESSURE      000000ab
[   78826.389038] EV_ABS       ABS_MT_POSITION_X    000000ab
[   78826.389068] EV_ABS       ABS_MT_POSITION_Y    0000025b
[   78826.389068] EV_ABS       ABS_MT_SLOT          00000001
[   78826.389068] EV_ABS       ABS_MT_TRACKING_ID   00000020
[   78826.389068] EV_ABS       ABS_MT_PRESSURE      000000b9
[   78826.389099] EV_ABS       ABS_MT_POSITION_X    0000019e
[   78826.389099] EV_ABS       ABS_MT_POSITION_Y    00000361
[   78826.389099] EV_SYN       SYN_REPORT           00000000
[   78826.468688] EV_ABS       ABS_MT_SLOT          00000000
[   78826.468688] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[   78826.468719] EV_ABS       ABS_MT_SLOT          00000001
[   78826.468719] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[   78826.468719] EV_SYN       SYN_REPORT           00000000
复制代码


当坐标信息被写进/dev/input/event1设备文件之后,应用程序只须要监听该设备文件的变化就能够知道用户进行了哪些触摸操做,可是实际操做这么作的话会很是繁琐,这一套繁琐的工做会交于各个系统的GUI框架来完成,Android系统上就是WindowManagerService了,它是Android系统内的窗口管理服务,它会负责读取位置信息,并分发到指定的App,回调响应的监听函数(onTouch)。

到这里,用户的触摸事件就传递到App的页面中了,这个时候回调onTouch方法,响应用户的触摸事件。在Android上打开一个H5,通常是打开一个Activity,这个Activity内部会有一个WebView,它会调用loadUrl方法来加载URL。
node

容器启动


上面咱们提到H5是在WebView里被加载并渲染的,而WebView又是在Activity被加载的,因此一个H5容器的启动就包含两个方面:
linux

  1. Activity启动与建立
  2. WebView启动与建立

注:为何会特意聊聊容器启动呢,由于这个也是H5页面体验的重要组成部分,由于是Native的关系,前端同窗可能会关注不到。并且容器导航阶段是重要的预加载时机,咱们能够在这里作不少事情,例如:android

  1. 接口预加载
  2. HTML文档预加载
  3. 资源预加载
  4. 导航的时候建立一个JS Engine,能够提早执行JS逻辑,把导航预加载这个能力开放给前端。

1 Activity启动与建立

注:这部份内容涉及比较多的Android知识,前端同窗能够跳过。git


Activity的启动流程图(放大可查看)以下所示:

github

activity_start_flow.png


整个流程涉及的主要角色有:

  • Instrumentation: 监控应用与系统相关的交互行为。
  • AMS:组件管理调度中心,什么都不干,可是什么都管。
  • ActivityStarter:Activity启动的控制器,处理Intent与Flag对Activity启动的影响,具体说来有:1 寻找符合启动条件的Activity,若是有多个,让用户选择;2 校验启动参数的合法性;3 返回int参数,表明Activity是否启动成功。
  • ActivityStackSupervisior:这个类的做用你从它的名字就能够看出来,它用来管理任务栈。
  • ActivityStack:用来管理任务栈里的Activity。
  • ActivityThread:最终干活的人,Activity、Service、BroadcastReceiver的启动、切换、调度等各类操做都在这个类里完成。


注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏之后,就有了多个ActivityStack,因而就引入了ActivityStackSupervisior用来管理多个ActivityStack。

整个流程主要涉及四个进程:

  • 调用者进程,若是是在桌面启动应用就是Launcher应用进程。
  • ActivityManagerService等所在的System Server进程,该进程主要运行着系统服务组件。
  • Zygote进程,该进程主要用来fork新进程。
  • 新启动的应用进程,该进程就是用来承载应用运行的进程了,它也是应用的主线程(新建立的进程就是主线程),处理组件生命周期、界面绘制等相关事情。


有了以上的理解,整个流程能够归纳以下:

  1. 点击桌面应用图标,Launcher进程将启动Activity(MainActivity)的请求以Binder的方式发送给了AMS。
  2. AMS接收到启动请求后,交付ActivityStarter处理Intent和Flag等信息,而后再交给ActivityStackSupervisior/ActivityStack
    处理Activity进栈相关流程。同时以Socket方式请求Zygote进程fork新进程。
  3. Zygote接收到新进程建立请求后fork出新进程。
  4. 在新进程里建立ActivityThread对象,新建立的进程就是应用的主线程,在主线程里开启Looper消息循环,开始处理建立Activity。
  5. ActivityThread利用ClassLoader去加载Activity、建立Activity实例,并回调Activity的onCreate()方法。这样便完成了Activity的启动。

注:读者能够发现这上面有不少函数有Locked后缀,这表明这些函数须要进行多线程同步(synchronized)操做,它们会读写一些多线程共享的数据。

2 WebView启动建立


WebView若是只是把它当作一个View,那它的启动流程就与Android其余View没有什么区别了,从代码或者XML文件里解析并构建View对象。可是咱们这里探讨的不是一个简单的View建立,而是借此讨论Chromium引擎(WebView基于Chromium内核实现)在Android平台的启动流程。

当咱们提到启动流程,先思考一下启动了什么,答案固然是启动了进程&线程,它是咱们代码运行的载体,于是Chromium的启动流程能够进一步提炼为Chromium中各类进程的启动流程。Android 7.0加入开发者选项,能够打开多进程。Android 8.0默认打开多进程。目前只有Render Process,GPU Process仍然为Browser Process的一个线程。

什么是Chromium多进程架构?

Chromium是一个多进程的架构,多进程架构的意义:

  • 稳定性:不一样H5页面运行在不一样的进程中,彼此不会相互影响。独立进程还能够减小主进程的内存压力,提高主进程的稳定性。
  • 安全性:Chromium利用双重防御机制实现SendBox机制。

image.png

Multi-process Architecture

如上图所示Chromium有多个进程,具体说来:

> 另外,Chromium在Android系统上有两套实现。

  • Chrome浏览器:每一个进程包含Main Thread、IO Thread等多个线程。Render Process和GPU Process基于Android Service组件实现。
  • Android WebView:Android View组件,能够在Android App中使用,加载H5.
  • 单进程:Render Process和GPU Process变成Browser Process中的线程。
  • 多进程:Android 7.0加入开发者选项,能够打开多进程。Android 8.0默认打开多进程。目前只有Render Process,GPU Process仍然为Browser Process的一个线程。


当咱们构建一个WebView实例的时候,它会先去加载Chromium相关so库,而后启动Browser、Render等进程以及进程运行的必要组件。这样整个容器就启动起来了。启动流程以下所示:

webview_startup.svg

  • Browser:负责启动Browser的是//android_webview里的AwBrowserProcess,它封装了//content/public/android下的BrowserStartupController。启动Browser的流程实际上就是在App的UI线程建立一个Browser Main Loop,Chromium之后须要请求Browser执行一个操做时,就能够像Browser Main Loop发送一个Task,固然这个Task绘制UI线程里执行。
  • Render:负责启动Render的是//android_webview里的AwContents,它封装了//content/public/android下的ContentViewCore。启动Render在不一样的版本有所区别。
    • 在Android O以前是单进程架构,它会在当前App进程中建立一个线程,之后网页就在这个线程中渲染。这个称之为In-Process Renderer。
    • 在Android O以后是多进程架构,它会单首创建一个进程,之后网页在这个进程里渲染。这个称之为Out-of-Process Renderer。
  • GPU:Chromium的GPU实如今App的Render Thread,Render Thread是App本身建立的,由于Chromium无需单独启动它。不过android_webview模块会启动一个DeferredGpuCommandService服务,当CHromium的Browser和Render须要执行GPU操做时,会向DeferredGpuCommandService服务发送请求,DeferredGpuCommandService服务经过App的UI线程将GPU操做提交给Render Thread执行。


容器启动之后,即可以调用loadUrl去加载页面了。

页面加载


WebView容器建立完成之后,就能够调用它的loadUrl方法加载页面了。它会先向服务端请求HTML Document文档资源。发起请求前会先对URL进行解析,而后开启一个网络线程去请求HTML Document资源。以下所示:

image.png

注:上图中前面展现了是在一个浏览器内部发生页面导航跳转的状况,基本逻辑都是一直的,当咱们直接打开一个H5时,能够视为从Start url request这个阶段开始。


按照颜色划分,整个页面导航加载的流程有三个角色:

  • 黄色:Browser
  • 蓝色:Network
  • 绿色:Renderer
  1. BrowserInitialization:网址转换,将输入的关键字等转换为真正的网址。(直接打开H5没有这一步)。
  2. BeiginNavigation:调用BeiginNavigation函数开始导航。
  3. Start url request:启动URL请求。网络请求由NavigationURLLoader来管理。
  4. Read Response:读取响应。
  5. Find render:收到URL请求响应以后,跳回浏览器界面准备构建页面。
  6. Commit:告诉Renderer Process,收到了HTML文档,有新的页面须要渲染。
  7. Frame has commited navigation:给Browser Process一个ACK,告诉它已经收到了渲染请求。至于导航流程已经所有完成,可是因为尚未进入渲染流程,于是界面仍是白屏。
  8. Load:读取HTML文档的Response,对其进行解析,并呈现的网页中。
  9. Load Stop:一旦Renderer Process渲染完成,它会通知Browser Process页面已经加装完成。


咱们再把上面和W3C Navigation Timing进行对比,就能够对应上了。

image.png


关于HTTP请求&响应

网络请求主要由NavigationURLLoader来管理,发起HTTP请求主要包含如下流程:

  1. DNS解析
    1. 若是浏览器有缓存,则直接使用浏览器缓存,不然使用本机缓存。
    2. 若是本地没有,则使用DNS解析服务,查询对应IP。
  2. TCP建连(若是是HTTPS,还须要链接TLS链接)
    • 三次握手创建链接
    • 四次挥手断开链接
  3. 请求数据传输
    • 应用层发送数据请求
    • 传输层经历三次握手链接TCP链接
    • 网络层进行IP寻址
    • 数据链路层将数据封装成帧
    • 物理层利用物理介质传输
  • 读取请求响应
    • 2xx:响应成功
    • 3xx:重定向
    • 4xx:客户端错误
    • 5xx:服务端错误

这一块的内容网上资料不少,就再也不展开了。


等到拿到HTML Response之后,开始进行流式传输Response到Renderer Process,开始进行渲染以及处理资源。接下来主要包含如下两件事情:

  1. Renderer Process将HTML渲染成与用户交互的页面,它主要包含两个部分:
    • Blink:页面渲染,包含DOM tree、Layout、Paint、Raster等。
    • V8:脚本解析&执行,修改页面,响应事件,下载资源等。
  2. 图片、JS、CSS等资源的下载(ResourceRequests)。

页面渲染


渲染流程里的图片来自于Chromium工程师的ppt Life of a Pixel的截图。

浏览器的渲染过程就是把网页经过渲染管道渲染成一个个像素点,最终输出到屏幕上。这里面就涉及3个角色

  • 输入端:网页,Chromium将其抽象成Content。
  • 渲染管线:主要是Blink负责DOM解析、样式布局、绘制等操做,将网页内容转换为绘制指令。
  • 输出端:主要负责把绘制指令转换为像素,显示在屏幕上。


什么是输入端(Content)?

咱们在Chromium这个项目里会频繁的看到Content这个概念,那么Content究竟是什么呢。Content是渲染网页内容的区域,在Java层对应AwContent,底层有WebContents表示,以下所示:

image.png

content在代码由content::WebContents来描述,它在独立的Render进程由Blink建立。具体说来Content对应着前端开发中涉及的HTML、CSS、JS、image等,以下所示:

image.png


什么是渲染管线(Rendering Pipeline)?

渲染管线能够理解为对渲染流程的拆解,向工厂流水线同样,上一个车间生成的半成品送到下一个车间继续装配。拆解渲染流程有助于把渲染流程简单化,提升渲染效率。

渲染时动态的,内容发生变化时,就会触发渲染,更新像素点,和Android的绘制系统同样,触发绘制也是由invalidate机制触发的,触发渲染后,执行整个渲染管线是很是昂贵的,于是Blink也在想法设法减小没必要要的渲染动做,提升渲染效率。

  • 触发的条件以下所示:
    • scrolling
    • zooming
    • animations
    • incremental loading
    • javascript
  • 各个流程的触发方法以下:
    • Style:Node::SetNeedsStyleRecalc()
    • Layout:LayoutObject::SetNeedsLayout()
    • Paint:PaintInvalidator::InvalidatePaint()
    • RasterInvalidator::Generate()


渲染管道把网页转换为绘制指令后,它并不能直接把绘制指令变成像素点(光栅化)显示在屏幕上(Window),这个时候就须要借助操做系统本身的能力(底层的图形库),在图形界面这一块大部分平台都遵循OpenGL标准化的API。例如Windows上的DirectX,Android上的Vulcan。以下图所示:

image.png


经过上面的描述,咱们了解了Conntent从哪里来,要到哪里去。总的来讲就是把HTML、CSS、JS等转换为正确的OpenGL指令,而后渲染到屏幕上,与用户交互。

在了解了渲染的基本要素之后,咱们来看看具体的渲染流程是怎样执行的,以下所示:

image.png


咱们先来讲结构

从上到下,分层来讲:

  • Blink:运行在Render进程的Render线程,它是Chromium的Blink渲染引擎,主要负责HTML/CSS的解析、jS的解释执行(V8)、DOM操做、排版、图层树的构建更新等任务。
  • Layer Compositor:运行在Render进程的Compositor线程,它负责接收Blink生成的Main Frame,负责图层树的管理、图层的滚动、旋转等矩阵变化,图层的分块、光栅化、纹理上传等任务。
  • Display Compositor:运行在Browser进程的UI线程,它负责接收Layer Compositor生成的Compositor Frame,输出最终的OpenGL绘制指令,将网页内容经过GL贴图操做绘制到目标窗口上。


这里面还提到了每一个层级向上输出的产物帧,帧(Frame)描述了渲染流水线下级模块向上级模块输出的绘制内容相关数据的封装。

  • Main Frame:包含了对网页内容的描述,主要以绘图指令的形式,或者理解为某个时间点对整个网页的一个矢量图快照。
  • Compositor Frame:Layer Compositor接收Blink生成的Main Frame,并转换成内部的合成器结构。它会被发往Browser,并最终到达Compositor Frame,它主要由两部分构成:
    • Resource:它是对Texture的封装,Layer Compositor为每一个图层分块,而后为每一个分块分配Resource,而后安排光栅化任务。
    • Draw Quad:它表明了绘制指令(矩形绘制指令,指定了坐标、大小、变换矩阵等属性),Layer Compositor接收到Browser的绘制请求时,它会为当前可见区域每一个图层的每一个分块生成一个Draw Quad绘制指令。
  • GL Frame:Display Compositor将Compositor Frame的每一个Draw Quad绘制指令转换成一个GL多边形绘制指令,使用对应的Resource封装的Texture对目标窗口进行贴图。这个GL绘图指令的集合就构成了一个GL Frame,最终由GPU执行这些GL指令完成网页在窗口可见区域的绘制。


整个渲染水流水线的调度基于请求和状态机响应,调度的中枢运行在Browser UI线程,它按照显示器的VSync信号向Layer Compositor发出输出下一帧的请求,而Layer Compositor根据自身的状态机的状态决定是否须要Blink输出下一帧。而Layer Compositor和Display Compositor是生成者和消费者的关系,Display Compositor持有一个Compositor Frame队列不断的进行取出和绘制,输出的频率取决于 Compositor Frame的输入帧率和自身GL Frame的绘制频率。

咱们再来讲流程

  1. Parse/DOM:将Content解析成DOM树,它是后面各个渲染流程的基础。
  2. Style:解析并应用样式表。
  3. Layout:布局。
  4. Compositing update:将整个页面按照必定规则,分红独立的图层,便于隔离更新。
  5. prepaint:构建属性树,使得能够单独操做某个节点(变换、裁剪、特效、滚动),不至于影响它的子节点。
  6. paint:paint这个单词名词有油漆、颜料的含义。动词有用颜料画等含义。这里我以为使用它的名词含义比较贴切,Paint操做会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。而后把这些操做封装在Dsipaly Item中,因此这些Display Item就像是油漆,它尚未真正的开始粉刷(绘制Draw)。
  7. Commit:commit会把paint阶段的数据拷贝的合成器线程。
  8. Tiling:raster接收到paint阶段的绘制指令以后,会先对图层进行分块。图块是栅格化(Raster)的基本工做单位。
  9. Raster:栅格化。
  10. Activate:栅格化是个异步的过程,于是图层树(Layer Tree)被分为了Pending Tree(负责接收Commit提交的Layer进行栅格化操做)和Activate Tree(从这里取出栅格化的Layer进行Draw操做),从Pending Tree拷贝Layer到Activate Tree的过程就叫作Activate。
  11. Draw:这里要和上面的Paint区分开来了,图块被栅格化之后,合成器线程会为每一个图块生成draw quads(quads有四边形之意,它表明了在屏幕特定位置绘制图块的指令,包含属性树里面的变换、特效等信息),这些draw quads被封装到Compositor Frame中输出给GPU,Draw操做就是生成draw quads的过程。
  12. Display:生成了Compositor Frame之后,Viz会调用GL指令把draw quads最终输出到屏幕上。


咱们来分别看具体的流程。

Blink

01 Parse


相关文档


相关源码


当咱们从服务器上下载了一份HTML文档,第一步就是解析,HTML解析器接收标签和文本流(HTML是纯文本格式)把HTML文档解析成DOM树。DOM(Document Object Model)即文档对象模型,DOM及时页面的内部表示,也为JavaScript暴露了API接口(V8 DOM API),可让JavaScript程序改变文档的结构、样式和内容。

它是一个树状结构,咱们在后续的渲染流程中还会看到不少树形结构(例如布局树、属性树等)由于它们都是基于DOM树的结构(HTML的结构)而来的。

image.png

注:HTML文档中可能包含多棵DOM树,由于HTML支持自定义元素,这种树一般被称为Shadow Tree。


解析HTML生成DOM树流程以下:

  1. HTMLDocumentParser负责解析HTML中的token,生成对象模型。
  2. HTMLTreeBuilder负责生成一棵完整的DOM树,同一个HTML文档能够包含多个DOM树,Custom Element元素具备一棵shadow tree。在shadow tree slot中传入的节点会被FlatTreeTraversal向下遍历时找到。


DOM树(DOM Tree)做为后续绘制流程的基础, 还会基于它生产各类类型的树,具体说来,主要会经历以下转换:

对象转换

  • DOM Tree -> Render Tree -> Layer Tree
  • DOM node -> RenderObject -> RenderLayer

DOM Tree(节点是DOM node)

当加载一个HTML时,会对他进行解析,生成一棵DOM树。DOM树上的每个节点都对应这网页里面的每个元素,网页能够经过JavaScript操做这棵DOM树。

image.png

How Webkit Works

Render Tree(节点是RenderObject)

可是DOM树自己并不能直接用于排版和渲染,所以内核会生成Render Tree,它是DOM Tree和CSS相结合的产物,二者的节点几乎是一一对应的。Render Tree是排版引擎和渲染引擎之间的桥梁。

image.png

How Webkit Works

Layer Tree(节点是RenderLayer)

渲染引擎并非直接使用Render Tree进行绘制的,为了更加方便的处理定位、裁剪、业内滚动等操做,渲染引擎会生成一棵Layer Tree。渲染引擎会为一些特定的RenderObject生成相应的RenderLayer,不过该RenderObject的子节点没有相应的RenderLayer,那么它就从属于父节点的RenderLayer。渲染引擎会遍历每个RenderLayer,再遍历从属于这个RenderLayer的RenderObject,将每个RenderObject绘制出来。

能够这么理解,Layer Tree决定了网页的绘制顺序,从属于RenderLayer的RenderObject决定了这个Layer的绘制内容。

什么样的RenderObject会成为RenderLayer呢。GPU Accelerated Compositing in Chrome是这样定义的:

  • It's the root object for the page
  • It has explicit CSS position properties (relative, absolute or a transform)
  • It is transparent
  • Has overflow, an alpha mask or reflection
  • Has a CSS filter
  • Corresponds to element that has a 3D (WebGL) context or an accelerated 2D context
  • Corresponds to a


对上面的流程不了解也不要紧,咱们下面会一一解释。

02 Style


当DOM树生成之后,就须要为每一个元素设置一个样式,有的样式只是会影响某个节点,有的样式会影响整个节点下面的整个DOM子树的渲染(例如,节点的旋转变换)。

image.png


相关文档


相关源码


样式通常都是样式渲染器共同做用的结果,它有复杂的优先级语义和渲染过程,过程总体分为三步:

1 收集、划分和索引全部样式表中样式规则。

image.png


CSSParser首先CSS文件解析成对象模型StyleSheetContents,它里面包含各类样式规则(StyleRule),这些样式规则具备丰富的表现形式。包含选择器(CSSSelector)和属性值映射(CSSPropertyValue)在这些样式规则中,对象以各类方式创建索引,进行更有效的查找。

另外,样式属性以声明的方式进行定义,定义在Chromium里的css_properties,json5这个json文件里,这些定义会经过py脚本生成特定的C++类。

2 访问每一个DOM元素并找到应用在该元素的全部规则。

样式引擎会遍历整个DOM树,计算每一个节点的样式,计算样式(ComputeStyle)会完成property到rule的映射,例如字体样式、边距、背景色等。这些就是样式引擎的输出。

image.png


3 结合这些规则以及其余信息(样式引擎由部分默认的样式)生成最终的计算样式。

03 Layout


计算并应用了每一个DOM节点的样式之后,就须要决定每一个DOM节点的摆放位置。DOM节点都是基于盒模型摆放(一个矩形),布局就是计算这些盒子的坐标。

image.png


布局操做是创建在 CSS盒模型基础之上的,以下所示:

|-------------------------------------------------|
    |                                                 |
    |                  margin-top                     |
    |                                                 |
    |    |---------------------------------------|    |
    |    |                                       |    |
    |    |             border-top                |    |
    |    |                                       |    |
    |    |    |--------------------------|--|    |    |
    |    |    |                          |  |    |    |
    |    |    |       padding-top        |##|    |    |
    |    |    |                          |##|    |    |
    |    |    |    |----------------|    |##|    |    |
    |    |    |    |                |    |  |    |    |
    | ML | BL | PL |  content box   | PR |SW| BR | MR |
    |    |    |    |                |    |  |    |    |
    |    |    |    |----------------|    |  |    |    |
    |    |    |                          |  |    |    |
    |    |    |      padding-bottom      |  |    |    |
    |    |    |                          |  |    |    |
    |    |    |--------------------------|--|    |    |
    |    |    |     scrollbar height ####|SC|    |    |
    |    |    |-----------------------------|    |    |
    |    |                                       |    |
    |    |           border-bottom               |    |
    |    |                                       |    |
    |    |---------------------------------------|    |
    |                                                 |
    |                margin-bottom                    |
    |                                                 |
    |-------------------------------------------------|
复制代码


相关文档


相关源码


基于DOM Tree会生成Layout Tee,生成每一个节点的布局信息。布局的过程就是遍历整个Layout Tree进行布局操做。

DOM Tree和Layout Tree也不老是一一对应的,若是咱们再标签里设置dispaly:none,它就不会建立一个布局对象(LayoutObject)。

image.png

04 Compositing Update


在Layout操做完成之后,理论上就能够开始Paint操做了,可是咱们以前提过,若是直接开始Paint操做,绘制整个界面,代价是很是昂贵的。所以便引入了一个图层合成加速的概念。

什么是图层合成加速(Compositing Layer)?

图层合成加速基本思想是把整个页面按照必定规则分红多个图层(就像Photoshop的图层那样),在渲染时只须要操做必要的图层,其余图层只须要参与合成就好了,以此提升渲染效率。完成这个工做的线程叫Compositor Thread,值得一提的是Compositor Thread还具有处理输入事件的能力(例如滚动事件),可是若是在JavaScript注册了事件监听,它会把输入事件转发给主线程处理。

image.png

具体说来是为某些RenderLayer拥有本身独立的缓存,它们被称为合成图层(Compositing Layer),内核会被这些RenderLayer建立对应的GraphicsLayer。

  • 拥有本身的GraphicsLayer的RenderLayer在绘制的时候就会绘制在本身的缓存里面。
  • 没有本身的GraphicsLayer的RenderLayer会向上查找父节点的GraphicsLayer,直到RootRenderLayer(它老是会有本身的GraphicsLayer)为止,而后绘制在有GraphicsLayer的父节点的缓存里。

image.png

这样就造成了与RenderLayer Tree对应的GraphicsLayer Tree。当Layer的内容发生变化时,只须要更新所属的GraphicsLayer便可,而单一缓存架构下,就会更新整个图层,会比较耗时。这样就提升了渲染的效率。可是过多的GraphicsLayer也会带来内存的消耗,虽然减小了没必要要的绘制,但也可能由于内存问题致使总体的渲染性能下贱。于是图层合成加速追求的是一个动态的平衡。


什么样的RenderLayer会被建立GraphicsLayer呢,GPU Accelerated Compositing in Chrome是这样定义的:

  • Layer has 3D or perspective transform CSS properties
  • Layer is used by
  • Layer is used by a element with a 3D context or accelerated 2D context
  • Layer is used for a composited plugin
  • Layer uses a CSS animation for its opacity or uses an animated webkit transform
  • Layer uses accelerated CSS filters
  • Layer has a descendant that is a compositing layer
  • Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer overlaps a composited layer and should be rendered on top of it)


图层化的决策是由Blink来负责(将来可能会转移到Layer Compositor决策),根据DOM树生成一个图层树,并以DisplayList记录每一个图层的内容。

了解了图层合成加速的概念之后,咱们再来看看发生在Layout操做以后的Compositing update(合成更新),合成更新就是为特定的RenderLayer(建立规则咱们已经描述过了)建立GraphicsLayer的过程,以下所示:

image.png

05 Prepaint


什么是属性树?

在描述属性的层次结构这一块,以前的方式是使用图层树的方式,若是父图层具备矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),须要递归的应用到子节点,时间复杂度是O(图层数),这在极端状况下会有性能问题。

所以引入了属性树的概念,合成器提供了变换树、裁剪树、特效树等。每一个图层都由若干节点id,分别对应不一样属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O(要变化的节点),以下所示:

image.png


Prepaint的过程就是构建属性树的过程,以下所示:

image.png

06 Paint


建立完属性树(Prepaint)之后,就开始进入Paint阶段了。

相关文档


相关源码


Paint操做会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。而后把这些操做封装在Dsipaly Item中,这些Dsipaly Item存放在PaintArtifact中。PaintArtifact就是是Paint阶段的输出。

到目前为止,咱们创建了能够重放的绘制操做列表,但没有执行真正的绘制操做。

注:重放(replay),如今图形系统大都采用recrod & replay机制,采集绘制指令与执行绘制指令相互分离,提升渲染效率


image.png


在绘制的过程当中,会涉及一个绘制顺序的问题,它使用的是stacking order(z-index),而不是DOM order。z-index会决定绘制顺序,在没有z-order指定的状况下,Paint会按照如下顺序进行绘制。

image.png

  • 背景色
  • floats
  • 前景色
  • 轮廓


Paint操做最终会在Layout Tree的基础上生成一棵Paint Tree。

image.png


Layer Compositor

07 Commit


Paint阶段完成之后,进入Commit阶段。该阶段会更新图层和属性树的副本到合成器线程,以匹配提交的主线程状态。说的通俗点,就是把主线程里Paint阶段的数据(layers and properties)拷贝到合成器线程,供合成器线程使用。

image.png

08 Tiling


可是合成器线程接收到数据后,并不会当即开始合成,而是进行图层分块,这里又涉及一个分块渲染的技术。

什么是分块渲染?

分块渲染(Tile Rendering)就是把网页的缓存分为一格一格的小块,一般为256x256或者512x512,而后分块进行渲染。

分块渲染主要基于两个方面的考虑:

  • GPU合成一般是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),不少GPU对纹理的大小是有限制的,好比长宽必须是2的幂次方,最大不能超过2048或者4096等。没法支持任意大小的缓存。
  • 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由全部WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。


图块(tiling)是栅格化工做的基本单位。 栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一块儿,就造成了一个图层,以下所示:

image.png

09 Raster


图层分块完成之后,接着就会进行栅格化(Raster)。

什么是光栅化(栅格化)?

光栅化(Raterization),又称栅格化,它用于执行绘图指令生成像素的颜色值,光栅化策略分为两种:

  • 同步光栅化:光栅化和合成在同一线程,或者经过线程同步的方式来保证光珊化和合成
    • 直接光栅化:直接将全部可见图层的eDisplayList中的可见区域的绘图指令进行执行,在目标Surface的像素缓冲区上生成像素的颜色值。固然若是是彻底的直接光栅化,就不涉及图层合并了,也就不须要后面的合成了。
    • 间接光栅化:容许为指定图层分配额外的缓冲区,该图层的光栅化会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区(Android里能够调用View.setLayerType容许应用为View分配像素缓冲区)经过合成输出大欧姆表Surface的像素缓冲区。Android和Flutter主要使用直接光栅化的测量,同时也支持间接光栅化。
  • 异步分块光栅化


上面说到,在Paint阶段会生成DisplayItem列表,它们是对绘制指令的封装。光栅化(Raster)或者栅格化的过程就是把这些绘图指令变成位图(像素点,每一个像素点都带有本身的颜色)。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593585931104-95b9507b-bbdc-43d9-8eab-5a0c430bb6c2.png#align=left&display=inline&height=265&margin=%5Bobject%20Object%5D&name=image.png&originHeight=549&originWidth=1242&size=300772&status=done&style=none&width=600)

光栅化的过程还包括图片解码。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1594026056074-d76001e0-9120-4b19-ac5c-f11593d6514e.png#align=left&display=inline&height=271&margin=%5Bobject%20Object%5D&name=image.png&originHeight=576&originWidth=1276&size=358615&status=done&style=none&width=600)

过去GPU只是做为一个内存(GPU Memory),这些内存被GL纹理(OpenGL中的标识符)所引用。咱们会将栅格化的像素点放到主内存中,而后上传到GPU,以减少内存压力。

如今GPU已经能够运行产生像素点的着色器,能够在GPU上进行栅格化,这种模式成为加速栅格(硬件加速)。无论是硬件栅格化仍是软件栅格化,本质上都是生成了某种内存中像素的位图,这个时候尚未显示到屏幕上。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593585957507-654a5009-fc3c-47eb-bb35-b46cf91d1c47.png#align=left&display=inline&height=291&margin=%5Bobject%20Object%5D&name=image.png&originHeight=549&originWidth=1132&size=256218&status=done&style=none&width=600)

GPU栅格化并非直接调用GPU,而是经过Skia图形库(Google维护的2D图形库,在Android、Flutter、Chromium都有使用)发出的OpenGL调用。以下所示:

Skia提供了某种抽象层,屏蔽了底层硬件、路径、贝塞尔曲线等复杂的概念,当须要栅格化显示项(Display Item)时,会先去调用SkCanvas上面的方法,它是Skia的调用入口。SkCanvas提供了Skia内部更多的抽象,在硬件加速时,它会构建另外一个绘图操做缓冲区,而后对其进行刷新,在栅格化任务结束时,经过flush操做,咱们得到了真正的GL指令。GL指令运行在GPU Process。

![image.png](https://cdn.nlark.com/yuque/0/2020/png/279116/1593586064954-8bed11e0-a743-415e-8e20-54fcb68ccc43.png#align=left&display=inline&height=268&margin=%5Bobject%20Object%5D&name=image.png&originHeight=566&originWidth=1267&size=330761&status=done&style=none&width=600)

Skia和GL指令能够运行在不一样进程,也能够运行在同一个进程这就产生了两种调用方式。
  1. In Proess Raster
  2. Out of Proess Raster


1 老版本的调用采用这种方式,Skia运行在Renderer Process,负责产生GL指令,GPU有单独的GPU Process,这种模式下Skia没法直接进行渲染系统调用,在初始化Skia的时候回给它一个函数指针表(指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理),函数指针表转换为真正的OpenGL API的过程称为命令缓冲区(GpuChannelMsg_FlushCommandBuffers),

单独的GPU进程有利于隔离GL操做,提高稳定性和安全性,这种模式也称为沙箱机制(不安全的操做运行在独立的进程里)。

image.png


2 新版本把绘制操做放到了GPU Process,在GPU一侧运行Skia,这有助于提高性能。

image.png



接下来就是执行GL指令,GL指令通常是由底层so库提供,在Windows平台上OpenGL还会被转换为DirectX(Microsoft的图形API,用于图形加速)。

image.png

10 Activate


在Commit以后,Draw以前有一个Activate操做。Raster和Draw都发生在合成器线程里的Layer Tree上,可是咱们知道Raster操做是异步的,有可能须要执行Draw操做的时候,Raster操做还没完成,这个时候就须要解决这个问题。

它将Layer树分为:

  • Pending Tree:负责接收commit,而后将Layer进行Raster操做
  • Active Tree:会从这里取出栅格化好的Layer进行draw操做。


这个拷贝的过程就称为Activate,以下所示:

image.png


事实上Layer Tree主要有四种:

  • 主线程图层树:cc::Layer,始终存在。
  • Pending树:cc::LayerImpl,合成器线程,用于光栅化阶段,可选。
  • Active树:cc::LayerImpl,合成器线程,用于绘制阶段,始终存在。
  • Recycle树:cc::LayerImpl,合成器线程,与Pending树不会同时存在。


主线程的图层树由LayerTreeHost拥有,每一个图层以递归的方式拥有其子图层。Pending树、Active树、Recycle树都是LayerTreeHostImpl拥有的实例。这些树被定义在cc/trees目录下。之因此称之为树,是由于早期它们是基于树结构实现的,目前的实现方式是列表。

11 Draw

当每一个图块都被光栅化之后,合成器线程会为每一个图块生成draw quads(在屏幕指定位置绘制图块的指令,包含了属性树里面的变换、特效等操做),这些draw quads指令被封装在CompositorFrame对象中,CompositorFrame对象也是Render Process的输出产物。它会被提交到GPU Process中。咱们平时提到的60fps输出帧率里面的帧指的就是Compositor Frame。

Draw操做就是栅格化的图块生成draw quads的过程。

image.png

Display Compositor

12 Display


相关文档


Draw操做完成之后,就生成了Compositor Frame,它们会被输出到GPU Process。 它会从多个来源的Render Process接收Compositor Frame。

  • Browser Process也有本身的Compositor来生成Compositor Frame,这些通常是用来绘制Browser UI(导航栏,窗口等)。
  • 每次建立tab或者使用iframe,会建立一个独立的Render Process。


image.png


Display Compositor运行在Viz Compositor thread,Viz会调用OpenGL指令来渲染Compositor Frame里面的draw quads,把像素点输出到屏幕上。

什么是VIz?

Viz是VIsual的缩写,它是Chromium总体架构转向服务化的一个重要组成部分,包含Compositing、GL、Hit Testing、Media、VR/AR等众多功能。


VIz也是双缓冲输出的,它会在后台缓冲区绘制draw quads,而后执行交换命令最终让它们显示在屏幕上。

什么是双缓冲机制?

在渲染的过程当中,若是只对一块缓冲区进行读写,这样会致使一方面屏幕要等到去读,而GPU要等待去写,这样要形成性能低下。一个很天然的想法是把读写分开,分为:

  • 前台缓冲区(Front Buffer):屏幕负责从前台缓冲区读取帧数据进行输出显示。
  • 后台缓冲区(Back Buffer):GPU负责向后台缓冲区写入帧数据。

这两个缓冲区并不会直接进行数据拷贝(性能问题),而是在后台缓冲区写入完成,前台缓冲区读出完成,直接进行指针交换,前台变后台,后台变前台,那么何时进行交换呢,若是后台缓存区已经准备好,屏幕尚未处理完前台缓冲区,这样就会有问题,显然这个时候须要等屏幕处理完成。屏幕处理完成之后(扫描完屏幕),设备须要从新回到第一行开始新的刷新,这期间有个间隔(Vertical Blank Interval),这个时机就是进行交互的时机。这个操做也被称为垂直同步(VSync)。

到这里,整个渲染流程就结束了,前端的代码变成了能够与用户交互的像素点。

相关文章
相关标签/搜索