渲染基础

135 阅读19分钟

渲染基础

webkit的布局计算使用RenderObject树并保存计算结果到RenderObject树中。RenderObject树同其他树,构成了webkit渲染的主要基础设施。

RenderObject树

RenderObject基础类

对于可视节点,webkit需要将它们的内容绘制到最终的网页结果中,所以webkit会为其建立相应的RenderObject对象。

一个RenderObject对象保存了绘制DOM节点需要的各种信息,如样式布局信息。经过webkit处理后,RenderObject对象知道如何绘制自己。

RenderObject对象构成RenderObject树。这棵树是基于DOM树建立的,是为了布局计算和渲染等机制而构建的内部表示。

但RenderObject树节点和DOM树节点不是一一对应关系。为DOM树节点创建一个RenderObject树节点,主要遵循以下规则:

    • DOM树的document节点。
    • DOM树的可视节点。如html、body、div。(非可视节点不创建RenderObject节点)
    • 某些情况下,webkit需要建立匿名的RenderObject节点。
      • 该节点不对应于DOM树中的任何节点,只是出于webkit处理上的需要。如,匿名的RenderBlock节点。

前面提到的影子DOM也遵循以上规则。(JavaScript无法访问影子DOM,但webkit仍需要创建并渲染RenderObject。)

webkit在创建DOM树的同时,也创建RenderObject对象。如DOM树动态加入新节点,webkit也可能创建对应的RenderObject对象。图7-1描述了创建RenderObject对象时涉及的主要类。

每个element对象都会递归调用attach函数。该函数检查element对象是否需要创建RenderObject。

如果需要,该函数会调用BodeRenderingContext类,根据DOM节点类型,来创建对应的RenderObject节点。

DOM树中,元素节点包含很多类型。同样,RenderObject树中节点也有很多类型。图7-2描述了RenderObject类及其子类。

图中间是RenderObject类,包含了RenderObject的主要虚函数,大致可以分为以下几类:

  • 为了遍历和修改RenderObject对象的函数。
    • 遍历:parent()、firstChild()、nextSibling()、previousSibling()等
    • 修改:addChild()、removeChild()等。
  • 用来计算布局和获取布局相关信息的函数
    • 如,layout()、style()、enclosingBox()等
  • 用来判断该RenderObject对象属于哪种类型的子类。
    • 类似IsASubClass的函数。
  • 跟RenderLayer对象相关的操作。
  • 坐标和绘图相关的操作。
    • webkit使用这些操作将内容绘制在传入的对象中。
    • 如,paint()、repaint()等

RenderBoxModelObject类:描述跟CSS中框模型相关联类的基类。子类如,RenderInline类(div:inline-box)和RenderBox类。

RenderBox类:使用箱子模型的类,包括了外边距、边框、内边距和内容等信息。

RenderBlock类用来表示块元素。

RenderBlock的子女必须都是内嵌元素,或都是非内嵌元素,所以,为了处理上的方便,webkit某些情况下需要建立匿名的RenderBlock对象。

当RenderBlock对象包含两种元素时,

    • webkit会为相邻的内嵌元素创建一个块节点,即RenderBlock对象。
    • 然后设置该对象为原先内嵌元素父亲的子女。
    • 最后设置这些内嵌元素为RenderBlock对象的子女。

RenderObject对象没有对应的DOM树节点,所以webkit统一使用Document节点来对应匿名对象。

RenderObject树

RenderObject树的创建过程主要是有NodeRenderingContext类来负责,图7-3描述了webkit创建RenderObject对象并构建RenderObject树的过程。

基本思路:

  • 首先,webkit检查DOM节点是否需要创建RenderObject对象。
  • 如果需要,webkit会创建或获取一个NodeRenderingContext对象,
  • 该对象会分析需要创建的RenderObject对象的父亲节点、兄弟节点等,
  • 设置这些信息后,完成插入树的动作。

建立后的RenderObject树和DOM树之间的对应关系,如图7-4所示。

  • 使用虚线箭头表示两种树的节点对应关系。
  • HTMLDocument节点对应RenderView节点。
  • RenderView节点是RenderObject树的根节点。

网页层次和RenderLayer树

层次和RenderLayer对象

网页是可以分层的,有两点原因:

  • 方便网页开发者开发网页并设置网页层次
  • 简化渲染逻辑,方便webkit处理。

webkit会为网页的层次创建相应的RenderLayer对象。

当某些RenderObject节点出现,webkit会为这些节点创建RenderLayer对象。

一般来说,某个RenderObject节点的后代都属于该节点,除非webkit为某个后代RenderObject节点创建了一个新的RenderLayer对象。

RenderLayer树是根据RenderObject树建立的一棵新树。RenderLayer节点和RenderObject节点不是一一对应关系。建立RenderLayer节点的基本规则如下:

  • DOM树的Document节点对应的RenderView节点。
  • DOM树的Document的子女节点。
    • 即HTML节点对应的RenderBlock节点。
  • 显式指定CSS位置的RenderObject节点。
  • 有透明效果的RenderObject节点。
  • 节点有溢出(overflow)、alpha或反射等效果的RenderObject节点。
  • 使用Canvas 2D和3D(WebGL)结束的RenderObject节点。
  • Video节点对应的RenderObject节点。

除了根节点是RenderLayer节点。

一个RenderLayer节点的父亲就是该节点对应的RenderObject节点祖先链中最近的祖先,并且祖先所在的RenderLayer节点同该节点的RenderObject节点不同。

每个RenderObject节点包含的RenderObject节点,其实是一棵RenderObject子树。

webkit在创建RenderObject树后,也会创建RenderLayer树。(某些RenderLayer节点可能在执行JavaScript代码时或更新页面样式时被创建。)

RenderLayer类没有子类。

构建RenderLayer树

RenderLayer树的构建非常简单:根据规则判断是否需要创建新的RenderLayer对象,并设置父亲和兄弟关系即可。

图7-5展示了webkit生成的RenderLayer树。

图7-6是webkit内部表示的具体结构RenderObject树、RenderLayer树和布局信息中的大小和位置信息。下面根据RenderLayer树的节点来分析。

  • 首先,layer at(x,x)表示的是不同的RenderLayer节点,下面所有的RenderObject子类的对象均属于该RenderLayer对象。
    • 以第一个RenderLayer节点为例,它对应于DOM树中的Document节点。
    • 后面的“(0,0)”表示该节点在网页坐标系中的位置,“1028x683”表示节点大小
    • 第一层包含的RenderView节点后面的信息同理。

  • 其次,第二个layer包含了HTML中绝大部分元素。
    • head元素没有相应的RenderObject对象。(非可视元素)
    • canvas不在第二个layer中,尽管该元素是RenderBody节点的子女。
    • 该layer层包含一个匿名的RenderBlock节点。
      • 该节点包含了RenderText和RenderInline等子节点。
  • 再次,第三个layer层。
    • JavaScript为canvas元素创建了一个WebGL的3D绘图上下文对象,webkit重新生成了一个新的RenderLayer对象。
  • 最后,三个层次的创建时间。
    • 在创建DOM树后,webkit会接着创建第一个和第二个layer层。
    • 在执行JavaScript代码时才创建第三个layer层。
      • webkit检查出JavaScript为canvas创建了3D绘图上下文,才会创建新的layer层。(遇到canvas元素时并不创建。)

渲染方式

绘图上下文

在webkit中,绘图定义了一个抽象层,即绘图上下文,所有绘图的操作都在该上下文中进行。

绘图上下文分为两种:

    • 2D绘图上下文。
    • 3D绘图上下文。

这两种上下文都是抽象基类,只提供接口,具体绘制由不同的移植提供不同的实现。图7-7描述了抽象类和webkit的移植实现类的关系。

PlatformGraphicsContext类和PlatformGraphicsContext3D类是两个表示上下文的类,类定义取决于各个移植。

    • 在Safari移植中,这两个是CGContent和CGLContextObj;
    • 在chromium移植中,则是PlatformContextSkia和GrContect。

注:这些不是父子关系,而是webkit通过C语言的typedef将不同移植的类重命名成PlatformGraphicsContext和PlatformGraphicsContext3D。

2D绘图上下文具体作用:

    • 提供基本绘图单元的绘制接口以及设置绘图的样式。
    • 绘图接口包含:画点、画线、画图片、画多边形、画文字等
    • 绘图样式包括:颜色、线宽、字号大小、渐变等。

3D绘图上下文,主要用处是支持CSS3D、WebGL等。具体在第八章介绍。

RenderObject对象知道自己需要画什么样的店,什么样的图片。所以,RenderObject对象调用绘图上下文的这些基本操作,就是绘制实际的显示结果。

图7-8描述了RenderObject类和GraphicsContext类的关系。

渲染方式

网页渲染方式:

    • 软件渲染
    • 硬件加速渲染
    • 混合模式
  • 每个RenderLayer对象可以想象成图像中的一个层,各个层一同构成一个图像。
  • 每个层对应网页的一个或一些可视元素,会被绘制到该层上,这一过程称为绘图操作。
    • 如果绘图操作使用CPU完成,称之为软件绘图。
    • 如果由GPU完成,称之为GPU硬件加速绘图。
  • 理想情况下,每个层都有绘制的存储区域,用来保存绘图的结果。
  • 最后,将这些层的内容合并到同一个图像中,称之为合成,使用了合成技术的渲染称之为合成化渲染。

在RenderObject树和RenderLayer树之后,webkit将内部模型转换成可是结果分为两个阶段:

    • 每层的内容进行绘图工作,
    • 将这些绘图结果合并为一个图像。

对于软件渲染来说,没有合成阶段。原因很简单,没有必要。

在软件渲染中,通常渲染结果就是一个位图(BitMap),绘图每一层都使用该位图,区别在于绘制位置可能不同(每一层都按照从后到前的顺序)。一个位图就能够解决问题。图7-9是网页的三种渲染方式。

  • 软件渲染中网页使用的一个位图,实际就是一块CPU使用的内存空间。
  • 第二、三种方式,都是用了合成化的渲染技术。
    • 即,使用GPU硬件来加速合成这些网页层,这里称为硬件加速合成。
  • 但这两种方式又有区别:
    • 第二种方式:其中某些层使用CPU绘图,另外一些层使用GPU绘图。
      • 对于使用CPU绘制的层,结果会保存在CPU中,之后传输到GPU中合成。
    • 第三种方式:使用GPU来绘制所有合成层。

渲染的基本知识:

  • 首先,对于常见的2D绘图操作,在性能上,使用GPU不一定比CPU有优势。
    • 如绘制文字、点、线等。
    • 原因是,CPU使用缓存机制有效减少重复绘制的开销,且不需要GPU并行性。
  • 其次,GPU的内存资源相对CPU的来说比较紧张,且网页分层使得GPU内存使用更多。

鉴于以上来看,三者的存在都有其合理性,下面分析一下各自的特点:

  • 软件渲染:最常见,也是浏览器最早使用的渲染方式。
    • 比较节省内存,尤其GPU内存。
    • 只能处理2D方面的操作,比较适合渲染,没有复杂绘图或多媒体方面需求的简单网页。
    • 当网页中有个更新小型区域的请求(如动画)时,软件渲染的代价相对小一些。
      • 软件渲染只需要计算一个极小的区域,
      • 硬件渲染可能要重新绘制其中的一层或多层,再合成这些层。
      • 硬件渲染代码可能会大得多。
    • 但遇上HTML5的很多新技术,软件渲染显然无能为力。
      • 一、能力不足:典型的例子是CSS3D、WebGL等。
      • 二、性能不好:如视频、Canvas 2D等。
      • 因此,软件渲染技术使用得越来越少,尤其是移动领域。
  • 硬件加速的合成渲染:
    • 每个层的绘制和所有层的合成均使用GPU硬件来完成,对需要使用3D绘图的操作特别合适。
    • 硬件加速机制能够支持HTML5定义的2D或3D绘图标准。
    • 某些情况下,更新某个层的一个区域,硬件加速的合成渲染代价可能更小。
      • 软件渲染没有为每一层提供后端存储,需要把和这个区域有重叠的所有层次的相关区域依次从后向前重新绘制一遍。
      • 硬件加速渲染只需要重新绘制更新发生的层次。
      • 当然,这取决于网页的结构和渲染策略。
  • 软件绘图的合成渲染:
    • 结合了前两种方式的优点。
    • 因为很多网页既包含基本的HTML元素,又包含HTML5新功能,
      • 基于性能和内存综合考虑,使用CPU和GPU分别绘制图层,再合成。

webkit软件渲染技术

软件渲染过程

分析软件渲染过程,需要关注两个方面:

    • RenderLayer树
    • 每个RenderLayer树所包含的RenderObject子树。

对于每个RenderObject对象,需要三个阶段绘制:

  1. 绘制该层中所有块的背景和边框。
  2. 绘制浮动内容。
  3. 前景,即内容部分、轮廓等。

注:内嵌元素的背景、边框、前景都是在第三阶段被绘制的。

图7-10 描述了一个RenderLayer层是如何绘制自己和子女的,这一过程是递归的。主要节选了一些重要步骤。且图中有些步骤并不是总发生的。

  1. 首先,对于当前的RenderLayer对象,webkit需要绘制反射层(ReflectionLayer),这是由css定义的。
  2. 然后,webkit绘制RenderLayer对象对应的RenderObject节点的背景层。不包括RenderObject子女。
    1. PaintBackground-ForFragments,即调用PaintPhaseBlockBackground函数。
    2. Fragments的含义是,可能绘制的几个区域。
      1. 因为网页需要更新的区域可能不连接,而是多个小块。
      2. 所以,webkit绘制时更新这些小块即可。
  1. paintList(z坐标为负数的子女层)阶段,负责绘制很多Z坐标为负责的子女层。
    1. 这是一个递归过程。
    2. Z坐标为负数的层在当前RenderLayer层后面,
    3. webkit先绘制后面的层,当前层可能覆盖他们。
  1. PaintBackgroundForFragments() 这个步骤比较复杂,包含四个子阶段:
    1. 首先,进入PaintPhaseChildBlockBackground阶段。
      1. webkit绘制该RenderLayer节点对应的RenderObject节点的所有后代节点的背景。
        1. 即,绘制该层对应RenderObject的后代节点的背景。
      1. 如果某个被选中,webkit改为绘制选中区域背景。(内容被选中可能是另外的颜色)。
    1. 其次,进入PaintPhaseFloat阶段。
      1. webkit绘制浮动元素。
    1. 再次,进入PaintPhaseForeground阶段。
      1. 绘制RenderObject节点的内容和后代节点的内容(如文字)等。
    1. 最后,进入PaintPhaseChildOutlines阶段。
      1. 绘制所有后代节点的轮廓。
  1. 进入 PaintOutlineForFragments 步骤。
    1. 绘制RenderLayer对象对应的RenderObject节点的轮廓。
  1. 进入绘制RenderLayer对象的子女步骤。
    1. 首先绘制溢出的RenderLayer节点。
    2. 依次绘制Z坐标为正数的RenderLayer节点。
  1. 进入该RenderObject节点的滤镜步骤。
    1. 这是CSS标准定义在元素之上的最后一步。

上面是从RenderLayer节点和它包含的RenderLayer子树来解释软件绘图这一过程,下面针对RenderLayer树包含的每个RenderObject来解释这一过程。

由于RenderObject有很多子类,很多子类的绘制比较简单,这里以典型的RenderBlock类为例,图7-11给出了绘制的RenderBlock类的过程。

paint是RenderObject基类的绘图函数,用来绘制该对象的入口函数,在RenderBlock类中被重新实现了。

一个RenderObject类的paint函数可能会被多次调用,因为不同会直接断需要调用它绘制不同部分。(图7-11右侧标记了在哪些阶段 会/不会 调用该绘制函数),这些阶段顺序由RenderLayer对象中的调用过程控制。

图中的paintContents函数主要用来遍历和绘制它的子女。

某些情况下,webkit不需要该函数。如,RenderLayer对象只需要绘制对应RenderObject子树的根节点时。

对于RenderObject其他子类节点,绘制过程是这一过程的子集。如,RenderText类没有子女,也不需要绘制框模型的边框,只需绘制自己的内容。

在这一过程中,webkit所使用的绘图上下文是2D的。因为没有GPU加速,3D绘图上下文没法工作。这意味着:

    • 每一层的RenderObject紫薯不能包含使用3D绘图的节点,如,Canvas 3D(WebGL)节点等。
    • 每个RenderLayer层上使用的CSS 3D变形等操作也没办法得到支持。

webkit第一次绘制网页时,绘制的区域等同于可视区域的大小。

此后,webkit只是先计算需要更新的区域,然后绘制同这些区域有交集的RenderObject节点。

图7-12 描述了在软件渲染过程中webkit实际更新的区域,即软件渲染过程的生成结果。

webkit软件渲染结果的储存方式,在不同的平台可能不一样,但基本上都是CPU内存的一块区域,多数情况下是一个位图(BitMap)。

chromium的多进程软件渲染技术

chromium引进了多线程模型,所以需要将渲染结果从Renderer进程传递到Browser进程。

注:webkit的软件渲染过程在Renderer进程中进行,网页的显示Browser进程中。

先来看Renderer进程。

    • chromium移植的接口类是RenderVIewImpl。
      • 包含一个WebViewImpl类(用于表示一个网页的渲染结果)。
    • RenderVIewImpl继承自RenderWidget类。
      • 作用是同Browser进程通信。
      • RenderWidget类的作用:
        • 负责调度页面渲染和页面更新到实际的WebViewImpl类等操作。
        • 负责同Browser进程的通信。
    • PlatformCanvas类,即 SkiaCanvas。
      • RenderObject树的实际绘制操作和绘制结果都由该类完成。类似与2D绘图上下文和后端存储的结合体。

再来看Browser进程。

    • RenderWidgetHost类:
      • 负责同Renderer进程的通信。
        • 传递Browser进程中网页操作的请求给Renderer进程的RenderWidget类,并接收来自对方的请求。
    • BackingStore类:
      • 后端的存储空间,通常是网页可视区域的大小,存储数据就是页面的显示结果。
      • 作用:
        • 保存当前的可视结果。
          • 因此Renderer进程的绘制工作不影响网页结果的显示。
        • webkit只需绘制网页的变动部分,chromium把变动更新到该后端存储即可。

最后来看两个进程是如何传递信息和绘制内容的。

    • 两个进程传递绘制结果是通过TransportDIB类完成。
      • 该类是一个共享内存的实现。
    • 对Renderer进程来说,
      • Skia Canvas 把内容绘制到共享内存的位图中。
    • 对Browser进程来说,
      • 当接收到绘制完成的通知消息时,(来自Renderer进程)
      • Browser进程把共享内存的内容复制到BackingStore对象中,
      • 释放共享内存。

Browser进程的后端存储被绘制在显示窗口中,用户就能看到网页结果。

图7-13 显示的是软件渲染的架构图。主要思想来源于chromium的官方网站。

多进程的渲染过程大致如下:

    1. RenderWidget类收到更新请求,chromium创建一个共享内存区域。
    2. chromium创建Skia的SkCanvas对象,RenderWidget把实际绘制工作派发给RenderObject树。
      1. webit遍历RenderObject树,
      2. 每个RenderObject节点绘制自己和子女的内容,
      3. 并存储到(SkCanvas对象对应的)共享内存的位图中。
    1. RenderWidgetHost类把位图复制到BackingStore对象中,并调用Paint函数把结果绘制到窗口中。

两种会触发重新绘制网页中某些区域的请求:

    • 前端请求:从Browser进程发起的请求。
      • 可能是浏览器自身的请求,也可能是 X 窗口系统(或其他窗口系统)的请求。
      • 典型例子:用户因操作网页引起的变化。
    • 后端请求:由于页面自身逻辑而发起更新部分区域的请求。
      • 如,HTML元素或样式的改变、动画等。

下面详细解释,当有绘制或更新某个区域的请求时,chromium和webkit是如何处理这些请求的。具体过程如7-14。

  1. Renderer进程的消息循环(Message Loop)调用处理“界面失效”的回调函数。
    1. 该函数主要调用RenderWidget::DoDeferredUpdated来完成绘制请求。
  1. RenderWidget::DoDeferredUpdated 函数调用 Layout 函数。
    1. 来触发检查是否有 需要重新计算的布局和更新请求。
  1. RenderWidget 类调用transportDIB类创建共享内存。
    1. 内存大小为绘制区域的高x宽x4。
    2. 同时调用Skia图形库创建SkCanvas对象。
    3. 该对象绘制目标是,一个使用共享内存存储的位图。
  1. 当渲染页面时,ScrollView类请求按从前到后的顺序遍历,并绘制RenderLayer对象搞得内容到位图中。
    1. webkit绘制RenderLayer对象的步骤如下:
      1. webkit计算重绘的区域是否和RenderLayer对象重合。
      2. 如果有,webkit要求绘制该层中所有RenderObject对象。
  1. 绘制完成后,
    1. Renderer进程发送UpdateRect的消息给Browser进程,同时返回以完成渲染过程。
    2. Browser进程接收到消息后,
      1. 由BackingStoreManager类来获取或创建BackingStoreX对象,
      2. 该对象大小与可视区域相同,包括整个网页的坐标信息。
      3. 根据UpdateRect的更新区域的位置信息,将共享内存的内容绘制到自己的对应存储区域中。
  1. 最后,Browser进程将UpdateRect的回复消息发送到Renderer进程。
    1. Renderer进程知道已使用完共享内存,可以进行回收利用操作。