Android 排版引擎实现 - 流排版模型

Frank Xu
微信读书
Published in
5 min readAug 27, 2016

注:这一部分内容需要对浏览器实现有所了解

自顶而下的树排版模型

CSS 的规范对于分页也不是特别友好。比如一个边框横跨两页甚至多页的时候,应该如何处理第一页的最底端边框?中间页的上下边框和最后一页的最顶端边框呢?同理可以应用于 padding 和 margin 等盒模型的定义上来。

排版引擎从准确性上是不能够把 DOM 树分段的,因为 DOM 树上的父子节点是包含关系,每个节点都要遵循 CSS 的盒模型,布局好父节点才能够知道子节点允许存放的盒大小。所以大部分的排版引擎都只考虑一页的情况,改造成在手机上使用,实际上是把整章书摆在一个无限高度的画布上一路画下去然后按屏幕高度再进行切分。

这样使得 DOM 树必须作为整体加载到内存自顶而下进行遍历排版和渲染,即先构造 DOM 树,把树上的内容摆到画布上进行布局。在这种模型下,很难把 DOM 树分成多段来处理,意味着必须把全部内容加载到内存才能排版。

主动拉的流排版模型

对于大部分书籍而言,其实没有那么复杂的盒模型嵌套,就算有,在文档的某个位置其实是可以“大致反算出”当前的盒模型的。这样的角度其实不需要提前构造好整棵 DOM 树(带全部文本和全部样式),只需要一个构造出来 DOM 的区间结构即可。

这篇文章提到的 DOM 树索引,给定一个位置,我们可以得到这个位置对应的定义样式(Styles),通过级联计算,可以得到当前位置经过转换后的实际样式(Computed)。

比如得到文本:

Hello, World!

和 DOM 索引树:

      [0-12) ➙ font-size: 14px;
/ | \
[0-6) [6-11) [11-12)

font-weight: bold;

假定只关心两个字体属性:font-size,font-weight。

  1. 开始处理每一个节点的时候,给定一个位置,我们把游标移动到包含这个位置的最深子节点。从这个例子上看,处理Hello, 的时候,我们应该把游标移动到 0–6 这个区间节点。
  2. 先取 font-size,当前节点没有定义,而 font-size 由是属于可以继承的样式定义,则对应查找父节点 0–12,此时得到12px,转为实际的 dpi 即可得到 Hello, 的字体大小。
  3. 再取 font-weight,同样的也是允许继承的属性,不断地往上查找都找不到的时候,则指定默认值:normal,实际上每个排版引擎都会有一份默认的样式列表(FirefoxWebKit)。

大部分样式都是可以允许往上继承的,但是像 background 就不能继承。

不同属性的继承规则计算

倍数值和实际值

如果样式定义的值是一个倍数:120%,则需要继续往上查找实际值 px 或者 pt。如果是 em,因为 em 是相对于字体大小的,还需要重新计算当前节点的字体大小,然后再计算实际的值。

如:

div { font-size: 12px; }
div p { padding: 1.2em; }
div p span { padding: 80%; }

则最终的 div p span 的 padding 应该是:0.8*1.2*12=11.52px。

盒模型的继承计算

拉取的模型能处理好大部分常见的情况,但是遇到嵌套的盒模型,单靠简单的继承覆盖是不正确的,需要把所有的父节点盒模型定义做一次归一。

比如:

div { padding: 1em; }
div p { padding: 1em; }

则 div p 的 padding 值应该是 2em,同样的需要应用到 margin 和 border 等等。

游标优化

流模型先读取文本再取样式,排版引擎不会回溯已经排版过的内容,文本一般都是顺序读取的,而渲染翻页的时候也是左右滑动翻页。

所以在 DOM 索引树上的定位比较好做,给每个区间节点纪录一个上节点和下节点组成双向链表,这样索引树在定位的时候只需要拿游标左右游走,就能够轻松处理大部分的常见情况。

当然,对于进度条快速跳转的情况,复杂度还是线性的。

大部分书籍的样式不会非常频繁的变动样式,对于区间不动的游标游走,告知排版引擎不需要重新读样式,复用原来的样式即可,这样在大片文字都是相同样式的时候效果非常明显。

对节点计算结果做 Memorization

对于 DOM 树上的节点,父节点的重复率是相当高的。如果子节点没有定义字体大小,那么所有的子节点都会继承父节点的字体大小。

如果父节点的字体大小不做缓存,意味着所有子节点的查询都要重新计算,实际上并没有必要。这种情况下可以对每个节点持有一份 Computed 缓存,保留计算过的结果。

写在最后

原来浏览器一次性全部排版的做法,会把压力全部交给了渲染引擎,渲染引擎需要充分的优化才能做到分段处理,逐步渲染。这种架构在 WebKit2 上有了进一步改进,分离绘制和渲染,其实 Webkit2 的绘制也就是我们这里的排版部分。

而如果能够在绘制阶段就做好分段,那渲染引擎可以更轻量,也更好隔离。比如我们目前的架构想更换别的渲染引擎,渲染引擎本身仅需要处理一页的元素和位置即可,而不需要考虑整体的分段策略和逻辑。

--

--

Frank Xu
微信读书

WeRead at WeChat. Growth Analyst, Data & Infra Engineer.