像素的一生
Posted yerikyu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了像素的一生相关的知识,希望对你有一定的参考价值。
提到浏览器不得不说Chrome,Chrome是Google发行的商业产品,而Chromium是一个开源版本的Chrome,两者很像但是不完全一样。
写这篇文章是想追忆像素的由来,我们且从chrome入手,窥探其内核是如何将web内容转换为像素。
渲染
事实上这个转换的过程就是渲染,网页的渲染可以表示为Content
经过rendering
最后呈现的过程,即*Code -> 可交互的页面*
简单的说浏览器作为应用,底层分别有content,Blink,V8,Skia等等,一层一层像套娃一样一层引用一层。对比普通应用的项目来说就是不断用第三方库和组件来拼凑应用,Chrome也不例外
- content可以理解为就是除了浏览器主进程下的书签导航之外,网页内容这一部分,会随着网页不同而变化的部分
- Blink渲染引擎,应该都听过就是网页的排版引擎,现存的Chrome/Edge都在用,作为开源项目维护,是在渲染进程里,其实现了 Web 平台中API 和 Web 规范的语义。
- Blink又嵌套了V8 javascript engine来执行JS代码
何为content
可以看到content就是WebContents对象,C++代码的一个类。其代表的区域其实是标签页页打开的部分(即下图红色部分)。而浏览器主进程还包含有地址栏、导航按钮、菜单、扩展,安全提示的小弹窗等等。
在Chrome中其安全模型实现的关键:渲染发生在沙盒进程中。这么做的好处在于当渲染进程render process
挂了不会引起整个浏览器停止服务
渲染进程render process
包含Blink渲染排版引擎和Chromium compositor
(图中绿色的CC简写)
作为content来说,其基本构建块是文本、图像、标记(围绕文本)、样式(定义标记的呈现方式)和脚本(可以动态修改上述所有内容)。
当然了,其他类型的内容以特殊的方式呈现比如video
, canvas
, WebAssembly
, WebGL
, WebVR
, PDF
, ...,这里不做讨论。
在古早时期,当时的网页只是通过网络以纯文本形式提供的数千行 html、CSS 和 JavaScript。
那个时候没有编译或打包的概念,然而这种简单性是网络早期成功的关键。
综上,content就是网页代码最后运行的结果,浏览器开发者工具可以看到最后是一个经过处理后的HTML的结构。而这个HTML在渲染流水线里是一个输入:)
像素的一生
像素pixels
到底是怎么出现的呢?为何一串简简单单普普通通的代码可以获的满屏的色彩,彷佛一念花开,一念世界。
写过C/C++代码的同学知道,我们必须使用操作系统提供的底层API去画图,通过操作系统底层去调用驱动程序,令驱动程序驱动硬件将图形库的像素放到屏幕上。
今天大多数平台上都提供了OpenGL
的标准化API。在Windows上有一个额外的DirectX
转换。这些库提供诸如“纹理”和“着色器”之类的低级图形基元,并允许执行类似“在这些坐标处绘制一个三角形到虚拟像素缓冲区”之类的底层操作。未来计划用Vulkan替代Skia来做底层图形化调用。
因此渲染流水线的整个过程就是将输入的HTML、CSS、JS转化为OpenGL调用,最后在屏幕上呈现像素
像素的意义
简单来说,像素就是为了可以更加舒服的表达自身的意义,在此认为像素意义在于两种渲染:
- 初次渲染,将网页内容转化为底层OpenGL调用去显示页面
- 再次渲染,在JS运行,用户输入,异步请求或者滑动等交互介入后,再次渲染页面起到交互的目的,
随着时间推移,每个渲染阶段的结果会为了提高渲染效率而被缓存下来。此外还有JS API会查询一些渲染数据如某个DOM节点的信息
渲染阶段
我们不妨将把渲染管道分成多个阶段,每个阶段都是像素生命周期的一个环节,从图中可以看出原来的content
内容会被各个阶段stage
处理为中间数据,最后才呈现为画面呈现出来。
parsing
HTML 标签在文档上强加了一个语义上有意义的层次结构。 例如,一个<div>
可能包含两个段落,每个段落都有文本。 所以第一步是解析这些标签来构建一个反映这种结构的对象模型。
DOM
我们常说DOM树的原因,通过一层层铺垫的结构,形似一棵树
其是JavaScript
操作网页的接口,全称为文档对象模型Document Object Model
。我们主要关注三个概念:文档、元素、节点
- 整个文档是一个文档节点
- 每个标签是一个元素节点
- 包含在元素中的文本是文本节点
- 每一个属性是一个属性节点
- 注释属于注释节点
因此常用的操作DOM的五种方法:
- getElemenById
- getElementsByTagName
- getElementsByClassName
- getAttribute
- setAttribute
DOM(Document Object Model)本质上是一棵树,树有父子,邻居的关系,而且这棵树是暴露API给JS调用,JS可以查询和修改这棵树。JS引擎V8通过bindings
的系统将DOM包装为DOM API供给Web开发者调用
在生产、学习的过程中,我们不可避免的需要在同一份文档中夹带多份DOM树,树多了就成了森林,对于森林的处理则是采用影子树shadow tree
的形式对当前的DOM树进行套娃。
还记得我们在使用入vue中经常会采用的一种特性,v-slot
,其本质上就是应用了影子树,
如下图的示例,自定义元素custom element有shadow tree。ShadowRoot的子元素其实被嵌入到slot元素里了
本质上最后是在遍历树后合成视图,也就是两棵树合并为一棵树
style
构建 DOM 树后,下一步是处理 CSS 样式。CSS 选择器选择其属性声明应应用于的 DOM 元素子集。
![image](E91BB520A37C4614AA7C8C8BB491BF78)
通过style这个属性,我们可以对像素进行各种个性化处理,如旋转跳跃、浮动变色、黯淡闪现等等,当然了这些属性也不能太浪,有可能会出现一些使用上的冲突,因此现在前端工程中定义了一种新的专门的编程语言,可以为CSS增加一些编程的特性,编译后成正常的CSS文件。具有无需考虑浏览器的兼容问题,让CSS更加简洁,适应性更强,可读性更佳,更易于代码的维护等诸多好处。
目前常见的css的预处理器
- less
- scss
- sass
实现原理
CSS 解析器根据每个活动样式表构建样式规则模型。
样式规则以各种方式索引以进行有效查找。
如上图所示属性类在构建时由Python脚本自动生成,以声明方式定义了所有样式属性,如右上侧css_properties.json
经过py脚本转化为.cc
文件
样式表可能位于项目工程中<style>
元素、单独加载的资源 (styles.css) 或由浏览器提供。
样式解析(或重新计算)从活动样式表中获取所有已解析的样式规则,并计算每个 DOM 元素的每个样式属性的最终值。 这些存储在一个名为ComputedStyle
的对象中,本质上它只是从样式属性到值的映射。
我们可以在开发者工具中发现所有 DOM 元素的ComputedStyle
。它也暴露在Javascript
中。 这些都是基于Blink
的ComputedStyle
对象,注意到有时候一些属性增加了布局layout
数据。我们还可以通过getComputedStyle
的JSAPI
去获取。
layout
在构建了 DOM 并计算了所有style之后,下一步是确定所有元素的视觉几何形状。
对于这个块级元素,我们正在计算一个矩形的坐标,该矩形对应于该元素占据的内容区域的几何区域,如计算x
,y
,width
,height
这些数据
在最简单的情况下,布局按 DOM 顺序一个接一个地放置块,垂直下降。 我们称之为“块流”。
文字和内联元素如<span>
则是左右浮动的,而且内联元素会被行尾打断(自动换行)。当然也有从右到左的语言,比如阿拉伯语和希伯来语
当然了布局也包括字体的排列,因为布局需要考虑文本在那里进行换行, Layout
使用名为 HarfBuzz
的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。字体成型必须考虑到排版特征,如字距调整 letter-spacing
和连字。
布局可以计算单个元素的多种边界矩形。例如,当存在溢出时, Layout
将同时计算边界框和布局溢出。如果节点的溢出是可滚动的, Layout
还会计算滚动边界并为滚动条预留空间。最常见的可滚动DOM节点是文档本身
表格元素或样式需要更复杂的布局,这些元素或样式指定诸如将内容分成多列、位于一侧且内容在其周围流动的浮动对象、或文本垂直而不是水平排列的东亚语言。
请注意 DOM 结构和 ComputedStyle
值(如“float: left”)如何作为布局算法的输入。
此外渲染流水线的每个阶段都会使用到前面阶段的结果
![image](54929B36E302485FBC111030C450CC73)
通过遍历DOM树创建渲染树LayoutTree
,节点一一对应。布局树中的节点实现布局算法。根据所需的布局行为,LayoutObject
有不同的子类。比如LayoutBlockFlow
就是块级Flow
的文档节点。样式更新阶段也构建布局树。
在样式解析最后结束时需要构建布局树LayoutTree
,布局阶段遍历布局树,对布局树每个节点LayoutObject
执行布局,计算几何数据、换行符,滚动条等。
DOM
节点跟Layout
节点不一定是一一对应
一般情况下一个DOM节点会有一个LayoutObject,但是有时候 LayoutObject
是没有DOM节点与之对应的。
比如上图,span标签外部没有section标签嵌套,但是LayoutTree会自动创建 LayoutBlock
的匿名节点与之对应,再比如样式有 display:none
的样式,那么也不会创建对应的 LayoutTree
。
最后,如果是 shadowTree
的话,其 LayoutObject
节点可能会在不同的 TreeScope
里
layout引擎的未来
LayoutNG
代表下一代的布局引擎,2020年布局引擎还在过渡阶段,所以有中间形态,如上图包含了LayoutObject
和LayoutNGMixin
混合节点。未来所有节点都会变成LayoutNG
的layout object
NG节点的更新主要是因为之前的节点包含了输入、输出还有布局算法的信息,也就是说单个节点可以看到整棵树的状态(节点有可能需要获取父节点的宽高数据,但是父节点正在递归子节点布局中,实际上还没确定最后的布局)。
而新的NG节点对输入和输出做了明显的区分,而且输出是immutable
的,可以缓存结果
布局结果指向描述物理几何的片段树,如图一个NGLayoutResult
对应几个NGPhysicalFragment
,对应右上角的几个矩形图形,如果NGLayoutResult
没变化则对应整块都不会变化。
实例
大家且看这段代码会渲染出什么效果
<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树如下图所示
那如果用LayoutTree
来表示呢?其实Layout树和DOM树很像,节点几乎是一一对应的,但是注意这里anonymous
匿名节点被创建出来,它只有一个块级子元素。一个布局节点只能拥有块级元素或者内联元素其中之一
图中的子元素前面两个其实共享了匿名LayoutNGBlockFlow
,也就是说有共同的父节点
paint
绘制paint
阶段创建绘制指令列表paint ops list
绘制指令paint op
可以理解为在某些坐标用什么颜色画一个矩形类似的意思,
每个布局对象LayoutObejct
可以有多个显示项目,对应于其视觉外观的不同部分,如背景、前景、轮廓等
正确的绘制顺序非常重要,这样当元素重叠时,它们才能正确堆叠。顺序可以由样式控制,而不是完全依靠DOM的先后顺序
每个绘制阶段paint phase
都需单独遍历堆叠上下文staking context
。
一个元素甚至可能部分位于另一个元素的前面,部分位于另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对自己的子树进行遍历。
例子
且看这段代码渲染出来的效果
<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节点渲染出来的结果,包含了四个绘制指令paint ops:
-
document
背景色绘制 - 块级元素的背景色绘制
- 块级元素的前景色绘制(包含文本的绘制)
文本绘制操作包含文本块的绘制,其中包含每个字的字符和偏移量以及字体。如图这些数据都是HarfBuzz计算后得到的raster
中文说的栅格化或者光栅化,本文取PS图层右键的栅格化为译文。熟悉PS的会知道矢量图形栅格化后放大图形会"糊"是不做栅格化处理直接放大矢量图形则不会。原因就是栅格化后只记录了单像素点的rgba
值,放大后本来一个点数据要填满N个点,图像就"糊"
raster
raster
将绘制指令转化为位图,可以把显示列表里的绘制操作执行的过程,成为任务,也称栅格化。比如PS里的合并图层任务,主要区别就是本来矢量的图任务后会变成位图 bitmap
,后面再缩放就会模糊。
生成的位图 bitmap
中的每个单元格都包含对单个像素的颜色和透明度进行编码的位。这里用十六进制 FFFFFFFF
表示一个点的 rgba
值
其还对嵌入在页面中的图像资源进行解码。 绘制操作引用压缩数据(JPEG、PNG 等),然后 raster 调用适当的解码器对其进行解压缩。
GPU加速
GPU还可以运行生成位图的命令(“加速栅格化”)。请注意,此时这些像素还没有出现在屏幕上
raster
产生的位图数据存储在GPU内存中,通常是OpenGL纹理对象引用的GPU内存。
过去通常是存在内存里再传给GPU,但是现代GPU可以直接运行着色器shader并在GPU上生成像素,这种情况称为“加速栅格化”。但是两个结果都是一致的,最终内存(主存或者GPU内存)里拥有位图 bitmap
raster通过Skia发出GL调用
添加到视频合成并导出时,叠加图像会变得像素化