微信公众号:爱写bugger的阿拉斯加 若有问题或建议,请后台留言,我会尽力解决你的问题。html
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和作的笔记。前端
而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。git
书接上文 浏览器内核之 CSS 解释器和样式布局github
本文剖析 WebKit 为网页渲染而构造的各类类型的内部结构表示,并介绍基本的网页软件渲染方式。web
WebKit的布局计算使用 RenderObject 树并保存计算结果到 RenderObject 树。 RenderObject 树同其余树(如 RenderLayer 树等),构成了 WebKit 渲染的为要基础设施。canvas
为了解释本章内容,我使用如下基础的前端代码来讲明。后端
<!DOCTYPE html>
<html lang="en">
<head></head>
<body>
<div>abc</div>
<canvas id="webg1" width="80" height="80"></canvas>
<a href="mailto:joe@example.com?subject=feedback">email me</a>
<img src="" alt="">
<input type="button" name="" />
<select name="" multiple>
<option value="">option</option>
</select>
<table>
<tr>
<td>data</td>
</tr>
</table>
<script>
var canvas = document.getElementById('webg1')
var g1 = canvas.getContext('experimental-webg1')
if (g1) {
alert("There's no WebGl context available. ")
return
}
</script>
</body>
</html>
复制代码
上面代码通过 WebKit 解释以后,生成 DOM 树,也很容易想象到。在 DOM 树构建完成以后,WebKit 会为 DOM 树节点构建 RenderObject 树。请听我娓娓道来。浏览器
不可视节点: 在 DOM 树中,该节点用户不可见,只是起到一些其余方面而不是显示内容的做用。如 “meta” 、“head”、“script” 节点等。缓存
可视节点: 在 DOM 树中,该节点用户可见,能够显示一块区域,如文字、图片、2D 图形等。bash
对这些 “可视节点”,由于 WebKit 须要将它们的内容绘制到最终的网页结果中,因此 WebKit 会为它们创建相应的 RenderObject 对象。
一个 RenderObject 对象保存了为绘制 DOM 节点所须要的各类信息,例如样式布局信息,通过 WebKit 的处理以后,RenderObject 对象知道如何绘制本身。
这些 RenderObject 对象同 DOM 的节点对象相似,它们也构成一棵树,在这里咱们称这为 RenderObject 树。RenderObject 树是基于 DOM 树创建起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示。RenderObject 树节点和 DOM 节点不是一一对应关系。
是根据如下三条规则出发为 DOM 节点建立一个 RenderObject 对象的:
DOM 树的 document 节点。
DOM 树中的可视化节点,例如 html 、body、div 等。而 WebKit 不会为非可视化节点建立 RenderObject 节点。
某些状况下 WebKit 须要创建匿名的 RenderObject 节点,该节点不对应于 DOM 树中的任何节点,而是 WebKit 处理上的须要,典型的例子就是匿名的 RenderBlock 节点。
WebKit 处理影子 DOM 没有什么特别的不一样,虽然 JavaScript 代码无法访问影子 DOM ,可是 WebKit 须要建立并渲染 RenderObject。
WebKit 在建立 DOM 树的同时也建立 RenderObject 对象。若是 DOM 树被动态加入了新节点,WebKit 也可能建立相应的 RenderObject 对象。
每一个 Element 对象都会递归调用 “attach” 函数,该函数检查 Element 对象是否须要建立 RenderObject 对象,若是须要,该函数会使用 NodeRenderingContext 类来根据 DOM 节点的类型来建立对应的 RenderObject 节点。
DOM 树中,元素节点包含不少类型。同 DOM 树同样,RenderObject 树中的节点也有不少类型。
而图中间的 RenderObject 类还包含了 RenderObject 的主要虚函数,还能够分为如下 5 类:
RenderObject 对象构成了一棵树。RenderObject 树的建立过程主要是由 NodeRenderingContext 类来负责。
思路:首先 WebKit 检查该 DOM 节点是否须要建立 RenderObject 对象。若是须要,WebKit 创建或者获取一个建立 RenderObject 对象的 NodeRenderingContext 对象,NodeRenderingContext 对象会分析须要建立的 RenderObject 对象的父亲节点、兄弟节点等,设置这些信息后完成插入树的动做。
根据上面的代码生成图 7-4 所示的 DOM 树和 RenderObject 树。
上图中使用虚线箭头表示两种树的节点对应关系,其中 HTMLDocument 节点对应 RenderView 节点,RenderView 节点是 RenderObject 树的根节点。另外,WebKit 没有 HTMLHeadElement 节点(非可视化元素),由于没有被建立 RenderObject 子类的对象。
网页是有层次结构的,能够分层的,一是为了方便网页开发者开发网页并设置网页的层次,二是为了 WebKit 处理上的便利,为了简化渲染的逻辑。
WebKit 会为网页的层次建立相应的 RenderLayer 对象。当某些类型RenderLayer 的节点或者具备某些 CSS 样式的 RenderLayer 节点出现的时候,WebKit 就会为这些节点建立 RenderLayer 对象。通常来讲,某个 RenderObject 节点的后代都属于该节点,除非 WebKit 根据规则为某个后代 RenderObject 节点建立了一个新的 RenderLayer 对象。
**RenderLayer 树是基于 RenderObject 树创建起来的一棵新树。 **
并且有结论:RenderLayer 节点和 RenderObject 节点不是一一对应关系,而是一对多的关系。
RenderObject 节点须要创建新的 RenderLayer 节点,是根据如下基本规则:
DOM 树的 Document 节点对应的 RenderView 节点。
DOM 树中的 Document 的子女节点,也就是 HTML 节点对应的 RenderBlock 节点。
显式的指定 CSS 位置的 RenderObject 节点。
有透明效果的 RenderObject 节点。
节点有溢出(Overflow)、alpha 或者反射等效果的 RenderObject 节点。
使用 Canvas 2D 和 3D(WebGl)技术的 RenderObject 节点。
Video 节点对应的 RenderObject 节点。
除了根节点也就是 RenderLayer 节点,一个 RenderLayer 节点的父亲就是该RenderLayer 节点对应的 RenderObject 节点的祖先链中最近的祖先,而且祖先所在的RenderLayer 节点同该节点的 RenderLayer 节点不一样。基于这一原理,这些 RenderLayer 节点也构成了一棵 RenderLayer 树。
每一个 RenderLayer 节点包含的 RenderObject 节点实际上是一棵 RenderLayer 子树。 理想状况下,每一个 RenderLayer 对象都会有一个后端类,该后端类用来存储该 RenderLayer 对象绘制的结果。实际状况中则比较复杂,在不一样的渲染模式下,不一样 WebKit 的移植中,状况都不同。RenderLayer 节点的使用能够有效地减小网页结构的复杂程度,并在不少状况下可以减小从新渲染的开销。
在 WebKit 建立 RenderObject 树以后,WebKit 也会建立 RenderLayer 树。固然,某些 RenderLayer 节点也有可能在执行 JavaScript 代码时或者更新页面的样式被建立。同 RenderObject 类不一样的是,RenderLayer 类没有子类,它表示的是网页的一个层次,并无 “子层次” 的说法。
构建 RenderLayer 树的过程很是简单,甚至比构建 RenderObject 树还要简单。根据前面所述的条件来判断一个 RenderObject 节点是否须要创建一个新的 RenderLayer 对象,并设置 RenderLayer 对象的父亲和兄弟关系便可。
根据刚开始的代码,WebKit 中的 RenderObject 树表示如图 7-5 左边所示的结构。右边描述是就是 WebKit 所生成的对应的 RenderLayer 树。根据 RenderLayer 对象建立的条件来看,该示例代码的 RenderLayer 树应该包含三个 RenderLayer 节点——根节点和它的子女,以及叶子节点。
根据上面最初的代码,生成 图 7-6 ,表示 WebKit 内部表示的具体结构 RenderObject 树、RenderLayer 树和布局信息的中大小和位置信息。
首先,图中 ‘layer at(x,x)’ 表示的是不一样的 RenderLayer 节点,下面全部 RenderObject 子类的对象均属于该 RenderLayer 对象。
以第一个 RenderLayer 节点为例,它对应于 DOM 树中的 Document 节点。后面的 “(0,0)” 表示该节点在网页坐标系中的位置,最后的 “1028X683” 表示该节点的大小,第一层包含的 RenderView 节点后面的信息也是一样的意思。
其次,看第二个 layer ,其包含了 HTML 中的绝大部分元素。这里面有三点须要解释一下:
一,“head” 元素没有相应的 RenderObject 对象,由于 “head” 是一个不可视的元素;
二,“canvas” 元素并不在第二个 layer 中,而是在第三个 layer(RenderHTMLCanvas)中,虽然该元素仍然是 RenderBody 节点的子女;
三,该 layer 层中包含一个匿名(Anonymous)的 RenderBlock 节点,该匿名节点包含了 RenderText 和 RenderLnline 等子节点。
再次,第三个 layer 层,由于 JavaScript 代码为 “canvas” 元素建立了一个 WebGl 的 3D 绘图上下文对象,WebKit 须要从新生成一个新的 RenderLayer 对象。
最后,来讲明一下三个层次的建立时间。在建立 DOM 树以后,WebKit 会接着建立第一个和第二个 layer 层。可是,第三个 RenderLayer 对象是在 WebKit 执行 JavaScript 代码时才被建立的,这是由于 WebKit 须要检查出 JavaScript 代码是否为 “canvas” 确实建立了 3D 绘图上下文,而不是在遇到 ”canvas“ 元素时建立新的 RenderLayer 对象。
1.3.1 绘图上下文(GraphicsContext)
RenderObject 对象是用什么来绘制内容的呢?在 WebKit 中,绘图操做被定义了一个抽象层,就是绘图上下文,全部绘图的操做都是在该上下文中来进行的。
绘图上下文能够分红两种类型:一,是 2D 图形上下文(GraphicsContext),用来绘制 2D 图形的的上下文;二是 3D 绘图上下文,是用来绘制 3D 图形的上下文。
这两种上下文都是抽象基类,它们只提供接口,由于 WebKit 须要支持不一样的移植。而这两个抽象基类的具体绘制则由不一样的移植提供不一样的实现,每一个移植使用的实际绘图类很是不同,依赖的图形率也不同。
2D 绘图上下文的具体做用就是提供基本绘图单元的绘制接口以及设置绘图的样式。绘图接口包括画点,画线、画图片、画多边形、画文字等,绘图样式包括颜色、线宽、字号大小、渐变等。RenderObject 对象知道本身须要画什么样的点,什么样的图片,因此 RenderObject 对象调用绘图上下文的这些基本操做就是绘制实际的显示结果。关系看 图 7-8 。
关于 3D 绘图上下文,它的主要用处是支持 CSS3D、WebGL 等。
在现有的网页中,因为 HTML5 标准引入了不少新的技术,因此同一网页中可能既须要使用 2D 绘图上下文,也须要使用 3D 绘图上下文。对于 2D 绘图上下文来讲,其平台相关的实现既可使用 CPU 来完成 2D 相关的操做,也可使用 3D 图形接口(如 OpenGL)来完成 2D 的操做。而对于 3D 绘图上下文来讲,由于性能问题,WebKit 的移植一般都是使用 3D 图形接口(如 OpenGL 或者 Direct3D 等技术)来实现。
在完成构建 DOM 树以后,WebKit 会构建渲染的内部表示并使用图形库将这些模型绘制出来。 网页的渲染方式,有三种方式,一是软件渲染,二是硬件加速渲染,三是混合模式。
每一个 RenderLayer 对象能够被想象成图像中的一个层,各个层一同构成了一个图像。在渲染的过程当中,浏览器也能够做一样的理解。每一个层对应网页中的一个或者一些可视元素,这些元素都绘制内容到该层上,在本书中,一概把这一过程称为绘图操做。
若是绘图操做使用 CPU 来完成,称之为软件绘图。若是绘图操做由 GPU 来完成,称之为 GPU 硬件加速绘图。理想状况下,每一个层都有个绘制的存储区域,这个存储区域用来保存绘图的结果。最后,须要将这些层的内容合并到同一个图像之中,本书称之为合成(Compositing),使用了合成技术的渲染称之为合成化渲染。
因此在 RenderObject 树和 RenderLayer 树以后,WebKit 的机制操做将内部模型转换成可视的结果分为两个阶段:每层的内部进行绘图工做及以后将这些绘图的结果合成一个图像。对于软件渲染机制,WebKit 须要使用 CPU 来绘制每层的内容,而软件渲染机制是没有合成阶段的,由于没有必要,在软件渲染中,一般渲染的结果就是一个位图(Bitmap),绘制每一层的时候都使用该位图,区别在于绘制的位置可能不同,固然每一层都按照从后到前的顺序。固然,你也能够为每层分配一个位图,问题是,一个位图就已经可以解决全部的问题。
从上图可能看到,软件渲染中网页使用的一个位图,实际上就是一块 CPU 使用的内存空间。而图中的第二和第三种方式,都是使用了合成化的渲染技术,也就是使用 GPU 硬件来加速合成这些网页层,合成的工做都是由 GPU 来作,称为硬件加速合成(Accelerated Compositing)。可是,对于每一个层,这两种方式有不一样的选择,其中某些层,第二种方式使用 CPU 来绘图,另一些层使用 GPU 来绘图。对于使用 CPU 来绘图的层,该层的结果首先固然保存在 CPU 内存中,以后被传输到 GPU 的内存中,这主要是为了后面的合成工做。第三种渲染方式使用使用 GPU 来绘制全部合成层。第二和第三种方式其实都属于硬件加速渲染方式。前面的这些描述,是把 RenderLayer 对象和实际的存储空间对应,现实中不是这样的,这只是理想的状况。
渲染的基本知识:
首先,对于常见的 2D 绘图操做,使用 GPU 来绘图不必定比使用 CPU 绘图在性能上有优点,例如绘制文字、点、线等,缘由是 CPU 的使用缓存机制有效减小了重复绘制的开销并且不须要 GPU 并行性。
其次,GPU 的内存资源相对 CPU 的内存资源来讲比较紧张,并且网页的分层使得 GPU 的内存使用相对较多。
因此就目前的状况来看,三者的存在是有其合理性的。
在不少状况下,也就是没有那些须要硬件加速内容的时候,WebKit 可使用软件渲染技术来完成页面的绘制工做(除非读者强行打开硬件加速机制),目前用户浏览的不少门户网站、论坛网站、社交网站等所设计的网页,都是采用这项技术来完成页面的渲染。
而软件渲染过程须要关注两个方面,一是 RenderLayer 树,二是每一个 RenderLayer 所包含的 RenderObject 树。WebKit 遍历 RenderLayer 树来绘制各个层。
对于每一个 RenderObject 对象,须要三个阶段绘制本身。
一是绘制该层中全部块的背景和边框
二是绘制浮动内容
三是前景(Foreground),也就是内容部分、轮廓等部分。固然,每一个阶段还可能会有一些子阶段。
值得指出的是,内嵌元素的背景、边框、前景等都是在第三阶段中被绘制的。
图 7-10 描述了一个 RenderLayer 层是如何绘制本身和子女的,这过程是一个递归过程。 且是一个大体的过程。
最开始的时候,也就是 WebKit 第一次绘制网页的时候,WebKit 绘制的区域等同于可视区域大小。而这在以后,WebKit 只是首先计算须要更新的区域,而后绘制同这些区域有交集的 RenderObject 节点。也就是说,若是更新区域跟某个 RenderLayer 节点有交集,WebKit 会断续查找 RenderLayer 树中包含的 RenderObject 子树中的特定一个或一些节点,而不是绘制整个 RenderLayer 对应的 RenderObject 子树。图 7-12 描述了在软件渲染过程当中 WebKit 实际更新的区域,也就是以前描述软件渲染过程的生成结果。
Chromium 的设计与实现中,由于引入了多进程模型,因此 Chromium 须要将渲染结果从 Renderer 进程传递到 Browser 进程。
先是 Renderer 进程。
WebKit 的 Chromium 移植的接口类是 RenderViewImpl,该类包含一个用于表示一个网页的渲染结果的 WebViewImpl 类。其实 RenderViewImpl 类还有一个做用就是同 Browser 进程通讯,因此它继承自 RenderWidget 类。RenderWidget 类不只负责页面渲染和页面更新到实际的 WebViewImpl 类等操做,并且它负责同 Browser 进程的通讯。
另一个重要的设施是 PlatformCanvas 类,也就是 SkiaCanvas(Skia j是一个 2D 图形库),RenderObject 树的实际绘制操做和绘制结果都由该类来完成,它相似于 2D 绘图上下文和后端存储的结合体。
再次是 Browser 进程。
第一个设施就是 RenderWidgetHost 类,同样的必不可少,它负责同 Renderer 进程的通讯。RenderWidgetHost 类的做用是传递 Browser 进程中网页操做的请求给 Renderer 进程的 RenderWidget 类,并接收自对方的请求。
第二个是 BackingStore 类,顾名思义,它就是一个后端的存储空间,它的大小一般就是网页可视区域的大小,该空间存储的数据就是页面的显示结果。
BackingStore 类的做用很明显,第一,它保存当前的可视结果,因此 Renderer 进程的绘制工做不会影响该网页结果的显示;第二,WebKit 只须要绘制网页的变更部分,由于其他的部分保存在该后端存储空间,Chromium 只须要将网页的变更更新到该后端存储中便可。
最后是两个进程传递信息和绘制内容的实现过程。
两个进程传递绘制结果是经过 TransportDIDB 类来完成,该类在 Linux 系统下实际上是一个共享内存的实现。对 Renderer 进程来讲,Skia Canvvas 把内容绘制到位图中,该位图的后端便是共享的 CPU 内存。当 Browser 进程接收到 Renderer 进程关于绘制完成的通知信息,Browser 进程会把共享内存的内容复制到 BackingStore 对象中,而后释放共享内存。
根据上面的组成部分,一个多进程软件渲染过程大体以下:
RenderWidget 类接收到更新请求时,Chromium 建立一个共享内存区域。而后 Chromium 建立 Skia 的 SkCanvas 对象,而且 RenderWidget 会把实际绘制的工做派发给 RenderObject 树。具体来说,WebKit 负责遍历 RenderObject 树,每一个 RenderObject 节点根据须要来绘制本身和子女的内容并存储到目标存储空间,也就是 SkCanvas 对象所对应的共享内存的位图中。最后,RenderWidgetHost 类把位图复制到 BackingStore 对象相应区域中,并调用 ”Pint“ 函数来把结果绘制到窗口中。
两种会触发从新绘制网页某些区域的请求:
前端请求: 该类型的请求从 Browser 进程发起的请求,多是浏览器自身的一些需求,也有多是 X 窗口系统(或者其余窗口系统)的请求。一个典型的例子就是用户因操做网页引发的变化。
后端请求: 因为页面自制的逻辑而发起更新部分区域的请求,例如 HTML 元素或者样式的改变、动画等。一个典型的例子是 JavaScript 代码每隔 50ms 便会更新网页样式,这时样式更新会触发部分区域的重绘。
Renderer 进程的消息循环(Message Loop)调用处理 ”界面失效“的回调函数,该函数主要调用 RenderWidget::DoDeferredUpdate 来完成绘制请求。
RenderWidget::DoDeferredUpdate 函数首先调用 Layout 函数来触发检查是否有须要从新计算的布局和更新请求。
RenderWidget 类调用 TransportDIB 类来建立共享内存,内存大小为绘制区域的 高X宽X4 ,同时调用 Skia 图形库来建立一个 SkCanvas 对象。SKCanvas cf 对象的绘制目标是一个使用共享内存存储的位图。
当渲染该页面的所有或者部分时,ScrollView 类请求按照从前到后的顺序遍历并绘制全部 RenderLayer 对象的内容到目标的位图中。WebKit 绘制每一个 RenderLay 对象经过如下步骤来完成:首先 WebKit 计算重绘的区域是否呼 RenderLyaer 对象有重叠,若是有,WebKit 要求绘制该层中的所在 RenderObject 对象。
绘制完成后,Renderer 进程发送 UpdateRect 的消息给 Browser 进程,Renderer 进程同时返回以完成渲染的过程。Browser 进程接收到消息后首先由 BackingStoreManagere 类来获取或者建立 BackingStoreX 对象(在Linux 平台上),BackingStoreX 对象的大小与可视区域相同,包含整个网页的坐标信息,它根据 UpdateRect 的更新区域的位置信息将共享内存的内容绘制到本身的对应存储区域中。
最后 Browser 进程将 UpdataRect 的回复消息发送到 Renderer 进程,这是由于 Renderer 进程知道 Browser 进程已经使用完该共享内存,可能进行回收利用等操做,就样就完成了整个过程。
一个 RenderObject 对象保存了为绘制 DOM 节点所须要的各类信息
RenderObject 树是基于 DOM 树创建起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示。RenderObject 树节点和 DOM 节点不是一一对应关系
WebKit 在建立 DOM 树的同时也建立 RenderObject 对象。若是 DOM 树被动态加入了新节点,WebKit 也可能建立相应的 RenderObject 对象。
网页是有层次结构的,能够分层的,RenderLayer 树是基于 RenderObject 树创建起来的一棵新树。
RenderObject 对象是用绘图上下文来绘制内容的,全部绘图的操做都是在该上下文中来进行的。
Chromium 须要将渲染结果从 Renderer 进程传递到 Browser 进程
但愿本文对你有点帮助。
下期分享 第八章 硬件加速机制 敬请期待。