深度知识

Chromium 内核 Layout 模块学习

前言:blink 是整个 Chromium 代码仓库中的渲染内核的实现。为什么要学习 blink 的 Layout 模块,一方面作为前端开发,增加对底层原理的理解,知其然并且知其所以然。另一方面最近参与 DOC 编辑器排版引擎的优化和功能迭代,通过学习最复杂的 blink 的排版引擎,了解通用的排版引擎的设计思路。

在重点学习浏览器的排版逻辑之前,先整体回顾下浏览器的整体的渲染流程:

1. blink 数据解析、排版、渲染的流程简介

a. 解析排版渲染流程

一个 Web 页面的展示,简单来说可以认为经历了以下下几个步骤。

  1. JavaScript 修改 DOM 结构或者 CSS 样式。

  2. 计算样式,这个过程是根据 CSS 选择器,确定每个 DOM 元素匹配对应的 CSS 样式。

  3. 排版/重排。具体计算每个 DOM 元素最终在屏幕上显示的大小和位置。web 页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。

  4. 绘制/重绘。绘制文字、颜色、图像、边框和阴影等。

  5. 渲染层合并,由上一步可知,对页面中 DOM 元素的绘制是在多个层上进行的,此步骤将多个图层按顺序合并。

b.节点树的映射转换

  • DOM: core/dom/README.md

  • Style: core/css/README.md

  • Layout: core/layout/README.md

  • Paint: core/paint/README.md

  • Compositor thread: Chromium graphics

浏览器首先会解析 html 和 CSS ,生成对应的 DOM 节点树CSS 树,然后组合 DOM 节点树和 CSS 节点树构造出一棵 layout 节点树,来进行排版算法的计算,最后根据 layout 排版的结果来构造 paint layer 树,最终通过层合成、光栅化等步骤最终渲染在屏幕上。

具体实现可以参考相关目录代码和文档,这里不做详细介绍,接下来重点看 layout 模块的设计。

2. Layout 数据结构定义

先看一下 layout 模块的代码结构

a. Layout 代码目录结构

// layout模块总路径,该路径下的文件是LayoutNG架构以前的老排版流程实现LAYOUT_PATH = https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/layout/
// layout定义的一些对外接口,box定义LAYOUT_PATH/api
// layout坐标系的定义LAYOUT_PATH/geometry
// layout测量、断行算法的实现LAYOUT_PATH/line
//layoutNG模块,是chrome团队设计的排版新架构,用于支持更为复杂的CSS能力,并且设计了一些新的流程机制。从chrome75版本开始灰度layoutNG架构(inline、block等布局已经应用LayoutNG),并且chrome的开发人员正在用新架构逐渐替换老架构的流程。https://docs.google.com/document/d/1uxbDh4uONFQOiGuiumlJBLGgO4KDWB8ZEkp7Rd47fw4/edit#heading=h.jsqqsk3gvs48LAYOUT_PATH/layoutNG
// 图形的排版支持LAYOUT_PATH/shape
// svg的排版支持LAYOUT_PATH/svg

b. CSS 盒模型数据结构

上图描述了 CSS 转化的 box 模型结构(不包括溢出等场景的 box),定义了 box 的 padding、margin、border、width/height、scrollbar 等属性。

盒模型的 border 以及以内区域是用一个 LayoutRect 对象来表示:

参考这里的注释介绍,LayoutRect 的位置是从它本身的边到容器的边的距离,因此它的距离/位置包含了 margin 值和 left/top 的位移偏差。LayoutRect 记录了一个盒子的位置和大小。

LayoutRect 内部数据结构定义:

LayoutRect 也提供了一些对外接口。如下图代码逻辑,ClientWidth 表示减去了 border 和 scrollbar 的宽度

OffsetWidth 和 OffsetHeight 分别表示的是 frame_rect(LayoutRect 对象)的宽和高,包括了 border 和 scrollbar 的距离

c. 坐标系设计

(参考 blink 坐标系设计文档 )

Layout 和 Paint 会使用 4 种坐标系来计算,实际是 2 种,还有 2 种是文本方向改变的坐标系变体。

  1. Physical coordinates: 对于物理显示器(比如屏幕、打印)输入的物理坐标系,用于渲染层绘制

  2. Logical coordinates: 用于排版布局,适用于任何书写模式和方向 CSS 属性值的广义定位。以 before、after、start 或 end 命名的属性在此空间中。这些也分别称为“逻辑顶部”、“逻辑底部”、“逻辑左”和“逻辑右”,用于抽象文本方向改变的场景。(这里详细设计思想可以参考后面Layout 流程 – 排版方向)。

  3. Physical coordinates with flipped block-flow direction: 与 Physical coordinates 的用法相同,用于渲染层绘制。但对于写入模式:vertical-rl 块从右到左排列,block 的位置从左到右侧“翻转”。可以把这个坐标系理解为正常从左到右坐标系的镜面反射。

  4. Logical coordinates without flipping inline direction: 不包含文本方向的 Logical coordinates,用于排版布局。

d. 逻辑单位和物理单位

为了提高可读性并确保坐标系之间的正确转换,blink 使用了一组专用单位来表示每个坐标系的偏移、位置、大小和矩形(如果它们适用)。坐标系之间的转换必须显式进行,不允许隐式转换。

逻辑单元

即逻辑坐标系的单位。

LogicalOffset 和 LogicalSize 表示偏移量或大小。每个都有一对 LayoutUnits ,代表 inline 和 block 方向的偏移量或大小。

LogicalRect (上面盒模型有介绍)表示一个矩形并包含一个偏移量和一个大小,由上述两个单位表示。

这些都在逻辑坐标空间中,因为它们不考虑书写模式或方向性。

物理单位

即物理坐标系的单位

PhysicalOffset、PhysicalLocation 和 PhysicalSize 表示偏移、位置或大小。PhysicalLocation 与 PhysicalOffset 的不同之处在于,PhysicalLocation 表示距根节点的位置,而 PhysicalOffset 表示距父级的增量。每个都有一对 LayoutUnit,表示水平和垂直方向的偏移量或大小。

PhysicalRect 表示一个矩形并包含一个偏移量和一个大小,由上述两个单位表示。

这些都在物理坐标空间中,因为它们表示从上到下、从左到右的坐标系中的坐标。因此,从逻辑单元转换为物理单元需要解析坐标。Layout 流程-排版方向 部分介绍更多的细节。

精度和捕捉

无论使用的坐标系如何,所有值都以 CSS 像素为单位并表示为 LayoutUnit。LayoutUnit 是精度为 1/64 的定点单元。

对于绘画位置和大小需要捕捉到像素网格以确保清晰的渲染。此过程称为像素对齐,涉及从 CSS 像素转换为设备像素。与四舍五入位置和大小或计算封闭矩形像素对齐不同,生成的矩形与像素边界对齐,并尽可能接近原始位置和大小。

3. Layout 流程

  1. #### a. layout 树的生成流程

(1)触发 reflow

那 blink 是如何生成 layout tree 的呢,如上图。

熟悉 web 开发的同学都知道,有很多场景都会触发 reflow(也就是 blink 的 LayoutTree 的重新生成),比如 DOM 树解析完的时机, 调用了 UpdateStyleAndLayoutTree 方法去触发了 LayoutTree 的更新:

void Document::FinishedParsing() {    ...    ...    ...    if (!is_initial_empty_document_)      UpdateStyleAndLayoutTree();}

大概搜了一下 blink 的源码,大概有 100 多处地方会触发 UpdateStyleAndLayoutTree,后面介绍 blink 的 reflow 策略的文章会详细处理。

(2)生成 layout tree

(DOM tree 转 layout tree)

这里的 layout tree 和 DOM tree 几乎是一一对应的关系。(也有一些特殊场景,如上图,LayoutNGBlockFlow 不能同时包含 LayoutNGBlockFlow 和 LayoutInline,所以中间需要增加一层 LayoutNGBlockFlow(anonymouse))

生成树的过程中,同时会根据 CSS style 计算盒模型相关的属性(2. Layout 数据结构定义-CSS 盒模型数据结构 章节中有具体介绍到 layout tree 的 box 节点中。比如 box 的 padding、margin、border、width/height、scrollbar 等属性

(NGLayoutInputNode 抽象层)

在 LayoutNG 的设计中,抽象出了 NGLayoutInputNode 的层次,后续 blink 的重构中,会逐步迁移 layout tree 相关的逻辑。

最终渲染需要用到的,是 fragment tree,fragment tree 完全由 NGFragment 和 NGText 节点组成,并且是 immutable 的。每个节点都包含一个指向计算样式的后向指针、一个子节点列表以及节点本身的大小和位置。该位置是相对于直接父级的偏移。如下图:

(fragment tree)

b. 排版方向

上图显示了 block 和 inline 两种方向的一个表示。在从上到下的书写模式中,inline 是水平的,block 是垂直的。Layout 数据结构定义-坐标系设计 章节有介绍,layout 模块的坐标系主要是使用的 Logical coordinates(逻辑坐标系), 这也是 LayoutNG 新架构一个比较经典的设计,使用逻辑坐标系是可以抽象各种文本方向的(horizintal-tb、vertical-rl、vertical-lr 等)。

上下左右的边距都是用’before’, ‘after’, ‘start’, ‘end’ 来表示的,而不是我们常见的’left’, ‘right’, ‘top’, ‘bottom’。这样对于 horizintal-tb、vertical-rl、vertical-lr 等不同的场景,’before’, ‘after’, ‘start’, ‘end’ 这些变量的表意是一致符合预期的,只要通过这些变量,在不同的文字方向场景,来输出 box 的 width、height、offset 等属性。

这样对于排版流程,是得到了很好的抽象,逻辑也会简化,不会因为不同的文本方向而有多套排版逻辑。

学习下来,这里对腾讯文档 双向文本、竖向文本的排版设计也有一些启发。

c. 测量

介绍完坐标系,那浏览器排版时,是怎么知道每个文字的大小的呢。每一个字符 box 的大小,都是通过测量模块来输出结果的。测量也是浏览器来做文字断行的基础

Chrome 使用了字符处理库 HarfBuzz 来测量每一个glyph,从而获取每个 textrun box 的宽度。

注:glyph 的的概念可以参考介绍文章

HarfBuzz 被广泛用在 GNOME、KDE、Chrome、Android、Firefox、LibreOfiice 和 Java 等平台中。

d. 断行

如上图所示,当一行的文字占满后,会需要换行。所以浏览器的排版需要实现断行算法

早期 blink 的断行算法,通过 break iterator 和 breaking context 两个模块来实现。

break iterator 每一次迭代获取一个断行点,,可以是一个单词或单个字符,并测量从行第一个单词的开头到当前断行点的宽度。使用 breaking context 计算累积宽度并不断迭代,直到当前断行点超过可用空间。

如下图:

(早期测量的思路)

这个算法效率相当低,因为每个单词都是单独测量的,并且宽度是累积的。

对于使用 word-break: break-all 的情况,情况就会更糟。在这种模式下,每个非 CJK 字符都被视为一个 断行机会, 但不能单独测量字符,因为这会破坏字距调整和连字(kerning 效果)。相反,对于单词中的每个字符,宽度是从单词的开头到该字符测量的。这与单词的长度成二次方关系,这个性能问题有一个实际出现过的例子(错误 591793)。

(逐个字符测量)

chrome 团队即使尝试按字符测量,如上图,不过测量的性能仍然低下,并且无法处理 跨空格的字符,在 word-wrap: break-word 场景下也会造成一些测量错误 (bug 380667)

在 LayoutNG 新排版架构的设计中,blink 预先对文本做了分词,然后逐个单词传给测量引擎去测量,同时测量引擎新增了测量缓存的能力。

(LayoutNG 重构的测量思路)

如果断行机会在某个完整的单词后面,如上图在 dozen 单词后,由于所有的单词宽度已知,则不需要进一步的测量。

但是,如果断行机会在一个单词内,例如字符串中的字母“o”和“z”之间,则需要重新测量导致断行机会的部分。如果指定了 word-break: break-all 或在两个字符之间插入了软连字符,则可能会发生这种情况。

e. 约束空间

CSS 中的三种基本的定位机制(普通流、定位、浮动)

  1. 普通流:块级元素从上到下依次排列,框之间的垂直距离由框的垂直 margin 计算得到。行内元素在一行中水平布置。

  2. 定位:相对定位(position:relative), 绝对定位(position: absolute),固定定位(position:fixed)

  3. 浮动:浮动元素(float 属性)

之前介绍的内容,简单阐述了 普通流的排版的一些流程,但是 float,还有固定定位在 blink 里是如何排版的呢。

LayoutNG 模块中设计一个约束空间(**NGConstraintSpace**)的概念,如下图

图显示了一个 NGConstraintSpace,具有固定宽度、无限高度、浮动排除区域和定位 NGFragment。

调整大小

NGConstraintSpace 具有 inlineSize 和 blockSize 属性属性,如果定义了这些,则指定布局可以执行其布局的维度。如果其中一个是 无限的(由 -1 表示),则布局可以假设它有一个无限的空间来在该方向上执行其布局。

在上面的例子中,约束空间有一个固定的 inlineSize,但是一个无限的 blockSize(表示它可能在一个可滚动的元素中)。

某些 布局模式将其子项“拉伸”到固定大小(例如 flex、grid)。这将在每个方向的 NGConstraintSpace 上用一个标志表示(fixedInlineSize、fixedBlockSize)。如果为 true,则当前布局必须生成满足这些约束的片段。

排除项

NGConstraintSpace 有一个排除项列表 (NGExclusion),它指定布局不应将哪些区域 某些 子项放置在内。这些排除项用于表示浮动、内部/外部形状和 NGFragment 已放置在空间中的。

每个 NGExclusion 都有一种“浮动”或“正常”类型。由于某些子项(不同格式上下文中的块级框)忽略浮动排除,因此需要这种区别。

大多数 NGExclusions 只是 NGExclusionRect 类型,即矩形。为了支持 CSS Shapes[2] ,还将有 NGExclusionShape,它是一个任意形状。

4. 触发 Layout 的时机和 reflow 优化

这一块展开也有很多点,本篇篇幅有限先不介绍了。先预告一下,接下来一篇文章会详细介绍 Layout 内核里的 reflow 策略,并且结合 web 的一些性能优化的思路来解释。

篇幅有限有些内容没有详细展开,有偏差的地方欢迎指正!

5. 参考文档:

  1. blink 坐标系设计:https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/third_party/blink/renderer/core/layout/README.md#coordinate-spaces

  2. blink 断行算法:https://docs.google.com/document/d/1eMTBKTnWEMDu00uS2p8Xj-l9Pk7Kf0q5y3FbcCrWYjU/edit#heading=h.guvbepjyp0oj

  3. blink 最新官方 ppt 设计:https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGAWF53k96imRH8Mp34Y/edit#slide=id.ga884fe665f64_1343

  4. LayoutNG block fragmentation:https://docs.google.com/document/d/1EJOdFesZKspvrU7uWtGl-8ab2jIrzRF6NKJhwYOs6hU/edit#heading=h.9tki7tr3avwt

  5. css bfc 特性理解:https://www.zhangxinxu.com/wordpress/2015/02/css-deep-understand-flow-bfc-column-two-auto-layout/

  6. Block Layout:https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/layout/ng/BlockLayout.md

  7. Inline Layout:https://chromium.googlesource.com/chromium/src/+/refs/heads/main/third_party/blink/renderer/core/layout/ng/inline/README.md

  8. CSS layout api:https://drafts.css-houdini.org/css-layout-api/#layout-api-containers

  9. layoutng 文档:https://www.chromium.org/blink/layoutng

  10. blink 重构:https://juejin.cn/post/6844903687601520647

  11. Blink LayoutNG 内核官方设计文档:https://docs.google.com/document/d/1uxbDh4uONFQOiGuiumlJBLGgO4KDWB8ZEkp7Rd47fw4/edit#

  12. Blink Layout 学习博客:https://www.rrfed.com/2017/02/26/chrome-layout/

关于AlloyTeam


AlloyTeam 是国内影响力最大的前端团队之一,核心成员来自前 WebQQ 前端团队。
AlloyTeam负责过WebQQ、QQ群、兴趣部落、腾讯文档等大型Web项目,积累了许多丰富宝贵的Web开发经验。

这里技术氛围好,领导nice、钱景好,无论你是身经百战的资深工程师,还是即将从学校步入社会的新人,只要你热爱挑战,希望前端技术和我们飞速提高,这里将是最适合你的地方。

加入我们,请将简历发送至 alloyteam@qq.com,或直接在公众号留言~

期待您的回复?

6. 关注我们:

我们将为你带来最前沿的前端资讯。

来源: 印记中文