微信公众号:爱写bugger的阿拉斯加
如有问题或建议,请后台留言,我会尽力解决你的问题。
前言
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和做的笔记。
而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。
书接上文 浏览器内核之 CSS 解释器和样式布局
本文剖析 WebKit 为网页渲染而构造的各种类型的内部结构表示,并介绍基本的网页软件渲染方式。
WebKit的布局计算使用 RenderObject 树并保存计算结果到 RenderObject 树中。RenderObject 树同其他树(如 RenderLayer 树等),构成了 WebKit 渲染的为要基础设施。
1. RenderObject 树
1.1.1 RenderObject 基础类
为了解释本章内容,我使用以下基础的前端代码来说明。
abc
email me
data
上面代码经过 WebKit 解释之后,生成 DOM 树,也很容易想象到。在 DOM 树构建完成之后,WebKit 会为 DOM 树节点构建 RenderObject 树。请听我娓娓道来。
- 不可视节点: 在 DOM 树中,该节点用户不可见,只是起到一些其他方面而不是显示内容的作用。如 “meta” 、“head”、“script” 节点等。
- 可视节点: 在 DOM 树中,该节点用户可见,可以显示一块区域,如文字、图片、2D 图形等。
对这些 “可视节点”,因为 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 类:
1.2 RenderObject 树
RenderObject 对象构成了一棵树。RenderObject 树的创建过程主要是由 NodeRenderingContext 类来负责。
思路:首先 WebKit 检查该 DOM 节点是否需要创建 RenderObject 对象。如果需要,WebKit 建立或者获取一个创建 RenderObject 对象的 NodeRenderingContext 对象,NodeRenderingContext 对象会分析需要创建的 RenderObject 对象的父亲节点、兄弟节点等,设置这些信息后完成插入树的动作。
根据上面的代码生成图 7-4 所示的 DOM 树和 RenderObject 树。
上图中使用虚线箭头表示两种树的节点对应关系,其中 HTMLDocument 节点对应 RenderView 节点,RenderView 节点是 RenderObject 树的根节点。另外,WebKit 没有 HTMLHeadElement 节点(非可视化元素),因为没有被创建 RenderObject 子类的对象。
1.2 网页层次和 RenderLayer 树
1.2.1 层次和 RenderLayer 对象
网页是有层次结构的,可以分层的,一是为了方便网页开发者开发网页并设置网页的层次,二是为了 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 类没有子类,它表示的是网页的一个层次,并没有 “子层次” 的说法。
1.2.2 构建 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.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 等技术)来实现。
1.3.2 渲染方式
在完成构建 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 的内存使用相对较多。
所以就目前的情况来看,三者的存在是有其合理性的。
1.4 WebKit 软件渲染技术
1.4.1 软件渲染过程
在很多情况下,也就是没有那些需要硬件加速内容的时候,WebKit 可以使用软件渲染技术来完成页面的绘制工作(除非读者强行打开硬件加速机制),目前用户浏览的很多门户网站、论坛网站、社交网站等所设计的网页,都是采用这项技术来完成页面的渲染。
而软件渲染过程需要关注两个方面,一是 RenderLayer 树,二是每个 RenderLayer 所包含的 RenderObject 树。WebKit 遍历 RenderLayer 树来绘制各个层。
对于每个 RenderObject 对象,需要三个阶段绘制自己。
一是绘制该层中所有块的背景和边框
二是绘制浮动内容
三是前景(Foreground),也就是内容部分、轮廓等部分。当然,每个阶段还可能会有一些子阶段。
值得指出的是,内嵌元素的背景、边框、前景等都是在第三阶段中被绘制的。
图 7-10 描述了一个 RenderLayer 层是如何绘制自己和子女的,这过程是一个递归过程。
且是一个大致的过程。
最开始的时候,也就是 WebKit 第一次绘制网页的时候,WebKit 绘制的区域等同于可视区域大小。而这在之后,WebKit 只是首先计算需要更新的区域,然后绘制同这些区域有交集的 RenderObject 节点。也就是说,如果更新区域跟某个 RenderLayer 节点有交集,WebKit 会断续查找 RenderLayer 树中包含的 RenderObject 子树中的特定一个或一些节点,而不是绘制整个 RenderLayer 对应的 RenderObject 子树。图 7-12 描述了在软件渲染过程中 WebKit 实际更新的区域,也就是之前描述软件渲染过程的生成结果。
1.4.2 Chromium 的多进程软件渲染技术
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 进程
最后
希望本文对你有点帮助。
下期分享 第八章 硬件加速机制 敬请期待。
对 全栈开发 有兴趣的朋友可以扫下方二维码关注我的公众号 —— 爱写bugger的阿拉斯加
分享 web 开发相关的技术文章,热点资源,全栈程序员的成长之路。
大家一起交流成长。