Webkit 渲染基础与硬件加速

Webkit 渲染基础与硬件加速

当浏览器加载一个 html 文件并对它进行解析完毕后,内核就会生成一个极为重要的数据结构即 DOM 树,树上每个节点都对应着网页里面的某一个元素,而且开发人员也能够经过 JavaScript 操做这棵 DOM 树动态改变它的结构,可是 DOM 树自己并不能直接用于排版和渲染。html

浏览器中页面的渲染过程能够简化为如下五个步骤:web

image_1c82b7ng61nkqhsktmm19t5hsnm.png-59.6kB

从 DOM 到 RenderObject

在 DOM 树构建完成以后,Webkit 所要作的事情就是为 DOM 树节点构建 RenderObject 树,一个 RenderObject 对象保存为绘制 DOM 节点所须要的各类信息,从如下这些规则出发会为 DOM 树节点建立 RenderObject 对象:canvas

  • DOM 树的 document 节点
  • DOM 树中的可视节点
  • 某些状况下 Webkit 须要创建匿名的 RenderObject 节点,这样的节点不对应于 DOM 树中的任何节点,而是 Webkit 处理上的须要,典型的例子就是匿名用于表示块元素的 RenderBlock 节点
  • 虽然 Javascript 没法访问影子节点,可是须要为其建立并渲染 RenderObject
Tip:因而可知,网上有人说浏览器渲染的步骤中包含“将 DOM 树和 CSSOM 树合并为 Render 树”的说法是有些问题的。 CSSOM(CSS 对象模型)是用于提供方法可让开发者自定义一些脚原本操做其样式状态的,它的思想是在 DOM 中的一些节点接口中加入获取和操做 CSS 属性或者接口的 Javascript 接口,便于 Javascript 能够动态操做 CSS 样式。

由此咱们能够知道 RenderObject 树和 DOM 树不是一一对应的!咱们能够简单的认为,RenderObject 是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出和渲染引擎的输入。当 Webkit 建立 RenderObject 对象以后,每一个对象是不知道本身的位置、大小等信息的,Webkit 根据框模型来计算它们的位置、大小等信息的过程称为布局计算后端

Tip:从整个网页的加载和渲染过程来看 ,CSS 解释器和规则匹配处于 DOM 树创建以后,RenderObject 树创建以前,CSS 解释器解释后的结果会保存起来,而后 RenderObject 树基于该结果来进行规范匹配和布局计算。

既然已经实现绘制每一个 DOM 节点的方法,那是否是能够开辟一段位图空间,而后 DFS 遍历这个 RenderObject 树执行绘制方法,就像“盖章”同样把每一个 RenderObject 的内容一个个的盖到“画布上”,是否是就足够完成绘制?数组

若是没有层叠上下文,到这儿就能够结束了!

其实是若是没有 Positioning,Clipping,Overflow Scroll,CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z Indexing etc. 到这儿就能够结束了……浏览器

从 RenderObject 到 RenderLayer

Webkit 会为网页的层次建立相应的 RenderLayer 对象,当某些类型的 RenderObject 的节点或者具备某些 CSS 样式的 RenderObject 节点出现的时候,Webkit 就会为这些节点建立 RenderLayer 对象,通常来讲某个 RenderObject 节点的后代都属于该节点的 RenderLayer,除非 Webkit 根据规则为某个后代 RenderObject 节点建立一个新的 RenderLayer 对象,如下是 RenderObject 节点须要创建新的 RenderLayer 节点的规则:缓存

  • DOM 树的 document 节点对应的 RenderView 节点
  • DOM 树中 document 的子女节点,即 html 节点对应的 RenderBlock 节点
  • 显示指定 CSS 位置的 RendrObject 节点
  • 有透明效果的 RenderObject 节点
  • 节点有溢出(overflow)、alpha 或者反射等效果的 RenderObject 节点
  • 适用 canvas 2d 或者 3d(WebGL)技术的 RenderObject 节点
  • video 节点对应的 RenderObject 节点
由此咱们能够知道,RenderLayer 节点和 RenderObject 节点不是一一对应的,而是一对多的关系。

具体来讲,根据建立 RenderLayer 的缘由不一样能够将其分为常见的 3 类:数据结构

NormalPaintLayer

  • 根元素(html)
  • 有明确的定位属性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小于 1)
  • 有 CSS 滤镜(fliter)
  • 有 CSS mask 属性
  • 有 CSS mix-blend-mode 属性(不为 normal)
  • 有 CSS transform 属性(不为 none)
  • backface-visibility 属性为 hidden
  • 有 CSS reflection 属性
  • 有 CSS column-count 属性(不为 auto)
  • 有 CSS column-width 属性(不为 auto)
  • 当前有对于 opacity、transform、fliter、backdrop-filter 的应用动画

OverflowClipPaintLayer

  • overflow 不为 visible

NoPaintLayer

  • 不须要 paint 的 RenderLayer:好比一个没有视觉属性(背景、颜色、阴影等)的空 div

上文中讲解的从 DOM 到 RenderObject 以及从 RenderObject 到 RenderLayer 能够概括以下图:多线程

image_1c82ab03815321e7p1ugc7m419.png-89.1kB

软件渲染和硬件加速渲染

在 Webkit 中绘图操做被定义为一个抽象层即绘图上下文,全部绘图操做都是在该上下文中进行,能够分为两种类型:2d 图形上下文和 3d 图形上下文。其中 2d 图形上下文的具体做用就是提供基本绘图单元的绘制接口以及设置绘图的样式,绘图接口包括画点、画线、画图片、画多边形、画文字 etc.,绘图样式包括颜色、线宽、字号大小、渐变 etc.,而RenderObject 对象知道本身须要画什么样的点,什么样的图片。3d 绘图上下文的主要用处是支持 CSS3D、WebGL etc.。架构

网页的渲染方式主要有两种:软件渲染和硬件加速渲染。每一个 RenderLyaer 对象均可以被想象成一个层,各个层一同构成一个图像,在渲染过程当中,每一个层对应网页中的一个或者一些可视元素,这些元素都绘制内容到该层上,若是这些绘图操做由 CPU 莱完成则称之为软件绘图,若是这些绘图操做由 GPU 来完成则称之为硬件加速绘图。理想状况下,每一个层都有绘制的存储区域来保存绘图的结果,最后须要将这些层的内容合并到同一个图像中的过程称为合成(compositing),使用合成技术的渲染叫作合成化渲染

对于软件渲染机制,Webkit 须要使用 CPU 来绘制每层的内容,然而该机制是没有合成阶段的:在软件渲染中一般其结果就是一个位图(Bitmap),绘制每一层时都使用同一个位图,区别在于绘制的位置看你不同,每一层都按照从后到前的顺序。而使用合成化的渲染技术,以使用软件绘图的合成化渲染为例,对于使用 CPU 绘制的层,其结果保存在 CPU 内存中,以后传输到 GPU 中进行合成。

对于常见的 2d 绘图操做,使用 GPU 来绘图不必定比使用 CPU 绘图在性能上有优点,缘由是 CPU 使用缓存机制有效减小重复绘制得开销,并且不须要 GPU 的并行,而且 GPU 的内存资源相对 CPU 的内存资源更加紧张。

什么是位图

image_1c84k71cv1uo61vhafc9b6b1kh7m.png-37.1kB

在绘制出一个图片咱们应该怎么作,显然首先是把这个图片表示为一种计算机能理解的数据结构:用一个二维数组,数组的每一个元素记录这个图片中的每个像素的具体颜色。因此浏览器能够用位图来记录它想在某个区域绘制的内容,绘制的过程也就是往数组中具体的下标里填写像素而已。

什么是纹理

纹理其实就是 GPU 中的位图,存储在 GPU video RAM 中。前面说的位图里的元素存什么咱们本身定义好就行(是用3字节存256位rgb仍是1个bit存黑白本身定义便可),但纹理是 GPU 专用的,须要有固定格式便于兼容与处理,因此一方面纹理的格式比较固定,如 R5G6B五、A4R4G4B4 等像素格式, 另一方面 GPU 对纹理的大小有限制,好比长/宽必须是2的幂次方,最大不能超过2048或者4096等。

什么是光栅化

image_1c84kb5l6vk81jmhr8a5d0a6013.png-115.4kB

在纹理里填充像素不是那么简单的本身去遍历位图里的每一个元素而后填写这个像素的颜色便可,光栅化的本质是坐标变换、几何离散化,而后再填充

同时,光栅化从早期的 Full-screen Rasterization 基本都进化到如今的 Tile-Based Rasterization,也就是不对整个图像作光栅化,而是把图像分块后再对每一个 tile 单独光栅化。光栅化完成后将像素填充进纹理,再将纹理上传至 GPU,
缘由一方面如上文所说,纹理大小有限制,即便整屏光栅化也是要填进小块小块的纹理中,不如事先根据纹理大小分块光栅化后再填充进纹理里;另外一方面是为了减小内存占用(整屏光栅化意味着须要准备更大的buffer空间)和下降整体延迟(分块栅格化意味着能够多线程并行处理)。

每秒60帧的动效里,每次变更都重绘整个位图是很恐怖的性能开销!

非合成加速的渲染架构,全部的 RenderLayer 都没有本身独立的缓存,它们都按照前后顺序被绘制到同一个缓存里面,因此只要这个 RenderLayer 触发重绘,变化区域的缓存就须要从新生成,此时不但须要绘制发生变化的 RenderLayer,跟变化区域(Damage Region)相交的其它 RenderLayer 也须要被绘制。

浏览器自己并不能直接改变屏幕的像素输出,它须要经过系统自己的 GUI Toolkit,因此通常来讲浏览器会将一个要显示的网页包装成一个 UI 组件,一般叫作 WebView,而后经过将 WebView 放置于应用的 UI 界面上,从而将网页显示在屏幕上。

默认的状况下 UI 组件没有本身独立的位图缓存,构成 UI 界面的全部 UI 组件都直接绘制在当前的窗口缓存上,因此 WebView 每次绘制就至关于将它在可见区域内的 RenderLayer/RenderObject 逐个绘制到窗口缓存上。上述的渲染方式有一个很严重的问题,用户拖动网页或者触发一个惯性滚动时,网页滑动的渲染性能会十分糟糕:这是由于即便网页只移动一个像素,整个 WebView 都须要从新绘制。

要提高网页滑屏的性能,一个简单的作法就是让 WebView 自己持有一块独立的缓存,而 WebView 的绘制就分红了两步: 1) 根据须要更新内部缓存,将网页内容绘制到内部缓存里面 2) 将内部缓存拷贝到窗口缓存上。第一步咱们一般称为绘制(Paint)或者光栅化(Rasterization),它将一些绘图指令转换成真正的像素颜色值,而第二步咱们通常称为合成(Composite),它负责缓存的拷贝,同时还可能包括位移(Translation),缩放(Scale),旋转(Rotation),Alpha 混合等操做。

从 RenderLayer 到 GraphicsLayer

在现实状况中,因为硬件能力和资源有限,为了节省 GPU 的内存资源,硬件加速机制在 RenderLayer 树创建以后须要作三件事情来完成网页的渲染:

  1. Webkit 决定将哪些 RendeLayer 对象组合在一块儿,造成一个由后端存储(通常指 GPU 内存)的新层,对于一个 RenderLayer 对象来讲,若是它没有后端存储的新层,那么就使用其父亲所使用的合成层
  2. 将每一个合成层包含的 RenderLayer 内容绘制在其后端存储中,这里的绘制能够是软件绘制,也能够是硬件加速绘制
  3. 由合成器将多个合成层合成起来,造成网页的最终可视化结果(实际就是一张图片)

一个 RenderLayer 对象若是须要后端存储,它会建立一个 RenderLayerBacking 对象,负责 RenderLayer 对象所须要的各类存储,每一个 RenderLayer 对象均可以建立本身的后端存储,然而不是全部 RenderLayer 对象都有本身的 RenderLayerBacking,若是一个 RenderLayer 对象被 Webkit 按照必定的规则建立后端存储,那么该层被称为合成层,后端存储可能须要管理多个存储空间,使用 GraphicsLayer 类来表示。

每一个 GraphicsLayer 都拥有一个 GraphicsContext,用于为该 GraphicsLayer 开辟一段位图,也就意味着每一个 GraphicsLayer 都拥有一个独立的位图,GraphicsLayer 负责将本身的 RenderLayer 及其所包含的 RenderObject 绘制到位图里,而后将位图做为纹理交给 GPU 进行合成。若是一个 RenderLayer 对象具备如下特征之一,那么它就是合成层:

  • RenderLayer 具备 CSS3D 属性或者 CSS 透视效果
  • RenderLayer 包含 video 节点对应的 RenderObject 节点
  • RenderLayer 包含使用 canvas 2d 或者 3d(WebGL)技术的 RenderObject 节点
  • RenderLayer 使用 CSS 透明效果的动画或者 CSS 变换动画
  • RenderLayer 使用硬件加速的 CSS Filters 技术
  • RenderLayer 使用裁剪或者反射属性,而且其后代包含合成层
  • RenderLayer 有一个 Z 坐标比本身小的兄弟节点,且该兄弟节点是一个合成层

直接缘由

  • 硬件加速的 iframe 元素(好比 iframe 嵌入的页面中有合成层)
  • video 元素
  • 覆盖在 video 元素上的视频控制栏
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 硬件加速的插件:好比 flash etc.
  • 在 DPI 较高的屏幕上 fix 定位的元素会自动地被提高到合成层中;但在 DPI 较低的设备上却并不是如此:由于这个渲染层的提高会使得字体渲染方式由子像素变为灰阶
  • 有 3D transform
  • backface-visibility 为 hidden
  • 对 opacity、transform、fliter、backdropfilter 应用 animation 或者 transition(须要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提高合成层也会失效)
  • will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等须要设置明确的定位属性:好比 relative etc.)

后代元素缘由

  • 有合成层后代同时自己有 transform、opactiy(小于 1)、mask、fliter、reflection 属性
  • 有合成层后代同时自己 overflow 不为 visible(若是自己是由于明确的定位因素产生的 SelfPaintingLayer,则须要 z-index 不为 auto)
  • 有合成层后代同时自己 fixed 定位
  • 有 3D transfrom 的合成层后代同时自己有 preserves-3d 属性
  • 有 3D transfrom 的合成层后代同时自己有 perspective 属性

重叠缘由

重叠或者说部分重叠在一个合成层之上,最多见和容易理解的就是元素的 border box(content + padding + border) 和合成层的有重叠,其余的还有一些不常见的状况,也算是同合成层重叠的条件以下:

  • filter 效果同合成层重叠
  • transform 变换后同合成层重叠
  • overflow scroll 状况下同合成层重叠

假设重叠在一个合成层之上,其实也比较好理解,好比一个元素的 CSS 动画效果在运行期间,元素有可能和其余元素发生重叠的状况,须要注意的是该缘由下,有一个很特殊的状况:若是合成层有内联的 transform 属性,会致使其兄弟渲染层假设重叠从而提高为合成层。

基本上常见的一些合成层的提高缘由如上所说,咱们会发现:因为重叠的缘由可能随随便便就会产生出大量合成层来,而每一个合成层都要消耗 CPU 和内存资源,岂不是严重影响页面性能?!

层压缩

这一点浏览器也考虑到,所以就有层压缩(Layer Squashing)的处理。若是多个渲染层同一个合成层重叠时,这些渲染层会被压缩到一个 GraphicsLayer 中,以防止因为重叠缘由致使可能出现的“层爆炸”。

固然,浏览器的自动层压缩也不是万能的,在不少特定状况下,浏览器是没法进行层压缩的,而这些状况也是咱们应该尽可能避免的(如下状况都是基于重叠缘由而言):

  • 没法进行会打破渲染顺序的压缩
  • video 元素的渲染层没法被压缩,同时也没法将别的渲染层压缩到 video 所在的合成层上
  • iframe、plugin 的渲染层没法被压缩,同时也没法将别的渲染层压缩到其所在的合成层上
  • 没法压缩有 reflection 属性的渲染层
  • 没法压缩有 blend mode 属性的渲染层
  • 当渲染层同合成层有不一样的裁剪容器时,该渲染层没法压缩
  • 相对于合成层滚动的渲染层没法被压缩
  • 当渲染层同合成层有不一样的具备 opacity 的祖先层(一个设置 opacity 且小于 1 一个没有设置 opacity 也算是不一样)时,该渲染层没法压缩
  • 当渲染层同合成层有不一样的具备 transform 的祖先层时,该渲染层没法压缩
  • 当渲染层同合成层有不一样的具备 filter 的祖先层时,该渲染层没法压缩
  • 当覆盖的合成层正在运行动画时,该渲染层没法压缩,只有在动画未开始或者运行完毕之后,该渲染层才能够被压缩

多线程

进一步来讲,浏览器还可使用多线程的渲染架构,将网页内容绘制到缓存的操做放到另一个独立的线程(绘制线程),而原来线程对 WebView 的绘制就只剩下缓存的拷贝(合成线程),绘制线程跟合成线程之间可使用同步,部分同步,彻底异步等做业模式,让浏览器能够在性能与效果之间根据须要进行选择。

Main thread or WebKit/Blink thread

内核线程 - 负责解析,排版,Render 树绘制,JavaScript 执行等任务,它有可能执行真正的网页内容的光栅化,也有可能只是纪录绘制指令,由独立的光栅化线程执行

Rasterize thread

光栅化线程 - 若是内核线程只负责将网页内容转换为绘图指令列表,则真正的光栅化(执行绘图指令计算出像素的颜色值)由独立的光栅化线程完成

Compositor thread

合成线程 - 负责将网页内部位图缓存/纹理输出到窗口的帧缓存,从而把网页显示在屏幕上,可是在使用 GPU 合成的状况下,也有可能只是产生 GL 绘图指令,而后将绘图指令的缓存发送给 GPU 线程执行

GPU thread

GPU 线程 - 若是使用 GPU 合成,则由 GPU 线程负责执行 GL 绘图指令,访问 GPU,可能跟合成线程是同一个线程,也有多是独立的线程(合成线程产生GL指令 GPU 线程执行)

Browser UI thread

浏览器 UI 线程,若是跟 GPU 线程不是同一个线程,则只负责外壳的绘制,若是跟 GPU 线程是同一个线程,则同时负责绘制外壳的UI界面,和网页的合成输出,到窗口帧缓存

image_1c84d20k41v5kia34ig1kk71aro9.png-146.9kB

重排&重绘

重排和重绘是老生常谈的东西,你们也应该很是熟悉,但在这里能够结合浏览器机制顺带讲一遍。

重排

首先,若是你改变一个影响元素布局信息的 CSS 样式:好比 width、height、left、top etc.(transform除外),那么浏览器会将当前的 Layout 标记为 dirty,这会使得浏览器在下一帧执行重排,由于元素的位置信息发生改变将可能会致使整个网页其余元素的位置状况都发生改变,因此须要执行 Layout 全局从新计算每一个元素的位置。

须要注意到,浏览器是在下一帧、下一次渲染的时候才重排,并非 JS 执行完这一行改变样式的语句以后当即重排,因此你能够在 JS 语句里写 100 行修改 CSS 的语句,可是只会在下一帧的时候重排一次。

会触发重排的属性和方法以下:

Element

clientHeight, clientWidth, clientTop, clientLeft, focus(), getBoundingClientRect(), getClientRects(), innerText, offsetHeight, offsetLeft, OffsetParent, offsetTop, offsetWidth, outerText, scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth

Frame, Image

height, width

Range

getBoundingClientRect(), getClientRects()

SVGLocatable

computeCTM(), getBBox()

SVGTextContent

getCharNumAtPosition(), getComputedTextLength(), getEndPositionOfChar(), getExtentOfChar(), getNumberOfChars(), getRotationOfChar(), getStartPositionOfChar(), getSubStringLength(), selectSubString()

SVGUse

instanceRoot

window

getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY, webkitConvertPointFromNodeToPage(), webkitConvertPointFromPageToNode()

强制重排

若是你在当前 Layout 被标记为 dirty 的状况下访问 offsetTop、scrollHeight 等属性,那么浏览器会当即从新 Layout,计算出此时元素正确的位置信息,以保证你在 JS 里获取到的 offsetTop、scrollHeight 等是正确的。

这一过程被称为强制重排 Force Layout,强制浏览器将原本在渲染流程中才执行的 Layout 过程提早至 JS 执行过程当中,每次当咱们在 Layout 为 dirty 时访问会触发重排的属性都会 Force Layout,这会极大延缓 JS 的执行效率

另外,每次重排或者强制重排后,当前 Layout 就再也不 dirty,这时再访问 offsetWidth 之类的属性并不会再触发重排。

重绘

重绘也是类似的,一旦你更改某个元素的会触发重绘的样式,那么浏览器就会在下一帧的渲染步骤中进行重绘(也即一些介绍重绘机制中说的 invalidating),JS 更改样式致使某一片区域的样式做废,从而在一下帧中重绘 invalidating 的区域。

可是!有一个很是关键的行为就是:重绘是以合成层为单位的,也即 invalidating 的既不是整个文档也不是单个元素,而是这个元素所在的合成层。固然这也是将渲染过程拆分为 Paint 和 Compositing 的初衷之一:

Since painting of the layers is decoupled from compositing, invalidating one of these layers only results in repainting the contents of that layer alone and recompositing.

使用 transform 或者 opacity 来实现动画效果

修改一些 CSS 属性如 width、float、border、position、font-size、text-align、overflow-y etc. 会触发重排、重绘和合成,修改另外一些属性如 color、background-color、visibility、text-decoration etc. 则不会触发重排,只会重绘和合成。

image_1c84lrbps1ntk1hjl10eg9371sfp1g.png-49.3kB

接下来不少文章里就会说,修改 opacity、transform 这两个属性仅仅会触发合成,不会触发重绘,因此必定要用这两个属性来实现动画,没有重绘重排,效率很高…… 然而事实并非这样,只有一个元素在被提高为合成层以后,上述状况才成立

最后一句话:合成层提高并不是银弹!