原文链接请参见BeesAndroid项目,
我们平时有没有思考过这样一个问题,当我们打开一个H5页面时,从手指触摸到屏幕到页面被渲染出来,这期间发生了哪些事情。这是一个比较有深度的问题,它可以考验我们对整个Android浏览器体系的掌握程度,其中不乏软件和硬件方面的知识,深入了解这些可以帮助我们在分析问题和设计方案时,看的更广,看的更深。
《像素点旅行记WebView篇》将以这个问题为出发点,深入探讨H5渲染过程中涉及到的各种理论知识。
虽然简单来说,大致是上面四个流程。但是内部的实现还是非常复杂的,我们来一一探究。文章较长,可以选择性的进行阅读。
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/event1
[ 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。
上面我们提到H5是在WebView里被加载并渲染的,而WebView又是在Activity被加载的,所以一个H5容器的启动就包含两个方面:
注:为什么会特地聊聊容器启动呢,因为这个也是H5页面体验的重要组成部分,因为是Native的关系,前端同学可能会关注不到。而且容器导航阶段是重要的预加载时机,我们可以在这里做很多事情,例如:
- 接口预加载
- HTML文档预加载
- 资源预加载
- 导航的时候创建一个JS Engine,可以提前执行JS逻辑,把导航预加载这个能力开放给前端。
注:这部分内容涉及比较多的Android知识,前端同学可以跳过。
Activity的启动流程图(放大可查看)如下所示:
注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏以后,就有了多个ActivityStack,于是就引入了ActivityStackSupervisior用来管理多个ActivityStack。
整个流程主要涉及四个进程:
有了以上的理解,整个流程可以概括如下:
注:读者可以发现这上面有很多函数有Locked后缀,这代表这些函数需要进行多线程同步(synchronized)操作,它们会读写一些多线程共享的数据。
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机制。
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等进程以及进程运行的必要组件。这样整个容器就启动起来了。启动流程如下所示:
容器启动以后,便可以调用loadUrl去加载页面了。
WebView容器创建完成以后,就可以调用它的loadUrl方法加载页面了。它会先向服务端请求HTML Document文档资源。发起请求前会先对URL进行解析,然后开启一个网络线程去请求HTML Document资源。如下所示:
注:上图中前面展示了是在一个浏览器内部发生页面导航跳转的情况,基本逻辑都是一直的,当我们直接打开一个H5时,可以视为从Start url request这个阶段开始。
按照颜色划分,整个页面导航加载的流程有三个角色:
我们再把上面和W3C Navigation Timing进行对比,就可以对应上了。
网络请求主要由NavigationURLLoader来管理,发起HTTP请求主要包含以下流程:
- DNS解析
- 如果浏览器有缓存,则直接使用浏览器缓存,否则使用本机缓存。
- 如果本地没有,则使用DNS解析服务,查询对应IP。
- TCP建连(如果是HTTPS,还需要连接TLS连接)
- 三次握手建立连接
- 四次挥手断开连接
- 请求数据传输
- 应用层发送数据请求
- 传输层经历三次握手连接TCP连接
- 网络层进行IP寻址
- 数据链路层将数据封装成帧
- 物理层利用物理介质传输
- 读取请求响应
- 2xx:响应成功
- 3xx:重定向
- 4xx:客户端错误
- 5xx:服务端错误
这一块的内容网上资料很多,就不再展开了。
等到拿到HTML Response以后,开始进行流式传输Response到Renderer Process,开始进行渲染以及处理资源。接下来主要包含以下两件事情:
渲染流程里的图片来自于Chromium工程师的ppt Life of a Pixel的截图。
浏览器的渲染过程就是把网页通过渲染管道渲染成一个个像素点,最终输出到屏幕上。这里面就涉及3个角色
什么是输入端(Content)?
我们在Chromium这个项目里会频繁的看到Content这个概念,那么Content到底是什么呢。Content是渲染网页内容的区域,在Java层对应AwContent,底层有WebContents表示,如下所示:
content在代码由content::WebContents来描述,它在独立的Render进程由Blink创建。具体说来Content对应着前端开发中涉及的HTML、CSS、JS、image等,如下所示:
什么是渲染管线(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。如下图所示:
这里面还提到了每个层级向上输出的产物帧,帧(Frame)描述了渲染流水线下级模块向上级模块输出的绘制内容相关数据的封装。
整个渲染水流水线的调度基于请求和状态机响应,调度的中枢运行在Browser UI线程,它按照显示器的VSync信号向Layer Compositor发出输出下一帧的请求,而Layer Compositor根据自身的状态机的状态决定是否需要Blink输出下一帧。而Layer Compositor和Display Compositor是生成者和消费者的关系,Display Compositor持有一个Compositor Frame队列不断的进行取出和绘制,输出的频率取决于 Compositor Frame的输入帧率和自身GL Frame的绘制频率。
我们再来说流程
我们来分别看具体的流程。
相关文档
相关源码
当我们从服务器上下载了一份HTML文档,第一步就是解析,HTML解析器接收标签和文本流(HTML是纯文本格式)把HTML文档解析成DOM树。DOM(Document Object Model)即文档对象模型,DOM及时页面的内部表示,也为JavaScript暴露了API接口(V8 DOM API),可以让JavaScript程序改变文档的结构、样式和内容。
它是一个树状结构,我们在后续的渲染流程中还会看到很多树形结构(例如布局树、属性树等)因为它们都是基于DOM树的结构(HTML的结构)而来的。
注:HTML文档中可能包含多棵DOM树,因为HTML支持自定义元素,这种树通常被称为Shadow Tree。
解析HTML生成DOM树流程如下:
DOM树(DOM Tree)作为后续绘制流程的基础, 还会基于它生产各种类型的树,具体说来,主要会经历如下转换:
对象转换
- DOM Tree -> Render Tree -> Layer Tree
- DOM node -> RenderObject -> RenderLayer
DOM Tree(节点是DOM node)
当加载一个HTML时,会对他进行解析,生成一棵DOM树。DOM树上的每一个节点都对应这网页里面的每一个元素,网页可以通过JavaScript操作这棵DOM树。
How Webkit Works
Render Tree(节点是RenderObject)
但是DOM树本身并不能直接用于排版和渲染,因此内核会生成Render Tree,它是DOM Tree和CSS相结合的产物,两者的节点几乎是一一对应的。Render Tree是排版引擎和渲染引擎之间的桥梁。
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
- Corresponds to a
对上面的流程不了解也没关系,我们下面会一一解释。
当DOM树生成以后,就需要为每个元素设置一个样式,有的样式只是会影响某个节点,有的样式会影响整个节点下面的整个DOM子树的渲染(例如,节点的旋转变换)。
相关源码
样式一般都是样式渲染器共同作用的结果,它有复杂的优先级语义和渲染过程,过程整体分为三步:
1 收集、划分和索引所有样式表中样式规则。
计算并应用了每个DOM节点的样式以后,就需要决定每个DOM节点的摆放位置。DOM节点都是基于盒模型摆放(一个矩形),布局就是计算这些盒子的坐标。
|-------------------------------------------------| | | | 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)。
在Layout操作完成以后,理论上就可以开始Paint操作了,但是我们之前提过,如果直接开始Paint操作,绘制整个界面,代价是非常昂贵的。因此便引入了一个图层合成加速的概念。
什么是图层合成加速(Compositing Layer)?
图层合成加速基本思想是把整个页面按照一定规则分成多个图层(就像Photoshop的图层那样),在渲染时只需要操作必要的图层,其他图层只需要参与合成就行了,以此提高渲染效率。完成这个工作的线程叫Compositor Thread,值得一提的是Compositor Thread还具备处理输入事件的能力(例如滚动事件),但是如果在JavaScript注册了事件监听,它会把输入事件转发给主线程处理。
具体说来是为某些RenderLayer拥有自己独立的缓存,它们被称为合成图层(Compositing Layer),内核会被这些RenderLayer创建对应的GraphicsLayer。
- 拥有自己的GraphicsLayer的RenderLayer在绘制的时候就会绘制在自己的缓存里面。
- 没有自己的GraphicsLayer的RenderLayer会向上查找父节点的GraphicsLayer,直到RootRenderLayer(它总是会有自己的GraphicsLayer)为止,然后绘制在有GraphicsLayer的父节点的缓存里。
这样就形成了与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
- 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的过程,如下所示:
什么是属性树?
在描述属性的层次结构这一块,之前的方式是使用图层树的方式,如果父图层具有矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),需要递归的应用到子节点,时间复杂度是O(图层数),这在极端情况下会有性能问题。
因此引入了属性树的概念,合成器提供了变换树、裁剪树、特效树等。每个图层都由若干节点id,分别对应不同属性树的矩阵变换节点、裁剪节点和特效节点。这样的时间复杂度就是O(要变化的节点),如下所示:
Prepaint的过程就是构建属性树的过程,如下所示:
创建完属性树(Prepaint)以后,就开始进入Paint阶段了。
相关文档
相关源码
Paint操作会将布局树(Layout Tree)中的节点(Layout Object)转换成绘制指令(例如绘制矩形、绘制字体、绘制颜色,这有点像绘制API的调用)的过程。然后把这些操作封装在Dsipaly Item中,这些Dsipaly Item存放在PaintArtifact中。PaintArtifact就是是Paint阶段的输出。
到目前为止,我们建立了可以重放的绘制操作列表,但没有执行真正的绘制操作。
注:重放(replay),现在图形系统大都采用recrod & replay机制,采集绘制指令与执行绘制指令相互分离,提高渲染效率
Paint操作最终会在Layout Tree的基础上生成一棵Paint Tree。
Paint阶段完成以后,进入Commit阶段。该阶段会更新图层和属性树的副本到合成器线程,以匹配提交的主线程状态。说的通俗点,就是把主线程里Paint阶段的数据(layers and properties)拷贝到合成器线程,供合成器线程使用。
但是合成器线程接收到数据后,并不会立即开始合成,而是进行图层分块,这里又涉及一个分块渲染的技术。
什么是分块渲染?
分块渲染(Tile Rendering)就是把网页的缓存分为一格一格的小块,通常为256x256或者512x512,然后分块进行渲染。
分块渲染主要基于两个方面的考虑:
- GPU合成通常是使用OpenGL ES贴图实现的,这时候的缓存实际就是纹理(GL Texture),很多GPU对纹理的大小是有限制的,比如长宽必须是2的幂次方,最大不能超过2048或者4096等。无法支持任意大小的缓存。
- 分块缓存,方便浏览器使用统一的缓冲池来管理缓存。缓冲池的小块缓存由所有WebView共用,打开网页的时候向缓冲池申请小块缓存,关闭网页是这些缓存被回收。
图块(tiling)是栅格化工作的基本单位。 栅格化会根据图块与可见视口的距离安排优先顺序进行栅格化。离得近的会被优先栅格化,离得远的会降级栅格化的优先级。这些图块拼接在一起,就形成了一个图层,如下所示:
图层分块完成以后,接着就会进行栅格化(Raster)。
什么是光栅化(栅格化)?
光栅化(Raterization),又称栅格化,它用于执行绘图指令生成像素的颜色值,光栅化策略分为两种:
- 同步光栅化:光栅化和合成在同一线程,或者通过线程同步的方式来保证光珊化和合成
- 直接光栅化:直接将所有可见图层的eDisplayList中的可见区域的绘图指令进行执行,在目标Surface的像素缓冲区上生成像素的颜色值。当然如果是完全的直接光栅化,就不涉及图层合并了,也就不需要后面的合成了。
- 间接光栅化:允许为指定图层分配额外的缓冲区,该图层的光栅化会先写入自身的像素缓冲区,渲染引擎再将这些图层的像素缓冲区(Android里可以调用View.setLayerType允许应用为View分配像素缓冲区)通过合成输出大欧姆表Surface的像素缓冲区。Android和Flutter主要使用直接光栅化的测量,同时也支持间接光栅化。
- 异步分块光栅化
1 老版本的调用采用这种方式,Skia运行在Renderer Process,负责产生GL指令,GPU有单独的GPU Process,这种模式下Skia无法直接进行渲染系统调用,在初始化Skia的时候回给它一个函数指针表(指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理),函数指针表转换为真正的OpenGL API的过程称为命令缓冲区(GpuChannelMsg_FlushCommandBuffers),
单独的GPU进程有利于隔离GL操作,提升稳定性和安全性,这种模式也称为沙箱机制(不安全的操作运行在独立的进程里)。
在Commit之后,Draw之前有一个Activate操作。Raster和Draw都发生在合成器线程里的Layer Tree上,但是我们知道Raster操作是异步的,有可能需要执行Draw操作的时候,Raster操作还没完成,这个时候就需要解决这个问题。
它将Layer树分为:
这个拷贝的过程就称为Activate,如下所示:
主线程的图层树由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的过程。
相关文档
Draw操作完成以后,就生成了Compositor Frame,它们会被输出到GPU Process。 它会从多个来源的Render Process接收Compositor Frame。
Viz是VIsual的缩写,它是Chromium整体架构转向服务化的一个重要组成部分,包含Compositing、GL、Hit Testing、Media、VR/AR等众多功能。
VIz也是双缓冲输出的,它会在后台缓冲区绘制draw quads,然后执行交换命令最终让它们显示在屏幕上。
什么是双缓冲机制?
在渲染的过程中,如果只对一块缓冲区进行读写,这样会导致一方面屏幕要等到去读,而GPU要等待去写,这样要造成性能低下。一个很自然的想法是把读写分开,分为:
- 前台缓冲区(Front Buffer):屏幕负责从前台缓冲区读取帧数据进行输出显示。
- 后台缓冲区(Back Buffer):GPU负责向后台缓冲区写入帧数据。
这两个缓冲区并不会直接进行数据拷贝(性能问题),而是在后台缓冲区写入完成,前台缓冲区读出完成,直接进行指针交换,前台变后台,后台变前台,那么什么时候进行交换呢,如果后台缓存区已经准备好,屏幕还没有处理完前台缓冲区,这样就会有问题,显然这个时候需要等屏幕处理完成。屏幕处理完成以后(扫描完屏幕),设备需要重新回到第一行开始新的刷新,这期间有个间隔(Vertical Blank Interval),这个时机就是进行交互的时机。这个操作也被称为垂直同步(VSync)。
到这里,整个渲染流程就结束了,前端的代码变成了可以与用户交互的像素点。