像素是怎样练成的
{万物皆有裂痕,那是光照进来的地方|There's a track,a crack in everying .That's how the light gets in.}
大家好,我是柒八九。
前言
本来呢,最近在规划一篇关于浏览器的文章,但是在做文章架构梳理和相关资料查询的时候,发现浏览器在渲染页面的过程中,也别有洞天。索性,就单独将其作为一篇文章来写。
这里有几点说明。
- 平时开发中用的是
Chrome
浏览器,所以下面的文章都是以Chrome
浏览器为准。 - 本文中,
Chromium
和Chrome
可以认同是一个东西,不做强制区分,理由下文会讲到。 - 该篇文章过于长,可以按照自己喜好,酌情选读文章内容。
- 该篇文章主要讲浏览器内部渲染像素(页面)角度分析,而不会涉及到网络处理/JS解析等。有的话也是一带而过,我们后期会专门有一篇文章,带大家串一下流程的。
还有,之前我们写过浏览器相关
的知识点,如果想了解该系列文章(浏览器相关),可以参考我们已经发布的文章。如下是往期文章。
你能所学到的知识点
- 前置知识 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
Chromium
和Chrome
的关系Chromium
架构简析- 何为网页内容
- 何为{像素|Pixels}
- Chrome渲染过程是反复进行的
- 页面数据解析 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- HTML 解析为 DOM
- CSS 解析为 CSSOM
- 布局阶段生成 {不可变fragment树|immutable fragment tree}推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- {绘制|Paint}阶段生成{显示列表|Display List} 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- {光栅化|Raster}将部分{显示列表|Display List}转换位{位图|BitMap} 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
- GPU 进程中进行光栅化
- 页面状态发生变化
- 动画帧生成图层
- 合成分配(Compositing Assignments)发生在绘制之前
- 合成生成 property trees
- Display(viz)
- 双缓存
好了,天不早了,干点正事哇。
前置知识
Chromium 和 Chrome的关系
Chromium
和Chrome
之间存在密切的关系,可以理解为Chromium
是Chrome
的开源项目。
-
Chromium
:Chromium
是一个开源的Web浏览器项目,由Google
主导开发。它是一个完全开放的项目,源代码可以公开获取并进行自由修改。Chromium
项目包括浏览器引擎Blink
、JavaScript引擎V8
等组件。Chromium
致力于提供一个可扩展、快速和安全的Web浏览器解决方案,同时也是许多其他基于Chromium
的浏览器的基础。
-
Chrome
:Chrome
是由Google
基于Chromium
项目开发的Web浏览器。- 它是
Chromium
的商业版本
,针对普通用户提供了更多功能和服务。 Chrome
具有更多的集成功能,包括自动更新、PDF阅读器、Google账号同步
等。- 此外,
Chrome
还包括一些针对企业用户和开发人员的工具和功能。
- 它是
可以将
Chromium
视为Chrome
的基础,Chrome
在此基础上添加了自己的功能和服务。
想必大家都有Chrome
浏览器,我们可以做一个验证,大家在地址栏
中输入chrome://settings/help
或者按照如下的步骤。
关于Chorme
和Chromium
的关系就映入眼帘。如下图所示。
关于它们之间的关系,我们就不在赘述。
其它奇怪的知识
其实Chromium
也是可以被下载,同时也可以作为搜索引擎的。
市面上,很多浏览器都是基于Chromium
浏览器开发的。
Edge 浏览器
360浏览器
Chromium
架构简析
Chromium
被分成两个主要部分(不包括其他库):{浏览器|Browser}和{渲染器|Renderer}(包括Blink
,网络引擎)。
- {浏览器|Browser}是主进程,代表所有的
用户界面
和I/O
- 负责运行
用户界面
并管理{渲染器|Renderer}和其他进程 - 也称为
"浏览器进程"
或简称为{浏览器|Browser}
- 负责运行
- {渲染器|Renderer}是由{浏览器进程|Browser Process}驱动的子进程
- 渲染器使用
Blink
开源布局引擎来解释和布局HTML
。
- 渲染器使用
它们的关系如下
从源码架构角度来看Chromium
每个框代表一个
应用层
。任何一个低层级都不依赖于更高层级的内容。
我们按照从底层到顶层的顺序,来简单介绍下,每个层级的作用。
WebKit
:在Safari
、Chromium
和其他基于WebKit
的浏览器的渲染引擎。- 端口(
Port
)是WebKit
的一部分,它与平台相关的系统服务(如资源加载和图形生成)进行集成。
- 端口(
Glue
:将WebKit
的数据类型转换为Chromium
的数据类型的组件。- 这是
"WebKit嵌入层"
- 它是
Chromium
的基础。
- 这是
Renderer
/Render host
:这是Chromium
的"多进程嵌入层"
。- 它在进程上进行
代理通知
和命令发送
。
- 它在进程上进行
WebContents
:是Content
模块的组件。- 它可以轻松地嵌入到视图中,实现HTML的多进程渲染
- {浏览器|Browser}:代表浏览器窗口
- 它包含多个WebContents
Tab Helpers
:附加到WebContents
上的单独对象。- 浏览器将各种
助手对象
附加到它所持有的WebContents
上(如网站图标、信息栏等)。
- 浏览器将各种
将上面的比较生硬的词汇替换一下,然后就可以画出下面的关于Chromium
架构图。
这块的架构图,有些生硬,后期也会有专门的文章来进行讲解。
通过上文介绍,我们得到一个结论 :Chromium
≈ chrome
。 所以,下文中可以将Chromium
和chrome
看成一个东西。
下图是chrome
将content
生成页面信息的示意图。
何为网页内容
在
Chromium C++
代码库中,在架构层面上content
负责红色框中的所有内容。(可以看上面的架构图) 而Tabs
、地址栏
、导航按钮
、菜单
等不在content
的范围内。
Chrome
安全模型的关键是渲染发生在沙盒化的进程中。
Blink
是{渲染器|Renderer}进程中的代码子集,在content
命名空间内。Blink
实现了Web平台API和Web规范的语义。
{渲染器|Renderer}进程还运行一个称为{合成器|compositor}("cc"
)的组件。
对应的关系如下:(从进程和线程的关系角度看)
页面内容分类
content
是Chromium
中用于表示网页内部或 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.0 ,0.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}是一种用于表示和操作HTML
、XML
和XHTML
文档的编程接口。它将文档解析为一个由{节点|Node}和{对象|Object}组成的树形结构,这个树形结构被称为DOM树
。
DOM树
的根节点是{文档节点|Document Node},它代表整个文档。文档节点
下方是{元素节点| Element Node},表示HTML或XML文档中的标签。元素节点可以包含其他元素节点、{文本节点| Text Node}、{注释节点| Comment Node}等。
每个节点在DOM中都有特定的属性和方法,可以用于访问和操作节点的内容、属性和样式。一些常见的节点类型包括:
- {元素节点| Element Node}:代表
HTML
或XML
文档中的标签,如<div>
、<p>
、<a>
等。- 可以通过节点的标签名、属性和子节点等进行操作。
- {文本节点| Text Node}:代表元素节点中的文本内容,即标签之间的文本。
- {注释节点| Comment Node}:代表文档中的注释部分,以
<!--
开头和-->
结尾。 - {属性节点|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个
瞄准目标元素
- 类选择器
- 以
.
开头
- 以
- ID选择器
#
开头- 权重相当高
- ID一般指向唯一元素
- 属性选择器
- 含有
[]
的选择器 [title]{}/[title="test"]{}
- 含有
- 伪类选择器
- 前面有一个冒号(
:
)的选择器 :link
:选择未被访问的链接:visited
:选取已被访问的链接:active
:选择活动链接:hover
:鼠标指针浮动在上面的元素
- 前面有一个冒号(
- 伪元素选择器
- 有连续两个冒号(
::
)的选择器 ::before
: 选择器在被选元素的内容前面插入内容::after
: 选择器在被选元素的内容后面插入内容
- 有连续两个冒号(
关系选择器 (空格>~+
)4个
根据与其他元素的关系选择元素的选择器
- 后代选择器
- 选择所有合乎规则的后代元素
- 空格链接
- 相邻后代选择器
- 仅仅选择合乎规则的儿子元素
- 孙子,重孙子元素忽略
>
链接
- 兄弟选择器
- 选择当前元素后面的所有合乎规则的兄弟元素
~
链接
- 相邻兄弟选择器
- 仅仅选择当前元素相邻的那个合乎规则的兄弟元素
+
链接- 常见的使用场景是,改变紧跟着一个标题的段的某些表现方面
权重
!important
(10000
)- 内联(
1000
)- ID选择器(
0100
)- 类选择器(
0010
)- 标签选择器(
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
是一个用于获取元素计算后的样式的方法。
document.styleSheets
:- 这是一个属性,用于获取文档中
所有的样式表
(style sheets)。 - 可以使用
document.styleSheets
返回的样式表集合来访问和操作具体的样式表。
- 这是一个属性,用于获取文档中
window.getComputedStyle(element)
:- 这是一个方法,用于获取
指定元素的计算样式
(computed style)。 - 可以通过传递元素对象给
getComputedStyle
方法来获取该元素应用的最终样式。
- 这是一个方法,用于获取
document.styleSheets[i].cssRules
:- 这是一个属性,用于获取样式表中的所有规则(rules)。
- 可以使用
cssRules
属性返回的规则集合来访问和操作具体的样式规则。
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}表示元素发生层叠时有着特定的垂直显示顺序
一旦普通元素具有层叠上下文
,其层叠顺序就会变高
分两种情况
- 如果层叠上下文元素不依赖
z-index
数值,则其层叠顺序是z-index:auto
- 可看成
z-index:0
- 可看成
- 如果层叠上下文元素依赖
z-index
数值,则其层叠顺序由z-index
值决定
按照层叠顺序进行页面绘制
按正确的顺序绘制元素非常重要,这样它们在重叠时才能正确叠放。绘制顺序可以通过样式来控制。
绘制顺序是按照层叠顺序,而不是DOM顺序
可以看到,虽然yellow
的DOM顺序在green
的DOM之前,但是在绘制到页面上时,yellow
在green
的上面。(yellow
Z轴大)
每个绘制过程都是对层叠上下文的单独遍历
甚至有可能一个元素部分在另一个元素前面,部分在后面。这是因为绘制过程分为多个阶段,每个绘制阶段都会对子树单独遍历。
存在如下的页面结构:
<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)
某些样式属性会导致为布局对象创建一个图层。
只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:
transform:3D
变换:translate3d
,translateZ
;will-change:opacity | transform | filter
- 对
opacity
|transform
|fliter
应用了过渡和动画(transition/animation
) 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
准备就绪后,它们才进行交换。如下图:
VSync(垂直同步信号)
问题又来了:什么时候进行两个buffer的交换呢?
假如是 Back buffer
准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)
。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing
的状况。
VSync(垂直同步)是VerticalSynchronization
的简写,它利用VBI时期出现的vertical sync pulse
(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
一图胜千言
后记
分享是一种态度。
参考资料:
- Life of a Pixel
- 页面是如何生成的(宏观角度)
- RenderingNG中关键数据结构及其角色
- chromium结构
- CSS重点概念精讲
- 维基百科
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
转载自:https://juejin.cn/post/7249626243124789309