像素的一生

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代码

像素的一生_浏览器原理_02

何为content

可以看到content就是WebContents对象,C++代码的一个类。其代表的区域其实是标签页页打开的部分(即下图红色部分)。而浏览器主进程还包含有地址栏、导航按钮、菜单、扩展,安全提示的小弹窗等等。
在Chrome中其安全模型实现的关键:渲染发生在沙盒进程中。这么做的好处在于当渲染进程​​render process​​挂了不会引起整个浏览器停止服务

渲染进程​​render process​​包含Blink渲染排版引擎和​​Chromium compositor​​(图中绿色的CC简写)

像素的一生_数据_03
作为content来说,其基本构建块是文本、图像、标记(围绕文本)、样式(定义标记的呈现方式)和脚本(可以动态修改上述所有内容)。
当然了,其他类型的内容以特殊的方式呈现比如​​​video​​​, ​​canvas​​​, ​​WebAssembly​​​, ​​WebGL​​​, ​​WebVR​​​, ​​PDF​​, ...,这里不做讨论。

像素的一生_主线程_04
在古早时期,当时的网页只是通过网络以纯文本形式提供的数千行 html、CSS 和 JavaScript。
那个时候没有编译或打包的概念,然而这种简单性是网络早期成功的关键。
像素的一生_栅格_05
综上,content就是网页代码最后运行的结果,浏览器开发者工具可以看到最后是一个经过处理后的HTML的结构。而这个HTML在渲染流水线里是一个输入:)

像素的一生

像素​​pixels​​到底是怎么出现的呢?为何一串简简单单普普通通的代码可以获的满屏的色彩,彷佛一念花开,一念世界。

像素的一生_栅格_06


写过C/C++代码的同学知道,我们必须使用操作系统提供的底层API去画图,通过操作系统底层去调用驱动程序,令驱动程序驱动硬件将图形库的像素放到屏幕上。
今天大多数平台上都提供了​​​OpenGL​​​的标准化API。在Windows上有一个额外的​​DirectX​​转换。这些库提供诸如“纹理”和“着色器”之类的低级图形基元,并允许执行类似“在这些坐标处绘制一个三角形到虚拟像素缓冲区”之类的底层操作。未来计划用Vulkan替代Skia来做底层图形化调用。

因此渲染流水线的整个过程就是将输入的HTML、CSS、JS转化为OpenGL调用,最后在屏幕上呈现像素

像素的一生_数据_07


像素的意义

简单来说,像素就是为了可以更加舒服的表达自身的意义,在此认为像素意义在于两种渲染:

  • 初次渲染,将网页内容转化为底层OpenGL调用去显示页面
  • 再次渲染,在JS运行,用户输入,异步请求或者滑动等交互介入后,再次渲染页面起到交互的目的,

像素的一生_浏览器原理_08
随着时间推移,每个渲染阶段的结果会为了提高渲染效率而被缓存下来。此外还有JS API会查询一些渲染数据如某个DOM节点的信息

渲染阶段

我们不妨将把渲染管道分成多个阶段,每个阶段都是像素生命周期的一个环节,从图中可以看出原来的​​content​​内容会被各个阶段​​stage​​处理为中间数据,最后才呈现为画面呈现出来。

像素的一生_主线程_09

parsing

HTML 标签在文档上强加了一个语义上有意义的层次结构。 例如,一个​​<div>​​可能包含两个段落,每个段落都有文本。 所以第一步是解析这些标签来构建一个反映这种结构的对象模型。

像素的一生_数据_10

DOM

我们常说DOM树的原因,通过一层层铺垫的结构,形似一棵树
像素的一生_数据_11
其是​​​JavaScript​​​操作网页的接口,全称为文档对象模型​​Document Object Model​​。我们主要关注三个概念:文档、元素、节点

  • 整个文档是一个文档节点
  • 每个标签是一个元素节点
  • 包含在元素中的文本是文本节点
  • 每一个属性是一个属性节点
  • 注释属于注释节点

因此常用的操作DOM的五种方法:

  • getElemenById
  • getElementsByTagName
  • getElementsByClassName
  • getAttribute
  • setAttribute

DOM(Document Object Model)本质上是一棵树,树有父子,邻居的关系,而且这棵树是暴露API给JS调用,JS可以查询和修改这棵树。JS引擎V8通过​​bindings​​的系统将DOM包装为DOM API供给Web开发者调用

像素的一生_主线程_12
在生产、学习的过程中,我们不可避免的需要在同一份文档中夹带多份DOM树,树多了就成了森林,对于森林的处理则是采用影子树​​​shadow tree​​的形式对当前的DOM树进行套娃。

还记得我们在使用入vue中经常会采用的一种特性,​​v-slot​​,其本质上就是应用了影子树,

像素的一生_栅格_13
如下图的示例,自定义元素custom element有shadow tree。ShadowRoot的子元素其实被嵌入到slot元素里了
像素的一生_栅格_14
本质上最后是在遍历树后合成视图,也就是两棵树合并为一棵树

style

构建 DOM 树后,下一步是处理 CSS 样式。CSS 选择器选择其属性声明应应用于的 DOM 元素子集。
![image](E91BB520A37C4614AA7C8C8BB491BF78)
通过style这个属性,我们可以对像素进行各种个性化处理,如旋转跳跃、浮动变色、黯淡闪现等等,当然了这些属性也不能太浪,有可能会出现一些使用上的冲突,因此现在前端工程中定义了一种新的专门的编程语言,可以为CSS增加一些编程的特性,编译后成正常的CSS文件。具有无需考虑浏览器的兼容问题,让CSS更加简洁,适应性更强,可读性更佳,更易于代码的维护等诸多好处。
目前常见的css的预处理器

  • less
  • scss
  • sass

实现原理

像素的一生_浏览器原理_15
CSS 解析器根据每个活动样式表构建样式规则模型。

样式规则以各种方式索引以进行有效查找。

如上图所示属性类在构建时由Python脚本自动生成,以声明方式定义了所有样式属性,如右上侧​​css_properties.json​​经过py脚本转化为​​.cc​​文件


样式表可能位于项目工程中​​​<style>​​元素、单独加载的资源 (styles.css) 或由浏览器提供。

像素的一生_栅格_16

样式解析(或重新计算)从活动样式表中获取所有已解析的样式规则,并计算每个 DOM 元素的每个样式属性的最终值。 这些存储在一个名为​​​ComputedStyle​​ 的对象中,本质上它只是从样式属性到值的映射。

像素的一生_主线程_17
我们可以在开发者工具中发现所有 DOM 元素的​​​ComputedStyle​​​。它也暴露在​​Javascript​​​中。 这些都是基于​​Blink​​​的​​ComputedStyle​​​对象,注意到有时候一些属性增加了布局​​layout​​​数据。我们还可以通过​​getComputedStyle​​​的​​JSAPI​​去获取。

像素的一生_数据_18

layout

在构建了 DOM 并计算了所有style之后,下一步是确定所有元素的视觉几何形状。
对于这个块级元素,我们正在计算一个矩形的坐标,该矩形对应于该元素占据的内容区域的几何区域,如计算​​x​​,​​y​​,​​width​​,​​height​​这些数据

像素的一生_主线程_19
在最简单的情况下,布局按 DOM 顺序一个接一个地放置块,垂直下降。 我们称之为“块流”。

像素的一生_主线程_20


文字和内联元素如​​​<span>​​则是左右浮动的,而且内联元素会被行尾打断(自动换行)。当然也有从右到左的语言,比如阿拉伯语和希伯来语

像素的一生_浏览器原理_21
当然了布局也包括字体的排列,因为布局需要考虑文本在那里进行换行, ​​​Layout​​​ ​使用名为 ​​HarfBuzz​​​ ​的开源文本库来计算每个字形的大小和位置,这决定了文本的总宽度。字体成型必须考虑到排版特征,如字距调整 ​​letter-spacing​​​ ​和连字。
像素的一生_主线程_22
布局可以计算单个元素的多种边界矩形。例如,当存在溢出时, ​​​Layout​​​ ​将同时计算边界框和布局溢出。如果节点的溢出是可滚动的, ​​Layout​​ 还会计算滚动边界并为滚动条预留空间。最常见的可滚动DOM节点是文档本身

像素的一生_浏览器原理_23
表格元素或样式需要更复杂的布局,这些元素或样式指定诸如将内容分成多列、位于一侧且内容在其周围流动的浮动对象、或文本垂直而不是水平排列的东亚语言。
请注意 DOM 结构和 ​​​ComputedStyle​​ ​值(如“float: left”)如何作为布局算法的输入。
此外渲染流水线的每个阶段都会使用到前面阶段的结果
![image](54929B36E302485FBC111030C450CC73)
通过遍历DOM树创建渲染树​​​LayoutTree​​​,节点一一对应。布局树中的节点实现布局算法。根据所需的布局行为,​​LayoutObject​​​有不同的子类。比如​​LayoutBlockFlow​​​就是块级​​Flow​​的文档节点。样式更新阶段也构建布局树。

在样式解析最后结束时需要构建布局树​​LayoutTree​​,布局阶段遍历布局树,对布局树每个节点​​LayoutObject​​执行布局,计算几何数据、换行符,滚动条等。

​DOM​​节点跟​​Layout​​节点不一定是一一对应

像素的一生_栅格_24
一般情况下一个DOM节点会有一个LayoutObject,但是有时候 ​​​LayoutObject​​​ ​是没有DOM节点与之对应的。
比如上图,span标签外部没有section标签嵌套,但是LayoutTree会自动创建 ​​​LayoutBlock​​​ ​的匿名节点与之对应,再比如样式有 ​​display:none​​​ ​的样式,那么也不会创建对应的 ​​LayoutTree​​​ 。
最后,如果是 ​​​shadowTree​​​ ​的话,其 ​​LayoutObject​​​ ​节点可能会在不同的 ​​TreeScope​​ 里

像素的一生_主线程_25

layout引擎的未来

​LayoutNG​​代表下一代的布局引擎,2020年布局引擎还在过渡阶段,所以有中间形态,如上图包含了​​LayoutObject​​和​​LayoutNGMixin​​混合节点。未来所有节点都会变成​​LayoutNG​​的​​layout object​

像素的一生_栅格_26
NG节点的更新主要是因为之前的节点包含了输入、输出还有布局算法的信息,也就是说单个节点可以看到整棵树的状态(节点有可能需要获取父节点的宽高数据,但是父节点正在递归子节点布局中,实际上还没确定最后的布局)。
像素的一生_主线程_27
而新的NG节点对输入和输出做了明显的区分,而且输出是​​​immutable​​的,可以缓存结果

像素的一生_浏览器原理_28
布局结果指向描述物理几何的片段树,如图一个​​​NGLayoutResult​​​对应几​​个NGPhysicalFragment​​​,对应右上角的几个矩形图形,如果​​NGLayoutResult​​没变化则对应整块都不会变化。

像素的一生_数据_29

实例

大家且看这段代码会渲染出什么效果

<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>

如图所示
像素的一生_主线程_30
其对应的DOM树如下图所示
像素的一生_浏览器原理_31
那如果用​​​LayoutTree​​​来表示呢?其实Layout树和DOM树很像,节点几乎是一一对应的,但是注意这里​​anonymous​​匿名节点被创建出来,它只有一个块级子元素。一个布局节点只能拥有块级元素或者内联元素其中之一

图中的子元素前面两个其实共享了匿名​​LayoutNGBlockFlow​​,也就是说有共同的父节点

像素的一生_栅格_32

paint

绘制​​paint​​阶段创建绘制指令列表​​paint ops list​

绘制指令​​paint op​​可以理解为在某些坐标用什么颜色画一个矩形类似的意思,

每个布局对象​​LayoutObejct​​可以有多个显示项目,对应于其视觉外观的不同部分,如背景、前景、轮廓等

像素的一生_浏览器原理_33
正确的绘制顺序非常重要,这样当元素重叠时,它们才能正确堆叠。顺序可以由样式控制,而不是完全依靠DOM的先后顺序
像素的一生_浏览器原理_34
每个绘制阶段​​​paint phase​​​都需单独遍历堆叠上下文​​staking context​​。

一个元素甚至可能部分位于另一个元素的前面,部分位于另一个元素的后面。这是因为绘制在多个阶段中运行,每个绘制阶段都对自己的子树进行遍历。
像素的一生_栅格_35

例子

且看这段代码渲染出来的效果

<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>

像素的一生_主线程_36
我们不妨分析一下这个指令的解析过程,一个样式和DOM节点渲染出来的结果,包含了四个绘制指令paint ops:

  • ​document​​背景色绘制
  • 块级元素的背景色绘制
  • 块级元素的前景色绘制(包含文本的绘制)

像素的一生_浏览器原理_37
文本绘制操作包含文本块的绘制,其中包含每个字的字符和偏移量以及字体。如图这些数据都是HarfBuzz计算后得到的​​​raster​

中文说的栅格化或者光栅化,本文取PS图层右键的栅格化为译文。熟悉PS的会知道矢量图形栅格化后放大图形会"糊"是不做栅格化处理直接放大矢量图形则不会。原因就是栅格化后只记录了单像素点的​​rgba​​值,放大后本来一个点数据要填满N个点,图像就"糊"

像素的一生_浏览器原理_38

raster

​raster​​​ ​将绘制指令转化为位图,可以把显示列表里的绘制操作执行的过程,成为任务,也称栅格化。比如PS里的合并图层任务,主要区别就是本来矢量的图任务后会变成位图 ​​bitmap​​​ ​,后面再缩放就会模糊。
生成的位图 ​​​bitmap​​​ ​中的每个单元格都包含对单个像素的颜色和透明度进行编码的位。这里用十六进制 ​​FFFFFFFF​​​ ​表示一个点的 ​​rgba​​​ 值像素的一生_数据_39
其还对嵌入在页面中的图像资源进行解码。 绘制操作引用压缩数据(JPEG、PNG 等),然后 raster 调用适当的解码器对其进行解压缩。

像素的一生_主线程_40

GPU加速

GPU还可以运行生成位图的命令(“加速栅格化”)。请注意,此时这些像素还没有出现在屏幕上
​​​raster​​ ​产生的位图数据存储在GPU内存中,通常是OpenGL纹理对象引用的GPU内存。
过去通常是存在内存里再传给GPU,但是现代GPU可以直接运行着色器shader并在GPU上生成像素,这种情况称为“加速栅格化”。但是两个结果都是一致的,最终内存(主存或者GPU内存)里拥有位图 ​​​bitmap​​ ​
像素的一生_主线程_41
raster通过Skia发出GL调用

添加到视频合成并导出时,叠加图像会变得像素化

渲染出3D视频,单像素点实时渲染

学会这些逆天且实用的python技能,邻居家小孩这一生都会将你膜拜

使用 alpha 混合组件合成纹理

渲染出3D视频,单像素点实时渲染

07-07 生成器