likes
comments
collection
share

像素是怎样练成的

作者站长头像
站长
· 阅读数 105

{万物皆有裂痕,那是光照进来的地方|There's a track,a crack in everying .That's how the light gets in.}

大家好,我是柒八九

前言

本来呢,最近在规划一篇关于浏览器的文章,但是在做文章架构梳理和相关资料查询的时候,发现浏览器在渲染页面的过程中,也别有洞天。索性,就单独将其作为一篇文章来写。

这里有几点说明。

  1. 平时开发中用的是Chrome浏览器,所以下面的文章都是以Chrome浏览器为准。
  2. 本文中,ChromiumChrome可以认同是一个东西,不做强制区分,理由下文会讲到。
  3. 该篇文章过于长,可以按照自己喜好,酌情选读文章内容。
  4. 该篇文章主要讲浏览器内部渲染像素(页面)角度分析,而不会涉及到网络处理/JS解析等。有的话也是一带而过,我们后期会专门有一篇文章,带大家串一下流程的。

还有,之前我们写过浏览器相关的知识点,如果想了解该系列文章(浏览器相关),可以参考我们已经发布的文章。如下是往期文章。

  1. 页面是如何生成的(宏观角度)
  2. Chromium 最新渲染引擎--RenderingNG
  3. RenderingNG中关键数据结构及其角色
  4. 浏览器之客户端存储

你能所学到的知识点

  1. 前置知识 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
    1. ChromiumChrome的关系
    2. Chromium架构简析
    3. 何为网页内容
    4. 何为{像素|Pixels}
    5. Chrome渲染过程是反复进行的
  2. 页面数据解析 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
    1. HTML 解析为 DOM
    2. CSS 解析为 CSSOM
  3. 布局阶段生成 {不可变fragment树|immutable fragment tree}推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  4. {绘制|Paint}阶段生成{显示列表|Display List} 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
  5. {光栅化|Raster}将部分{显示列表|Display List}转换位{位图|BitMap} 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
    1. GPU 进程中进行光栅化
  6. 页面状态发生变化
    1. 动画帧生成图层
    2. 合成分配(Compositing Assignments)发生在绘制之前
    3. 合成生成 property trees
  7. Display(viz)
    1. 双缓存

好了,天不早了,干点正事哇。 像素是怎样练成的


前置知识

Chromium 和 Chrome的关系

ChromiumChrome之间存在密切的关系,可以理解为ChromiumChrome的开源项目。

  1. ChromiumChromium是一个开源的Web浏览器项目,由Google主导开发。它是一个完全开放的项目,源代码可以公开获取并进行自由修改。

    • Chromium项目包括浏览器引擎BlinkJavaScript引擎V8等组件。
    • Chromium致力于提供一个可扩展、快速和安全的Web浏览器解决方案,同时也是许多其他基于Chromium的浏览器的基础。
  2. ChromeChrome是由Google基于Chromium项目开发的Web浏览器。

    • 它是Chromium商业版本,针对普通用户提供了更多功能和服务。
    • Chrome具有更多的集成功能,包括自动更新PDF阅读器Google账号同步等。
    • 此外,Chrome还包括一些针对企业用户和开发人员的工具和功能。

可以将Chromium视为Chrome的基础,Chrome在此基础上添加了自己的功能和服务。

想必大家都有Chrome浏览器,我们可以做一个验证,大家在地址栏中输入chrome://settings/help或者按照如下的步骤。 像素是怎样练成的

关于ChormeChromium的关系就映入眼帘。如下图所示。

像素是怎样练成的 关于它们之间的关系,我们就不在赘述。


其它奇怪的知识

其实Chromium也是可以被下载,同时也可以作为搜索引擎的。


市面上,很多浏览器都是基于Chromium浏览器开发的。

Edge 浏览器 像素是怎样练成的

360浏览器 像素是怎样练成的


Chromium架构简析

Chromium被分成两个主要部分(不包括其他库):{浏览器|Browser}{渲染器|Renderer}(包括Blink,网络引擎)。

  • {浏览器|Browser}主进程,代表所有的用户界面I/O
    • 负责运行用户界面并管理{渲染器|Renderer}和其他进程
    • 也称为"浏览器进程"或简称为{浏览器|Browser}
  • {渲染器|Renderer}是由{浏览器进程|Browser Process}驱动的子进程
    • 渲染器使用Blink开源布局引擎来解释和布局HTML

它们的关系如下 像素是怎样练成的


从源码架构角度来看Chromium

像素是怎样练成的 每个框代表一个应用层。任何一个低层级都不依赖于更高层级的内容。

我们按照从底层到顶层的顺序,来简单介绍下,每个层级的作用。

  1. WebKit:在SafariChromium和其他基于WebKit的浏览器的渲染引擎。
    • 端口(Port)是WebKit的一部分,它与平台相关的系统服务(如资源加载和图形生成)进行集成。
  2. Glue:将WebKit的数据类型转换为Chromium的数据类型的组件。
    • 这是"WebKit嵌入层"
    • 它是Chromium的基础。
  3. Renderer / Render host:这是Chromium"多进程嵌入层"
    • 它在进程上进行代理通知命令发送
  4. WebContents:是Content模块的组件。
    • 它可以轻松地嵌入到视图中,实现HTML的多进程渲染
  5. {浏览器|Browser}:代表浏览器窗口
    • 它包含多个WebContents
  6. Tab Helpers:附加到WebContents上的单独对象
    • 浏览器将各种助手对象附加到它所持有的WebContents上(如网站图标、信息栏等)。

将上面的比较生硬的词汇替换一下,然后就可以画出下面的关于Chromium架构图。 像素是怎样练成的

这块的架构图,有些生硬,后期也会有专门的文章来进行讲解。


通过上文介绍,我们得到一个结论 :Chromiumchrome。 所以,下文中可以将Chromiumchrome看成一个东西。

下图是chromecontent生成页面信息的示意图。

像素是怎样练成的


何为网页内容

像素是怎样练成的

Chromium C++代码库中,在架构层面上content负责红色框中的所有内容。(可以看上面的架构图) 而Tabs地址栏导航按钮菜单等不在content的范围内。

Chrome安全模型的关键是渲染发生在沙盒化的进程中

  • Blink{渲染器|Renderer}进程中的代码子集,在content命名空间内。
  • Blink实现了Web平台API和Web规范的语义。

{渲染器|Renderer}进程还运行一个称为{合成器|compositor}"cc")的组件。

对应的关系如下:(从进程和线程的关系角度看) 像素是怎样练成的

页面内容分类

contentChromium 中用于表示网页内部或 Web 应用程序前端的所有代码的通用术语。也就是在上面架构图中的content

常见的类型包括文本图像HTML元素(包围文本的标记语言)、CSS(定义HTML元素的表现方式)和JavaScript(可以动态修改上述所有内容)。

像素是怎样练成的

除了上述列举的常见的内容类型,像<video>, <canvas>, WebAssembly, WebGL, WebVR, PDF也属于Content的范畴。

如果对WebAssembly不了解,可以翻看之前写的 -浏览器第四种语言-WebAssembly

还有关于WebGL也打算写相关系列的文章,敬请期待.....


我们通过一个真实的案例来看一下。下图是最近很🔥的ChatGPT的地址。左侧是真实的页面显示,右侧是该页面中包含的内容信息

像素是怎样练成的

可以看到一个真实的网页是由数千行HTML、CSS和JavaScript代码的纯文本形式的所组成网页的源代码是{渲染器|Renderer}的输入


何为{像素|Pixels}

{像素|Pixels}图像的最小单位,它是构成数字图像的基本元素

"像素"一词源自于"picture element"的缩写。每个像素代表了图像中的一个点,它具有特定的位置和颜色信息

在计算机图形中,{像素|Pixels}通常被表示为一个二维矩阵或数组,它们排列在网格中,形成图像的整体。每个像素可以存储图像的亮度颜色透明度等信息。对于彩色图像,通常使用RGB(红、绿、蓝)模型来表示每个像素的颜色,其中每个分量的取值范围通常是0到255之间。

{像素|Pixels}密度决定了图像的清晰度和细节水平。更高的像素密度意味着在给定的显示区域内有更多的像素,从而能够呈现更多的细节。常见的像素密度单位是每英寸像素数,称为PPI(Pixels Per Inch)

在计算机图形处理中,我们可以通过操作和改变像素的颜色、位置和透明度来实现图像的绘制、编辑和处理。像素在计算机图形、摄影、显示技术和计算机视觉等领域起着至关重要的作用,它们是数字图像的基本组成部分


CSS表示像素颜色的方式

表示方式示例描述
十六进制表示法#FF0000使用六位十六进制数表示颜色,每两位表示红、绿、蓝三个通道的亮度值,取值范围是00到FF
RGB表示法rgb(255, 0, 0)使用RGB值表示颜色
RGBA表示法rgba(255, 0, 0, 0.5)使用RGB值Alpha通道表示颜色Alpha通道的取值范围是0.0到1.00.0表示完全透明,1.0表示完全不透明
HSL表示法hsl(0, 100%, 50%)使用色相(Hue)、饱和度(Saturation)和亮度(Lightness)来表示颜色。色相的取值范围是0到360,饱和度和亮度的取值范围是0%到100%
HSLA表示法hsla(0, 100%, 50%, 0.5)HSL表示法类似,增加了一个Alpha通道来表示透明度,取值范围也是0.0到1.0

Chrome渲染过程是反复进行的

像素是怎样练成的

渲染过程可以被描述为:将 HTML/CSS/JavaScript等数据类型进行转换,并且输入到 OpenGL 以被调用,以显示像素。

同时,在Chrome渲染过程中,我们还希望获得正确的中间数据结构,以便快速响应之后的更新操作,并能够快速响应JS等的数据查询。

可以将渲染过程分为多个"生命周期阶段",生成这些中间输出。

像素是怎样练成的


页面数据解析

在之前的计算机底层知识系列中,我们讲过计算机CPU能直接解释运行的只有本地代码(机器语言)程序。用JS/Java等高级语言编写的源代码,需要通过各自的编译器编译后,转换成本地代码。 (有兴趣的可以翻看之前的文章)。下面的处理过程也是类似的。大家可以进行类推分析。

HTML/CSS/JS是不能够被浏览器直接识别的,是需要进行格式转换和处理。这里就涉及到编译原理相关的知识点。(后期有打算,写相关的编译原理的文章,我们这里就不展开说明了)

HTML 解析为 DOM

HTML标签通过语意化处理将网页进行了分层处理

例如,一个 <div> 可能包含两个<p>,每个<p>都带有文本信息。因此,第一步是解析这些标签,构建一个反映这种结构的{文档对象模型|Document Object Model}(简称:DOM)。

像素是怎样练成的


何为DOM

{文档对象模型|Document Object Model}是一种用于表示和操作HTMLXMLXHTML文档的编程接口。它将文档解析为一个由{节点|Node}{对象|Object}组成的树形结构,这个树形结构被称为DOM树

DOM树的根节点是{文档节点|Document Node},它代表整个文档。文档节点下方是{元素节点| Element Node},表示HTML或XML文档中的标签。元素节点可以包含其他元素节点、{文本节点| Text Node}{注释节点| Comment Node}等。

每个节点在DOM中都有特定的属性和方法,可以用于访问和操作节点的内容、属性和样式。一些常见的节点类型包括:

  1. {元素节点| Element Node}:代表HTMLXML文档中的标签,如 <div><p><a>等。
    • 可以通过节点的标签名、属性和子节点等进行操作。
  2. {文本节点| Text Node}:代表元素节点中的文本内容,即标签之间的文本。
  3. {注释节点| Comment Node}:代表文档中的注释部分,以<!--开头和-->结尾。
  4. {属性节点|Attribute Node}:代表元素节点的属性。

DOM提供了一组API,可以通过这些API来操作和修改DOM树。开发人员可以使用JavaScript或其他支持DOM的编程语言来访问和操作DOM。

通过DOM,我们可以动态地创建、修改、删除和查询文档的元素和内容,从而实现动态的Web页面交互和数据操作


DOM 反应了包含关系

一个{文档对象模型|Document Object Model}反映了包含关系

{文档对象模型|Document Object Model}中,每个HTML元素被表示为一个对象,这些对象之间通过父子关系来表示它们之间的包含关系。

例如,如果有一个包含两个段落的 <div> 元素,那么在DOM中,将会有一个表示 <div> 的对象,它包含两个表示段落的子对象。这样的层次结构可以通过递归方式表示整个文档的层次关系。

像素是怎样练成的


DOM的双面性

DOM具有双重功能,既作为页面的内部表示,又作为供JS查询或修改渲染的API。

像素是怎样练成的

JavaScript引擎(V8)通过一种称为{绑定|Bindings}的系统,将DOM Web API暴露给开发者。

JavaScript引擎(V8)通过{绑定|Bindings}机制将JavaScript与底层的DOM接口进行连接。这种机制允许开发者使用JavaScript来操作和操纵Web页面上的元素样式事件等。实际上,这些DOM Web API只是对底层DOM树的操作进行了封装,提供了一种更便捷和直观的方式来与DOM进行交互。


多个DOM树

在同一个文档中可能会存在多个DOM树

像素是怎样练成的

如上图所示,当我们使用自定义元素,在开启影子模式时,attchShadow({mode:'open'})就会产生多个DOM树。(如果对自定义元素的使用方式不是很明确的同学,可以参考这篇文章

宿主节点的子元素(在宿主树中)被分配到影子树中的<slot>中。

像素是怎样练成的

FlatTreeTraversal从宿主节点向下遍历直至影子节点,同时将<slot>替换为指定的元素。


CSS 解析为 CSSOM

构建完DOM树之后,下一步是处理CSS样式。CSS选择器用于选择DOM元素的子集,以对其添加指定的属性声明

像素是怎样练成的

在处理CSS样式时,浏览器会解析CSS文件内联样式,并将样式规则应用于DOM树中的相应元素。CSS选择器用于选择要应用样式的目标元素。选择器可以根据元素的标签名类名ID属性等进行匹配,以确定应用哪些样式规则。

这里多啰嗦几句,在CSS重点概念精讲中我们介绍过,选择器

这里我直接就拿来主义了。


CSS 选择器

选择器(.#[]:::)5个

瞄准目标元素

  1. 类选择器
    • .开头
  2. ID选择器
    • #开头
    • 权重相当高
    • ID一般指向唯一元素
  3. 属性选择器
    • 含有[]的选择器
    • [title]{}/[title="test"]{}
  4. 伪类选择器
    • 前面有一个冒号(:)的选择器
    • :link :选择未被访问的链接
    • :visited:选取已被访问的链接
    • :active:选择活动链接
    • :hover :鼠标指针浮动在上面的元素
  5. 伪元素选择器
    • 有连续两个冒号(::)的选择器
    • ::before : 选择器在被选元素的内容前面插入内容
    • ::after : 选择器在被选元素的内容后面插入内容

关系选择器 (空格>~+)4个

根据与其他元素的关系选择元素的选择器

  1. 后代选择器
    • 选择所有合乎规则的后代元素
    • 空格链接
  2. 相邻后代选择器
    • 仅仅选择合乎规则的儿子元素
    • 孙子,重孙子元素忽略
    • >链接
  3. 兄弟选择器
    • 选择当前元素后面的所有合乎规则的兄弟元素
    • ~链接
  4. 相邻兄弟选择器
    • 仅仅选择当前元素相邻的那个合乎规则的兄弟元素
    • +链接
    • 常见的使用场景是,改变紧跟着一个标题的段的某些表现方面

权重

  1. !important (10000)
  2. 内联1000
  3. ID选择器(0100
  4. 选择器(0010
  5. 标签选择器(0001

像素是怎样练成的 上面的优先级计算规则,内联样式的优先级最高,如果外部样式需要覆盖内联样式,就需要使用!important


例如,对于以下CSS规则:

h1 {
  color: blue;
}

.my-class {
  font-size: 16px;
}

第一个规则选择所有的 <h1> 元素,并将其文本颜色设置为蓝色。第二个规则选择具有类名为 my-class 的元素,并将其字体大小设置为16像素。

在应用CSS样式时,浏览器会遍历DOM树,匹配元素与选择器,并将相应的样式属性应用于匹配的元素。这样,每个元素都会根据匹配的CSS规则来设置其样式属性,从而实现页面的外观和布局。

通过处理CSS样式,我们可以为网页提供丰富的外观效果、布局和交互特性,使网页更加美观和易于使用。


CSS解析器

{CSS解析器|CSS Parser}会解析所有可达有效的样式表,包括内联样式表( <style>)、外部样式表(styles.css)和浏览器默认样式表。它会将样式规则解析为一个模型(这就是我们常说的CSSOM),其中包含选择器和对应的样式声明

像素是怎样练成的

  • 选择器描述了要应用样式的目标元素
  • 样式声明定义了要应用的具体样式属性和值。 解析后的CSSOM包含了这些选择器和声明的组合

为了提高样式规则的查找效率,{CSS解析器|CSS Parser}会对样式规则进行索引。这样可以快速定位匹配特定选择器的样式规则,而不需要遍历整个样式表。

此外,属性类是在构建时由Python脚本自动生成的。属性类用于在运行时快速查找具有相同样式属性的元素。它们被用作索引的一部分,以便在应用样式时能够高效地定位和处理相同属性的元素。

总而言之,CSS解析器根据活动样式表构建样式规则模型,并通过索引和属性类来优化样式的查找和应用过程。这样可以提高渲染效率,并确保正确地应用样式到文档的各个元素上。


ComputedStyle

像素是怎样练成的

在样式解析(或重新计算)过程中,解析器会遍历DOM树中的每个元素,并根据匹配的样式规则计算出每个元素的样式属性的最终值。这些最终值包括继承的值层叠的值以及通过CSS属性值计算得到的值。

所有计算得到的样式属性值会被存储在 ComputedStyle 对象中。这个对象可以被认为是一个巨大的映射,其中样式属性(如颜色、字体大小、边距等)与其对应的值关联起来。通过查询 ComputedStyle 对象,可以快速获取每个元素的最终样式属性值。

通过样式解析和计算,浏览器可以确定每个元素应用的最终样式,从而实现正确的页面渲染和布局。ComputedStyle 对象在渲染过程中起着重要的作用,为每个元素提供了其最终的样式属性值。


实践验证

我们可以通过Chrome开发者工具可以显示任何DOM元素的ComputedStyle像素是怎样练成的

也可以通过JavaScript访问,getComputedStyle 是一个用于获取元素计算后的样式的方法。

  1. document.styleSheets
    • 这是一个属性,用于获取文档中所有的样式表(style sheets)。
    • 可以使用document.styleSheets返回的样式表集合来访问和操作具体的样式表。
  2. window.getComputedStyle(element)
    • 这是一个方法,用于获取指定元素的计算样式(computed style)。
    • 可以通过传递元素对象给getComputedStyle方法来获取该元素应用的最终样式。
  3. document.styleSheets[i].cssRules
    • 这是一个属性,用于获取样式表中的所有规则(rules)。
    • 可以使用cssRules属性返回的规则集合来访问和操作具体的样式规则。
  4. element.style
    • 这是一个属性,用于获取或设置元素的内联样式(inline style)。
    • 可以通过element.style来访问和修改元素的样式属性。

使用 getComputedStyle 的基本语法如下:

var element = document.getElementById("elementId");
var computedStyle = window.getComputedStyle(element);

在上面的代码中,我们首先通过 document.getElementById 方法获取到一个具体的元素,并将其赋值给 element 变量。然后,我们使用 window.getComputedStyle 方法来获取该元素的计算后样式,将其赋值给 computedStyle 变量。

通过 getComputedStyle 获取到的样式是一个 CSSStyleDeclaration 对象,它包含了该元素所有计算后的样式属性和对应的值。

你可以通过以下方式来获取具体的样式属性的值:

var value = computedStyle.getPropertyValue("property");

其他语言的处理

其实在浏览器中,一共支持四种语言。 像素是怎样练成的

针对JS的处理,需要用到V8等引擎,WebAssembly也是需要做处理的。因为,篇幅有限,这里就不展开描述了。

针对JS的解析过程,可以参考JS执行流程

关于WebAssembly的介绍,可以参考浏览器第四种语言-WebAssembly


{布局|Layout}阶段生成 {不可变fragment树|immutable fragment tree}

在构建完DOM并计算所有样式后,下一步是确定所有元素的视觉几何属性

对于块级元素,我们需要计算一个矩形的坐标,该矩形对应于元素所占据的内容的几何区域像素是怎样练成的


块元素 和 内联元素

对于前端页面元素而言,一个元素的类型可以隶属于不同的类型。但是,在比较宏观的角度看,元素是否占一行还是可以和文本信息同行显示。可以把元素分成块元素内联元素

块元素

在最简单的情况下,布局按照DOM的顺序,从上到下,依次放置。我们称之为block flow。(单独占一行)

像素是怎样练成的


内联元素

文本节点和类似<span>的内联元素生成{内联框|inline boxes},通常在一行中从左到右流动

像素是怎样练成的

从右到左的内联流动方向则适用于RTL语言,如阿拉伯语希伯来语

像素是怎样练成的


确定字型的大小和位置

{布局|Layout}需要使用ComputedStyle 对象中的{字体|font}信息来测量文本。 (这里再重申一下,ComputedStyle是CSS被解析后的对象)

像素是怎样练成的

{布局|Layout}使用名为HarfBuzz文本整形库来计算每个字形的大小和位置,从而确定文本段的整体宽度。


矩形边界

{布局|Layout}可能会为单个元素计算多种类型的矩形边界。

例如,在出现溢出情况时,布局会计算{边框框盒|border box rect}{布局溢出框盒|layout overflow rect}

像素是怎样练成的

如果节点的溢出是可滚动的,布局还会计算{滚动边界|scroll boundaries}并保留滚动条的空间。

最常见的可滚动DOM节点是文档本身,它是树的根节点。

布局对象的内容可以超出其{边框框盒|border box rect}

像素是怎样练成的

同时,我们还可以设置如何处理超出部分的行为。

overflow:'auto'|'visible'|'hidden';

(是不是很熟悉)


其他复杂的布局情形

对于表格元素或指定将内容分成多列的样式,或者浮动对象使内容围绕其一侧流动,需要更复杂的布局。

像素是怎样练成的

但是,不管布局如何复杂,在布局阶段,有一个亘古不变的规则就是: DOM结构和计算样式值(ComputedStyle)是{布局|Layout}算法的输入

每个流水线阶段都使用前一个阶段的结果


布局行为不同,有不同的布局对象

{布局|Layout}在与DOM链接的单独树(布局树)上进行操作。(也就是说DOM树Layout树有关联,但是不是一个树)

{布局树|Layout Tree}中的节点实现了布局算法。

根据所需的布局行为,有不同的LayoutObject子类。样式更新阶段也会构建布局树。

布局阶段遍历布局树,对每个LayoutObject执行布局操作。

像素是怎样练成的


DOM 节点和布局对象不是一对一的关系

通常情况下,一个DOM节点对应一个LayoutObject。但有时候,一个LayoutObject没有对应的DOM节点。

甚至有可能一个节点有多个LayoutObject(例如,一个内联元素在块级子元素内,并且内联元素之前和之后都有文本)。可以参考下图中<span>inline</span>的布局对象。 像素是怎样练成的

最后,布局树的构建基于FlatTreeTraversal(FlatTreeTraversal在解析DOM的时候,当存在多个DOM树的时候,出现过哈),可以跨越影子DOM边界。


NG 布局引擎

布局引擎正在进行重写。布局树包含了传统布局对象NG布局对象的混合。最终,所有的布局对象将会是NG布局对象。

像素是怎样练成的

在NG中,布局的输入和输出被清晰地分离开来。输出是一个不可变的、可缓存的布局结果

像素是怎样练成的

{NG布局结果|NGLayoutResult}指向描述物理几何结构的{片段树|Fragments Tree}

像素是怎样练成的


实践验证

存在如下的页面结构。

<div style="max-width: 100px">  
  <div style="float: left; padding: 1ex">F</div>  
  <br>The <b>quick brown</b> fox  
  <div style="margin: -60px 0 0 80px">jumps</div>
</div>

最后的呈现效果。 像素是怎样练成的

生成DOM树

像素是怎样练成的


生成Layout树

{布局对象|Layout Object}大多数情况下与DOM元素一对一对应。

但是,在Layout树中也会存在anonymous布局对象,它是为了使其容器只包含块级子元素而创建的

{布局块|LayoutBlock}可以具有块级子元素或内联子元素,但不能同时具有两者。

虽然,文本"quick brown" 存在断行情况,但是它是存在一个LayoutText节点中。

像素是怎样练成的


生成不可变Fragment树

{片段树|Fragment Tree}中,我们可以看到断行的结果以及每个片段的位置和大小

像素是怎样练成的

片段的断行的结果

像素是怎样练成的

片段位置和大小

像素是怎样练成的

像素是怎样练成的


{绘制|Paint}阶段生成{显示列表|Display List}

通过上述的数据处理,我们已经获取到{布局对象|Layout Object}的几何属性,接下来我们就需要将其绘制处理了。

{绘制记录|Paint Records}绘制操作记录到{显示项|Display Items} 列表中。

绘制操作可以是诸如"在这些坐标上以这种颜色绘制一个矩形"之类的内容。

对于每个{布局对象|Layout Object}可能会有多个{显示项|Display Items},对应着其不同的视觉呈现部分,如背景、前景、轮廓等等。

像素是怎样练成的


按照层叠顺序进行元素绘制

层叠顺序

这里多啰嗦几句,在CSS重点概念精讲中我们介绍过,关于层叠上下文和层叠顺序,这里我们只是做简单的知识介绍,如果想了解更多,可以参考之前的文章。

{层叠顺序|Stacking Order}表示元素发生层叠时有着特定的垂直显示顺序

一旦普通元素具有层叠上下文,其层叠顺序就会变高

分两种情况

  1. 如果层叠上下文元素不依赖z-index数值,则其层叠顺序是z-index:auto
    • 可看成z-index:0
  2. 如果层叠上下文元素依赖z-index数值,则其层叠顺序由z-index值决定

像素是怎样练成的


按照层叠顺序进行页面绘制

按正确的顺序绘制元素非常重要,这样它们在重叠时才能正确叠放。绘制顺序可以通过样式来控制。

绘制顺序是按照层叠顺序,而不是DOM顺序

像素是怎样练成的

可以看到,虽然yellow的DOM顺序在green的DOM之前,但是在绘制到页面上时,yellowgreen的上面。(yellowZ轴大)


每个绘制过程都是对层叠上下文的单独遍历

甚至有可能一个元素部分在另一个元素前面,部分在后面。这是因为绘制过程分为多个阶段,每个绘制阶段都会对子树单独遍历。

存在如下的页面结构:

<section class="container">
  <div id="green"></div>
  <div id="blue"></div>
</section>

对应的样式如下:

#green {
    position: relative;
    background-color: green;
    height:200px;
    width:300px;
  }
  
#blue {
    position: absolute;
    top: 20px;
    left: 30px;
    width: 200px; 
    height: 100px; 
    background-color: blue;
    border: 5px solid black;
  }
  
#green::before {
    content: "绿色元素的文案信息";
    position: absolute;
    top: 20px;
    left: 10px;
    z-index: 2;
    color:white;
    font-weight: 700;
  }

在不考虑,CSS3其他特殊属性的情况下,当元素设置了z-index,就会生成一个层叠上下文,并且每个绘制阶段都是对层叠上下文的单独遍历

像素是怎样练成的


实践验证

存在如下的页面结果。

<style> #p {  
  position: absolute; padding: 2px;  
  width: 50px; height: 20px; 
  left: 25px; top: 25px;  
  border: 4px solid purple;  
  background-color: lightgrey;
} </style>
<div id=p> pixels </div>

呈现的效果如下: 像素是怎样练成的


生成显示项信息

两个DOM节点(包括#document)生成了三个{显示项|Display Items}和四个绘制操作

像素是怎样练成的


文本绘制

文本的绘制操作包含一个包含每个字形的标识符和偏移量的块。

该步包含在显示项列表中,看上图中,位于最后一个.

像素是怎样练成的


{光栅化|Raster}将部分{显示列表|Display List}转换位{位图|BitMap}

{显示列表|Display List}中的绘制操作通过称为{光栅化|Raster}的过程来执行。

像素是怎样练成的

最后生成的位图中的每个像素单元都包含用于编码单个像素的颜色透明度


图片解码

{光栅化|Raster}还会解码嵌入在页面中的图像资源

像素是怎样练成的

绘制操作引用了压缩数据(JPEG、PNG等),然后Raster调用相应的解码器对其进行解压缩。


GPU 进程中进行光栅化

渲染器进程是受沙盒保护的,因此它无法直接进行系统调用

命令缓冲区

光栅化的绘制操作被封装在GPU命令缓冲区中,以便通过IPC通道发送。

像素是怎样练成的


Skia

光栅化通过一个名为Skia的库调用OpenGL

Skia在硬件周围提供了一层抽象,并且能够理解更复杂的内容,如路径和贝塞尔曲线。

Skia是由Google维护的开源项目。它被集成在Chrome二进制文件中,但存在于一个单独的代码仓库中。

它还被其他产品(如Android操作系统)使用。Skia的GPU加速代码路径会构建自己的绘图操作缓冲区,在光栅化任务结束时进行刷新。

像素是怎样练成的


GPU加速生成位图

光栅化后的位图存储在内存中,通常是由OpenGL引用这些GPU内存。

像素是怎样练成的

GPU还可以执行生成位图的命令("加速光栅化")。

请注意,这些像素尚未显示在屏幕上!

绘制操作被发送到GPU进程进行光栅化。GPU进程可以发出真正的OpenGL调用

像素是怎样练成的


页面状态发生变化

上面所讲的流程从DOM=>style=>layout=>paint=>raster=>gpu是页面内容到内存中像素的全流程。但是,渲染过程不是静态的,而是需要无时无刻的将页面状态变化也要考虑进去。

所以,就又引入了我们下面的思考,页面状态发生变化该如何处理。

像素是怎样练成的


在讲变化前,我们再来介绍几个概念。

几个关于帧的知识点

  • 屏幕刷新频率
    • 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz。
    • 刷新频率取决于硬件的固定参数(不会变的)。
  • 逐行扫描:
    • 显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。
    • 以 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms
  • 帧率 (Frame Rate) :
    • 表示 GPU 在一秒内绘制操作的帧数,单位 fps。
    • 例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。
    • 帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
  • 画面撕裂(tearing):
    • 一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。

每个帧是内容在特定时间点的完整渲染状态

像素是怎样练成的


图层

{图层|Layer}是页面的一部分,可以独立于其他图层进行变换和光栅化。

像素是怎样练成的


{图层|Layer}

我们通过一个真实的案例,来看一下图层,并且它是如何被处理的。

有一个shake样式,它的作用是将指定的元素设置transform:rotate(xx)。让其可以实现在原本位置处,摆动。而这种情况,就是一个页面状态变化,是不能直接套用我们之前的渲染管道了。(我们之前的渲染管道是针对静态页面的)

.shake {
  animation: shake 1s infinite;
  transform-origin: center;
  width:100px;
  height:50px;
}

@keyframes shake {
  0% {
    transform: rotate(0);
  }
  50% {
    transform: rotate(-22.5deg);
  }
  100% {
    transform: rotate(22.5deg);
  }
}

示例1

现在有如下的页面结构

<div>  
  AAA  
  <p class="shake"> BBB </p>
</div>
CCC

通过浏览器的处理后,可以发现现在有了一个单独的layer 区域。 像素是怎样练成的

而页面中呈现的效果如下。 像素是怎样练成的


示例2

页面结构如下:

<div class="shake">  
  AAA  
  <p > BBB </p>
</div>
CCC

layer结构: 像素是怎样练成的 页面呈现效果 像素是怎样练成的


{合成|Compositing}

Compositing is the process or technique of combining visual elements from separate sources into single images, often to create the illusion that all those elements are parts of the same scene. -- 来自维基百科

翻译后的大概意思就是: {合成|Compositing}将来自不同来源的视觉元素组合成单一图像的过程或技术,通常是为了创造所有这些元素是同一场景的一部分的错觉。

下面我们直接看看在页面中通过新增不同的动画效果而合成的视觉效果

像素是怎样练成的

像素是怎样练成的

像素是怎样练成的


合成线程接收输入事件

像素是怎样练成的


图层提升(Layer Promotion)

某些样式属性会导致为布局对象创建一个图层。

像素是怎样练成的

只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:

  1. transform:3D变换:translate3dtranslateZ
  2. will-change:opacity | transform | filter
  3. opacity | transform | fliter 应用了过渡和动画(transition/animation
  4. video、canvas、iframe

其实,这里还和层叠上下文有牵扯,这里不展开说明了,具体可以参考CSS重点概念精讲


scrolling layers

像素是怎样练成的


合成分配(Compositing Assignments)生成 property trees

构建{图层树|Layer Tree}是主线程上的一个新的生命周期阶段。

目前,在{绘制|Paint}之前进行图层树的构建,并且每个图层单独进行绘制。

像素是怎样练成的


property trees

合成器可以对绘制图层的方式应用各种属性。这些属性存储在它们自己的树中。

像素是怎样练成的

像素是怎样练成的


Commit

在绘制完成后,提交(Commit)操作会在合成线程上更新图层列表和属性树的副本,以使其与主线程上的数据结构状态保持一致。

像素是怎样练成的


分割成瓦片(Tiling)

像素是怎样练成的

光栅化是在绘制之后的步骤,它将绘制操作转换为位图。图层可能很大,如果只有一部分可见,那么对整个图层进行光栅化既耗时间又没必要。

因此,合成线程将图层分割为{瓦片|Tiling}

瓦片是光栅化工作的单位。

瓦片使用专用的光栅化线程池进行光栅化。瓦片的优先级基于它们与{视口|Viewport}的距离。


瓦片被绘制为{四边形|Quads}

一旦所有瓦片完成光栅化,合成线程将生成“绘制四边形”(Draw Quads)。

四边形类似于在屏幕上的特定位置绘制一个瓦片的命令,考虑了图层树应用的所有变换。每个四边形引用了内存中瓦片的光栅化输出。四边形被封装在一个合成器帧对象中,并提交给浏览器进程。

像素是怎样练成的


Display(viz)

{合成帧|Compositor Frame}来自多个渲染器和浏览器(浏览器有自己的用于 UI 的合成器)。

{合成帧|Compositor Frame}与一个{表面|surfaces}相关联,表示它们将显示在屏幕上的位置。

{表面|surfaces}可以嵌入其他{表面|surfaces}

浏览器 UI 嵌入一个渲染器。渲染器可以嵌入其他渲染器,用于跨源 iframe(也称为站点隔离、"out of process iframe" 或 OOPIF)。

显示合成器在 GPU 进程的 Viz 合成器线程上运行。

  • 它是 viz 服务(简称为 "visuals")的一部分。

显示合成器接收传入的帧,并理解嵌入{表面|surfaces}之间的依赖关系("surface aggregation")

像素是怎样练成的


Viz 显示四边形

Viz还会发出GL调用来显示{合成帧|Compositor Frame}中的每个四边形。

这些GL调用在viz合成线程上,它们通过命令缓冲区进行序列化和代理,发送到GPU主线程,在那里解码器会发出真正的GL调用。

像素是怎样练成的


双缓存

为什么要设置双缓存?解决画面撕裂!那何为画面撕裂呢?

画面撕裂原因

屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。

简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。

双缓存

那咋解决画面撕裂呢?答案是使用 双缓存

由于图像绘制屏幕读取使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。

双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame/Front Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:

像素是怎样练成的

双缓存,CPU/GPU写数据到Back Buffer,显示器从Frame Buffer取数据

VSync(垂直同步信号)

问题又来了:什么时候进行两个buffer的交换呢?

假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。

当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。

VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。

像素是怎样练成的


一图胜千言

像素是怎样练成的


后记

分享是一种态度

参考资料:

  1. Life of a Pixel
  2. 页面是如何生成的(宏观角度)
  3. RenderingNG中关键数据结构及其角色
  4. chromium结构
  5. CSS重点概念精讲
  6. 维基百科

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

像素是怎样练成的

转载自:https://juejin.cn/post/7249626243124789309
评论
请登录