渲染

RenderObject树
RenderObject基础类

为了便于理解,引入书中代码段:

<html>
  <head></head>
  <body>
    <div>test</div>
    <canvas id="webgl" width="50" height="60"></canvas>
    <a href="http://test"></a>
    <img></img>
    <input type="button"></input>
    <select></select>
    <table width = "100" height="50">
      <tr>
        <td>a1</td>
      </tr>
    </table>
    <script type="text/javascript">
      var canvas = document.getElementById("webgl");
      var gl = canvas.getContext("experimental-webgl");
      if (!gl) {
        alert("No webgl context available");
        return;
      }
    </script>
  </body>
</html>

上述代码经过WebKit解释之后会生成DOM树,在DOM树构建完成之后会为DOM树节点构建RenderObject树,在DOM树中,有些元素是不可见的,例如表示文件头的“meta”节点,在最终的显示结果中,用户看不到它的存在,这样的节点称为“非可视化节点”,上述代码中的“head”,“script”就是这样的节点,另外的节点是用来展示网页内容的,如上述例子中的”body”,”div”,”span”,”canvas”,”img”等,这些节点可以显示一块区域,如文字,图片,2D图形等,这样的节点被称为“可视节点”,对于可视节点,webkit会为其建立相应的RenderObject对象,一个RenderObject对象保存了为绘制DOM节点所需要的各种信息,例如样式布局信息,这些RenderObject对象会构成RenderObject树,RenderObject树是基于DOM树建立起来的一棵新树,是为了布局计算和渲染等机制而构建的一种新的内部表示,RenderObject和DOM树不是一一对应的关系,满足一下三个规则的DOM树节点会创建一个RenderObject对象:

  • DOM树的document节点
  • DOM树中的可视化节点,例如html,body,div等
  • WebKit建立匿名的RenderObject节点,该节点不对应DOM树中的任何节点,而是WebKit处理上的需要,典型的例子为匿名的RenderBlock节点

WebKit在创建DOM树时也会创建RenderObject对象,如果DOM树被动态的加入新的节点,WebKit也可能会创建相应的RenderObject对象,下面是RenderObject对象被创建时所涉及到的主要类:
这里写图片描述
每个元素会递归调用“attach”函数,该函数检查Element对象是否需要创建RenderObject,如果需要,会使用NodeRenderingContext类来根据DOM节点的类型来创建对应的RenderObject节点。RenderObject树中的节点有很多类型,其中RenderObject类包含了RenderObject的主要虚函数,大致包含一下几块:

  • 为了遍历和修改RenderObject树而涉及到的众多函数,遍历操作函数如parent(),firstChild(),nextSibling(),previousSibling()等,修改操作函数如addChild(),removeChild()等
  • 用来计算布局和获取布局相关信息的函数,例如Layout(),style(),enchosingBox()等
  • 用来判断RenderObject对象属于哪种类型的子类,这里面有各式各样的类似”IsASubClass”的函数,这些函数可以知道它们的类型以作相应的转换
  • 跟RenderObject对象所在的RenderLayer对象相关的操作
  • 坐标和绘图相关的操作,WebKit使用这些操作让RenderObject对象 将内容绘制在传入的绘制结果对象中,例如paint(),repaint()等

RenderObject基类和它的主要子类:
这里写图片描述
对于匿名RenderObject对象由于它没有对应的DOM树中节点,WebKit统一使用Document节点来对应匿名对象。

RenderObject树

RenderObject对象构成了一棵树,该RenderObject树的创建主要由NodeRenderingContext类来负责,基本思路是WebKit检查该DOM节点是否需要创建RenderObject对象,如果需要,WebKit建立或者获取一个创建RenderObject对象的NodeRenderingContext对象,NodeRenderingContext对象会分析需要创建的RenderObject对象的父亲节点、兄弟节点等,设置这些信息后完成插入树的操作,RenderObject树和DOM树之间的区别以上述的代码为例做说明,具体如下:
这里写图片描述
图中虚线表示两种树节点的对应关系,其中HTMLDocument节点对应RenderView节点,RenderView节点是RenderObject树的根节点,另外,webkit没有为HTMLHeadElement节点(非可视化节点)创建RenderObject子类对象。

网页层次和RenderObject树
层次和RenderLayer对象

RenderLayer树是基于RenderObject树建立起来的一棵新树,并且RenderLayer节点和RenderObject节点不是一一对应的关系,当满足以下关系时RenderObject节点需要建立新的RenderLayer节点:

  • DOM树的Document节点对应的RenderView节点
  • DOM树中的Document的子女节点,也就是HTML节点对应的RenderBlock节点
  • 显示的指定CSS位置的RenderObject节点
  • 有透明效果的RenderObject节点
  • 节点有溢出,alpha,或者反射等效果的RenderObject节点
  • 使用Canvas2D和3D(WebGL)技术的RenderObject节点
  • Video节点对应的RenderObject节点

每个RenderLayer节点包含的RenderObject节点其实是一棵RenderObject子树,理想情况下每个RenderLayer对象都会有一个后端类,该后端类用来存储该RenderLayer对象绘制的结果,WebKit创建RenderObject树之后,WebKit也会创建RenderLayer树,某些RenderLayer节点有可能在执行JavaScript代码时或者更新页面的样式被创建,同RenderObject类不同,RenderLayer没有子类,它表示的是一个层次,没有子层次的说法。

构建RenderLayer树

根据前面创建RenderLayer的条件来判断一个RenderObject是否需要创建一个新的RenderLayer对象,并设置RenderLayer对象的父亲和兄弟关系即可,根据示例代码将RenderObject树根据规则创建对应的RenderLayer树,如下:
这里写图片描述

渲染方式
绘图上下文

在WebKit中绘图操作被定义了一个抽象层,被称为绘图上下文,所有的绘图操作都是在该上下文中来进行的,绘图上下文分成两种类型,第一种用来绘制2D图形的上下文,称为2D绘图上下文(GraphicsContext),第二种称为绘制3D图形上下文(GraphicsContext3D),这两种上下文都是抽象基类,只提供接口,具体绘制由不同的移植去实现。2D绘图上下文的具体作用是提供基本绘图单元的绘制接口以及设置绘图的样式,绘制接口包括点、线、图片、多边形、文字等,绘图样式包括颜色、线宽、字号大小、渐变等,RenderObject对象知道自己需要画什么样的点,什么样的图片,因此RenderObject对象调用绘图上下文的这些基本操作绘制显示结果。在现有的网页中,由于HTML5标准引入了很多新的技术,所以统一网页中可能既需要2D绘图上下文,也需要3D绘图上下文,对于2D绘图上下文来说,其平台相关的实现既可以使用CPU来完成2D相关的操作,也可以使用3D图形如Opengl来完成2D相关的操作,对于3D绘图上下文来说,因为性能问题WebKit通常使用3D图形接口(Opengl,Direct3D等)来实现。

渲染方式

在完成DOM构建之后,WebKit所要做的事情就是构建渲染的内部表示并使用图形库将这些模型绘制出来,关于网页的渲染方式,目前主要有两种,第一种是软件渲染,第二种是硬件加速渲染。每个RenderLayer对象可以被想象成图像中的一个层,各个层一同构成一个图像,其中软件绘图采用CPU,硬件加速绘图采用GPU来完成,并且在理想情况下每个层都有个绘制的存储区域,这个存储区域用来保存绘图的结果,最后将这些层的内容合并到同一个图像中,称之为合成,使用了合成技术的渲染称为合成话渲染。在RenderObject和RenderLayer树之后,WebKit的机制将内部模型转换为可视化结果分为两个阶段:每层的内容绘制及之后将这些绘图的结果合成一个图像。对于软件渲染,WebKit需要使用CPU来绘制每层的内容,软件渲染机制不需要合成阶段,因为在软件渲染的过程中通常渲染结果就是一个位图(Bitmap),绘制每一层的时候都使用该位图,区别在于绘制的位置可能不一样,每一层都是按照从后到前的顺序,下图是网页渲染的三种方式:
这里写图片描述
第一种方式使用软件渲染,在网页渲染过程中使用一个位图,也就是一块CPU使用的内存空间,后面两种方式使用了合成化的渲染技术,也就是使用GPU硬件加速合成这些网页层,合成的工作由GPU来完成,称为硬件加速合成,其中第二种方式有些特殊,其中一些层使用CPU来绘图,另外一些层使用GPU来做,对于使用CPU来绘图的层,该层的结果保存在CPU内存中,之后被传输到GPU的内存中,主要是为了后面的合成工作。对于这三种方式各有优缺点和使用场景,对于常见的2D绘图操作,使用GPU来绘图不一定比使用CPU绘图在性能上有优势,例如文字、线、点等,原因在于CPU使用的缓存机制有效减少了重复绘制的开销而且不需要GPU并行性,其次,GPU的内存资源相对与CPU的内存资源比较紧张,而且网页的分层使得GPU的内存使用相对比较多,下面列举这三种方式的特点:

  • 软件渲染很普遍,也是游览器最早使用的渲染方式,比较节省内存,特别是GPU内存,缺点在于只能处理2D方面的操作,对于没有复杂绘图或者多媒体的网页可采用软件渲染,但对于HTML5新的技术点软件渲染显得无计可施,一是因为能力不足,包括CSS3D、WebGL等,而是性能不好,例如视频、Canvas2D等,鉴于此软件渲染使用的场景越来越少,特别是移动领域。此外软件渲染相对于硬件加速渲染有另外一个优势,当网页中有一块小型区域需要更新的时候,软件渲染可能只需要更新一个极小区域,而硬件渲染可能需要重新绘制其中的一层或者多层,然后再合成这些层,因此硬件加速渲染的代价要高些
  • 对于硬件加速的合成化渲染来说,每个层的绘制和所有层的合成均使用GPU硬件来完成对使用3D绘图的操作来说特别合适,在这种方式下,RenderLayer树之后,WebKit和Chromium需要建立更多的内部表示,例如GraphicsLayer树、合成器中的层(如chromium中的CCLayer)等,这会消耗额外的内存空间,但一方面硬件加速机制能够支持现在所有的HTML5定义的2D或者3D绘图标准,另一方面如果需要更新某个层的一个区域,由于软件渲染没有为每一层提供后端存储,因而它需要将和这个区域有重叠部分的所有层次相关区域依次从后向前重新绘制一遍,而硬件渲染只需要重新绘制更新发生的层次
  • 软件绘制合成化渲染方式结合了前面两种方式的优点,因为很多网页既可能包含一些HTML的基本元素,也可能包含一些HTML5新功能,使用CPU绘制方式来绘制某些层,使用GPU来绘制其他一些层,这是基于性能和内存方面综合考虑的结果
WebKit软件渲染技术

在没有需要硬件加速的时候,WebKit使用软件渲染方式来完成页面的绘制工作,对于软件渲染主要关注两个方面,第一是RenderLayer树,第二是每个RenderLayer所包含的RenderObject子树,对于每个RenderObject对象,需要三个阶段绘制自己,第一阶段绘制该层中所有块的背景和边框,第二阶段是绘制浮动内容,第三阶段是前景(Foreground),包含内容部分,轮廓等部分,内嵌元素的背景、边框、前景等也是在该阶段绘制的,下图大致描述了RenderLayer层是如何绘制自己和子女的:
这里写图片描述

① 对于当前RenderLayer对象而言,WebKit首先绘制反射层(Reflectionlayer)
② 绘制RenderLayer对象对应的RenderObject节点的背景层(PaintBackground-ForFragments),调用“PaintPhaseBlockBackground”函数绘制该对象的背景层,不包括RenderObject的子女,“Fragments”的含义是可能绘制几个区域,因为网页需要更新的区域可能不是连续的,而是多个小块,因此WebKit绘制的时候需要更新这些非连续的区域就可
③图中“paintList”(z坐标为负数的子女层)阶段负责绘制Z坐标为负数的子女层,这是一个递归的过程,Z坐标为负数的层在当前RenderLayer对象层的后面,因此WebKit先绘制后面的层,然后当前RenderLayer对象层可能覆盖它们
④“PaintForegroundForFragments()”步奏相对较多,包括四个子阶段:第一进入“PaintPhaseChildBlockBackground”阶段,WebKit绘制RenderLayer节点对应的RenderObject节点的所有后代节点的背景,如果某个被选中的话,WebKit改为绘制选中区域的背景;第二进入“PaintPhaseFloat”绘制阶段,WebKit绘制浮动的元素;第三进入“PaintPhaseForeground”阶段,WebKit绘制RenderObject节点的内容和后代节点的内容;第四进入“PaintPhaseChildOutlilnes”绘制阶段,WebKit的目的是绘制所有后代节点的轮廓
⑤进入“PaintOutlineForFragments”步骤,WebKit在该步骤中绘制RenderLayer对象对应的RenderObject节点的轮廓
⑥进入绘制RendeLayer对象的子女步骤,WebKit首先绘制溢出的RenderLayer节点,之后绘制Z坐标为正数的RenderLayer节点
⑦进入该RenderObject节点的滤镜步骤

WebKit第一次绘制网页的时候,WebKit绘制的区域等同与可视区域的大小,在这之后,WebKit只是首先计算需要更新的区域,绘制同这些区域有交集的RenderObject节点,也就是说,如果更新区域跟某个RenderLayer节点有交集,WebKit会继续查找RenderLayer树中的特定一个或一些节点,而不是绘制整个RenderLayer对应的RenderObject子树,这与硬件加速不同,硬件加速渲染会绘制整个RenderLayer。WebKit软件渲染结果的存储方式在不同的平台有可能不一样,但基本上都是CPU内存的一块区域,多数情况下是一个位图(Bitmap),下面这个位图如何处理,如何跟之前的绘制结果合并,如何显示跟WebKit的不同移植有关。

Chromium多进程软件渲染技术

在Chromium中,需要将渲染结果从Render进程传递到Browser进程。
Render进程:WebKit的Chromium移植的接口类是RenderViewImpl,该类包含一个用于表示一个网页渲染结果的WebViewImpl类,此外RenderViewImpl也继承于RenderWidget类需要同Browser进程进行通信,RenderWidget类不仅负责调度页面渲染和页面更新到实际的WebViewImpl类等操作,而且还负责同Browser进程进行通信,另一个重要的设施是PlatformCanvas类,也就是SkiaCanvas(Skia是一个2D图形库),RenderObject树的实际绘制操作和绘制结果都是由该类来完成,它类似与2D绘图上下文和后端存储的结合体。
Browser进程:第一个设施是RenderWidgetHost类,它负责同Render进程进行通信,RenderWidgetHost类的作用是传递Browser进程中网页操作的请求给Render进程的RenderWidget类,并接收来自对方的请求,第二是BackingStore,它是一个后端的存储空间,它的大小通常就是网页可视区域的大小,该空间存储的数据就是页面的显示结果,BackingStore类的作用很明显,第一是保存当前的可视结果,所以Render进程的绘制工作不会影响该网页结果的显示,第二WebKit只需要绘制网页的变动部分,其余的部分保存在该后端存储空间,chromium只需要将网页的变动更新到该后端存储中即可。
这两个进程传递绘制结果是通过TransportDB类来完成,该类在Linux系统下其实是一个共享内存的实现,对Render进程来说,Skia Canvas把内容绘制到位图中,该位图的后端即是共享的CPU内存,当Browser进程接收到Render进程关于绘图完成的通知消息,Browser进程会把共享内存的内容复制到BackStoring对象中,然后释放共享内存,Browser进程中的后端存储最终会被绘制在显示窗口中,用户就能看到网页的结果。多进程软件渲染的过程大致如下:RenderWidget类接收到更新请求时,Chromium创建一个共享内存区域,然后Chromium创建skia的SkCanvas对象,并且RenderWidget会把实际的绘制工作派发给RenderObject树,具体来说,WebKit负责遍历RenderObject树,每个RenderObject节点根据需要来绘制自己和子女的内容并存储到目标存储空间,也就是SkCanvas对象所对应的共享内存的位图,最后,RenderWidgetHost类把位图复制到BackStoring对象的相应区域并调用“Paint”函数来把绘制结果绘制到窗口中。
下面列举两种会触发重新绘制网页中某些区域的请求:

  • 前端请求:该类型的请求从Browser进程发起,可能是游览器自身的一些需求,也可能是X窗口系统的请求,一个典型的例子就是用户因操作网页引起的变化
  • 后端请求:由于页面自身的逻辑而发起更新部分区域的请求,例如HTML元素或样式的改变、动画等,一个典型的例子是JavaScript代码每隔50ms便会更新网页的样式,这是样式更新会触发部分区域的重绘

下图给出了当有绘制或者更新某个区域的请求时,Chromium和WebKit是如何来处理这些请求的:
这里写图片描述
1. Render进程的消息循环调用处理“界面失效”的回调函数,该函数主要调用RenderWidget::DoDeferredUpdate来完成绘制请求
2. RenderWidget::DoDeferredUpdate函数首先调用Layout函数来触发检查是否需要重新计算布局和更新请求
3. RenderWidget类调用TransportDIB类来创建共享内存,内存大小为绘制区域的高*宽*4,同时调用skia图形库来创建一个SkCanvas对象,SkCanvas对象的绘制目标是一个使用共享内存存储的位图
4. 当渲染该页面的全部或部分时,ScrollView类请求按照从前到后的顺序遍历并绘制所有RenderLayer对象的内容到目标的位图中,WebKit绘制每个RenderLayer对象通过以下步骤来完成:首先WebKit计算重绘的区域是否和RenderLayer对象有重叠,如果有,WebKit要求绘制该层中的所有RenderObject对象
5. 绘制完成后,Render进程发送UpdateRect消息给Browser进程,Render进程同时返回以完成渲染过程,Browser进程收到消息后首先由BackingStoreManager类来获取或者创建BackingStoreX对象,BackingStoreX对象的大小与可视区域相同,包含整个网页的坐标信息,它根据UpdateRect的更新区域的位置信息将共享内存的内容绘制到自己对应的存储区域中

最后Browser进程将UpdateRect的回复消息发送到Render进程,Render进程知道Browser进程已经使用完该共享内存,可以进行回收利用等操作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值