4

5个角度对比拆解 Flutter Widget 和 CSS 布局原理的差异

 3 years ago
source link: https://zhuanlan.zhihu.com/p/273557876
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

5个角度对比拆解 Flutter Widget 和 CSS 布局原理的差异

这篇文章专门对比 Flutter Widget 的布局原理和 CSS 布局原理的差异,分享在对接过程中会遇到的问题和解决方案,帮大家理一理思路,内容可以分为这几部分:

  • CSS 和 Widget 参赛选手介绍
  • Let's Battle! 从五个角度正面硬刚
  • Love & Peace 讨论取长补短的可行性
  • Happy Ending 最重要的环节

(本文内容来自阿里巴巴淘系技术部门柳,后续分享更多阿里工程师技术干货,感兴趣的同学可以点击主页关注我们。)

CSS

CSS 是 Cascading Style Sheets(层叠样式表)的简写,是一种用来描述样式的标记语言,最初的想法诞生自 1994 年,1996 年落成第一版规范(参考 20 Years of CSS)。HTML 描述了页面的结构,CSS 描述页面呈现出来的样子,这对 CP 已经配合工作了二三十年,依然是布局圈里最高效的组合。

v2-3ffdfa9a119dbe57771b2f2ed0d78fe8_720w.jpg

▐ CSS is Awesome

CSS 描述布局很高效,这点无需质疑。后续出现的各种布局方案,前端框架 CSS in JS、XML描述文件、Flutter 等等,即使没有直接照搬 CSS 的功能,也深受 CSS 设计的影响。

CSS 很容易上手,经典的盒模型一学就会,文本相关的属性看名字就知道怎么回事,Flexbox 也是很好用的,但是 CSS 逐渐出现了一些很难驾驭的功能,多列布局就稍微难了一点,CSS Grid … 这代码也太难写了吧,我觉得这是给 AI 设计的,不是让人手写的,再加上 clip-path, filter, css houdini 等等,功能越来越强大,可以做滤镜画皮卡丘画油画甚至还可以做游戏。功能很强大,但是不实用,上述这些功能在生产环境基本上都是用 JS 实现的,没时间去磨 CSS。

▐ 渲染布局流程

浏览器里 CSS 的渲染过程,简化一下可以总结为 加载、解析、查询并作用到 DOM 节点、计算布局 这四个过程。浏览器首先加载 HTML 文件,遇到 <style> 标签开始加载 CSS 和解析 CSS,根据选择器的特性,计算出每个节点的样式,然后作用到 DOM 树上。

然后浏览器根据带有 ComputedStyle 的 DOM 树生成 LayoutTree,节点的 display 特性不同,生成的 LayoutObject 的类型也不同,然后布局算法会多次遍历这课树,计算出每个节点的 Rect。这个过程是很复杂的,并不是一个 DOM 节点就对应一个 Layout 节点,要考虑 display:none、伪元素、文本节点、shadow dom 等等,而且整个过程是同步的,在解析出的 DOM 节点已经布局完成后,如果浏览器又解析到一个 <style> 标签,那就又要查询匹配其中的 CSS,把新样式作用到 DOM 上,再计算一遍布局。

布局完成之后是执行 Paint,把布局信息一层一层的提交到 compositor 线程,然后再划分成一块一块的交给 GPU 线程去绘制。我觉得最后这两步是比较快的,在主线程里的 Layout 和 Paint 才是最耗时间的,而且和 JS 代码的执行搅在了一起。

Flutter Widget

和 CSS 不同,Flutter Widget 设计得很细致,分类清晰功能明确,不像 CSS 那样各种属性耦合在一起,互相影响。Widget 的设计是比较原子化的,基本不会互相影响,为了保持布局算法的高效,对 Widget 的嵌套方式有要求。

Flutter 设计得比较合理,有一个原因不容忽视,Flutter 在 Google 的开发团队和 Chrome 有很大渊源,有些人已经参与制定 CSS 规范很多年,都是在这个领域里很有经验的人。整体上没有 CSS 那么多的冗余和历史包袱,新框架都可以吸取以前的教训,站在巨人的肩膀上设计得越来越好用。换句话说,如果让 CSS 的设计者们,抛开历史包袱重新设计,不考虑向下兼容,很有可能就设计成了现在 Flutter Widget 的样子。

渲染布局流程

关于 Flutter 的渲染流程,官方文档Flutter 工作原理就是很好的学习资料,最大的亮点是次线性的布局,有接近 O(n) 的性能,比 CSS 高效得多。

具体过程大家去官网学习吧,这里就放一张图,本文的重点是 Battle!

Let's Battle!

▐ Round 1: 背后的大佬

在真正开始比较之前,先看一下他们背后是什么样的组织在支撑和运营着他们。

CSS 背后是 W3C,这是业界认可的标准化组织,而且被各大浏览器实现,浏览器厂商也在积极的推到标准的发展。CSS 是个开放的技术,它背后的大佬是 W3C、Chrome、Safai、Firefox 等等一系列盈利或者非盈利组织,大家互惠互利共同发展。

但是 Flutter 背后是是只有 Google,虽然也是开源的,但是设计与实现都是由 Google 的团队来主导,其他人都是在使用,真正有能力有机会参与开发的很少,PR 都是小修小补。框架和标准的一个差别,就是会不会向下兼容,框架有可能明天宣布推出 2.0,带上牛逼的优化和 breaking change,不向下兼容,是司空见惯的事。

从这个角度看,CSS 虽然臃肿,但毕竟是个标准化的技术,生命力顽强。你现在写的 CSS 代码,五年之后依然可以运行,现在写的 Flutter 代码到五年后就不好说了。假如 Google 宣布不维护 Flutter 了,很可能社区里就瞬间丧失了信心,那 Flutter 就死了;假如 Chrome 宣布不支持 CSS 了,CSS 依然活得好好的,Chrome 很可能会死(参考 IE),FireFox 要笑醒了。

▐ Round 2: 学习成本

要说学习成本,当然是 CSS 高效,先不讨论原理,先从几个侧面的案例说明一下。

市面上能出现“零基础,三个月成为前端高手!”的培训班,也侧面说明了前端学习成本低,其实他们的口号是错的,三个月肯定学不会前端,能学会的只有用 CSS 切页面。但是没有培训班开“三个月掌握Flutter”的课,一个熟练的前端开发者三个月学会是可能的,客户端上手更快一点,没有编程基础的人学完肯定一脸懵逼的要求退钱了。

另外微信可以举办“青少年微信小程序编程创意营” ,面向中小学生,小程序的 UI 就是受限的 HTML + CSS 来写的,但是 Flutter 要搞这种比赛的话,就得面向有熟练编程经验的人了。

如果从语法角度考虑的话,CSS 容易学是因为它只是一种描述性语言,不含复杂的编程逻辑,设计目标就是用来描述“我想要的 UI 是什么样子”的,是面向结果的描述,而 Widget 是要通过写 Dart 代码来实现的,UI 和代码逻辑写一起,通过代码一行行描述“我怎么把 UI 组合出来”的,是面向过程的描述,所以 CSS 更直观一些,写出来的代码更容易让人理解。还有个小原因,就是像 Fluter Widget 这样层层嵌套的代码,写起来和改起来都很麻烦,太依赖编辑器,复制粘贴不太方便(差不多的话,是可以抄一下代码的嘛)。

另外 CSS 诞生了这么多年,学习资料简直多到爆!本身规范事无巨细,还有 MDN 、CSS Tricks 、CodePen 等网站即授之以渔又授之于鱼,各种线上培训也把知识框架和学习路线都给你安排的明明白白的。相比之下,Flutter Widget 就只有官网,学习资料和社区生态都还差很多。

▐ Round 3: 开发效率

CSS 上手虽然简单,但是很难掌握,没有个几年的开发经验,没被它虐过千百遍,是根本驾驭不住它的。

v2-2f0630430bee9ec558cabef915eb25bc_b.jpg

假如要画出来 CSS 的学习曲线的话,初期肯定是快速上升,到了一定高度后就变慢了,甚至还迷之下降。但是 Flutter 刚开始学习时要了解很多概念,要转变思维,上手稍微慢了一点,但是越学越快。说个不恰当的比喻,写 CSS 可以看做是操作提线木偶,写 Widget 就相当于是搭乐高积木。

v2-9b5c3458e2485c6829b494ef401c61c0_b.jpg

因为 CSS 各种属性之间是可以互相影响的,输入和输出不是简单的对应关系,你以为你写了 width: 100px 它的宽度就一定是 100px 了?就像是绑了几百根线的木偶,让你拉动其中的五根来做一个 OK 的手势,你牵了其中一条线,动的可能不只是一个部位,而是整个上半身。

Flutter Widget 就更加原子化,而且对于谁可以嵌套谁是有要求的,就像是拼乐高积木,每块积木都很小,但是有明确型号的卡口,要先符合它的设计,然后再发挥创意拼装成各种造型。迫使你按照理想的方式去组合 Widget,避免写出性能太差的代码,CSS 就是任意属性都可以和任意属性在写在一起,而且标准里总能给出你一个合理的解释,所以写出来的代码很混乱,也增大了布局的难度。

所以 CSS 的开发效率初期较快,但是积累沉淀较难,大规模协作很难管理(耦合度高,全局作用域等),而 Flutter 的封装性更好,有利于协作,开发效率会越来越快的。

▐ Round 4: 天下武功,唯快不破!

要问 Flutter 的 Widget 和 CSS 谁更快?大家的共识也是 Flutter 更快吧。我觉得 Flutter 和 CSS 布局相比有两大性能优势:一个是次线性的布局算法,一个是更合理的线程模型。

Widget 的布局原理比 CSS 高效,这是牺牲了一部分灵活性换来的,以后扩展 Widget 时,都要遵循这些设计才能继续保持高效。CSS 简单的属性背后可以深挖出特别复杂的细节,使得布局模型越来越复杂,渲染管线越来越长,光 display 就有十几二十个值,每一条都对布局影响巨大,方便了开发者,但是给布局性能带来很大挑战。

关于线程模型,也是浏览器一直被诟病的性能瓶颈,主线程太忙了,JS 的执行、HTML/CSS 的解析、DOM 的构建,布局的运算,全都在主线程。相比之下 Flutter 划分的四个线程就比较均衡,GPU 线程做的工作和浏览器差不多,但是宿主平台(Android/iOS)的代码跑在 Platform 线程里,Flutter Framework 主要运行在 UI 线程里,另外还有 IO 线程实现网络和图片、字体等文件的加载。

▐ Round 5: 未来的发展

分享几个关于 CSS 现状的数据,W3C 官方定义的 CSS 样式有 520 条,Chrome 平台上统计的样式有 703 条,包括了一些带前缀的样式,有大量样式的使用率很低。支付宝小程序的同学总结过 Top 100 小程序里用到的不同 CSS 样式,有 184 条。

总结一下就是:W3C 标准里定义了 520 条样式,Chrome 还额外支持 180+ 条私货,但是常用的样式不超过 200 条。

CSS 有大量历史包袱,我自己也感觉大部分样式我都用不到,有些甚至是最佳实践里禁止使用的,学会 50 条 CSS 后就可以写 80% 种情况的布局了,对于难写的样式,我多加几层标签再写点 JS 也能实现。但是这些样式不能废弃,必须继续支持,新增一条好用的属性时,要解释清楚和现在所有属性的适配情况。

CSS 是负重前行,注定要越变越复杂。Widget 则是轻装上阵,而且解耦比较好,是可插拔可组合的,如果未来想废弃一些 Widget,把这部分 Widget 从主包里拆出来放到独立插件里就行了,想用的话自行引入。整个迭代过程受历史包袱的影响很小。

Love & Peace: 可否实现对接?

Battle 已经结束,友谊第一比赛第二,不讨论胜负,下面进入 Love & Peace 环节。

Flutter 的压线性布局让人眼馋, CSS 的灵活度又深受大家喜爱,可不可以取其精华去其糟粕,让开发者写 CSS 但是底层用 Flutter 来渲染?有这种想法的不是一个人,所以有很多方案把前端框架或小程序对接到 Flutter 上,在实现的时候,就会遇到如何把 CSS 转换为 Widget 的问题。

▐ 技术可行性

技术上当然是可行的,我在前一篇文章《打破重重阻碍,Flutter 和 Web 生态如何对接?》里介绍了各种实现方案,自己也写代码做过对接(用的C++魔改方案),跑通了整个渲染链路。

我介绍一下我的实现方式,可以简单分成三步:

1. 解析 CSS 语法

我写了个精简版的 CSSOM,是标准 CSSOM 的子集。用来实现样式表和样式属性和增删改查、样式值的解析与计算、选择器的匹配和查询等功能。(功能是独立的,有需要的话自取)

CSSOM 主要是为了处理 CSS 的上层语法,转成一致的数据格式,为下一步的转换做准备,有一部分 CSS 的语法是在这个过程实现的,例如 CSS 选择器,包括伪类选择器和选择器关系等,还有 @media 和 @keyframes 等功能,也可以实现 CSS variable 以及 calc()。无论上层是直接写 CSS 还是 CSS in JS,都保证下一步转换时输入的数据格式一致,可以简化后续的实现。

2. 实现 CSS 属性和 Widget 数据格式的映射

这部分要把 CSS 的基础数据格式转成构建 Flutter Widget 所依赖的数据格式。例如 CSS 的 color 属性会转成 Flutter 的 Color 类,margin 和 padding 会转成 EdgeInsets 类,flex-direction 将被转成 Axis 枚举,把文本相关的属性转换成 TextStyle 类。

这部分也是做原子性的转换,技术上看起来比较简单,只是转换数据格式而已,但是需要对 CSS 和 Widget 的设计都比较了解,知道同一个概念在双方语义中的对应关系,得搞清楚里边的技术细节。这个对应关系如果转换错了,后续的布局怎么调都调不对(过来人的经验…)

3. 构建 Widget 树

拿到 CSSOM 传来的数据,掌握了 CSS 和 Widget 数据结构的语义转换,然后在结合 HTML 定义的结构,就可以生成真正的 Widget 树了。

构建 Widget 树就不能只考虑 CSS 了, HTML + CSS 才和 Widget 对等。这里还要处理不同类型 HTML 节点和 Widegt 的对应关系,节点上带的布局样式不同,生成的节点也不同,Widget 的层次深度比 HTML 的要深。例如普通的 div 标签,如果仅包含普通盒模型样式,就转换成 Container;如果包含了 flex 相关的属性,根据具体配置的不同,转成 Flex/Center/Row/Column 等;如果包含了绝对定位就要转成 Positioned/Stack。这个过程也是很繁琐,包含了大量细节,需要理解默认 HTML 标签的语义、CSS 的层模型以及 BFC 等等,还需要理解 Flutter Widget 之间的嵌套限制,组合成 CSS 想要的效果。

▐ 使用限制

对于开头提到的问题,前面的技术可行性分析回应了「繁琐」,下面讨论一下「完备性」。

CSS 是灵活的,Widget 是受限的,把一个灵活的语法转换成受限的实现,注定是不完备的。

在 CSS 里,任意属性可以和任意属性写在一起,W3C 标准里总有一个明确的解释方式,HTML 和 CSS 是没有错误的,只有不符合预期。但是在 Flutter 里,某个 Widget 里可以放哪些 Widget 是明确的限制的,例如 Positioned 外层必须有个 Stack、Center 只能有一个子 Widget,不符合预期的嵌套是会报错的,写出的代码不会出现匪夷所思的混用,所以布局算法可以很快。从这个角度讲,CSS 是 O(n!) 的复杂度,而 Widget 是多项式复杂度,用 Widget 去实现 CSS 注定是不完备的。(这难道是个 P 和 NP 问题……?)

在限制 CSS 写法的情况下,能不能对接到 Widget 的实现?这个是可以的。

想要 Widget 次线性布局的性能,就必须牺牲 CSS 的一部分灵活性。想要用技术突破这个限制?那就要改这套次线性的布局算法了,改完之后就不再是 Flutter 了,性能优势也没有了。

以我的实践经验来看,Widget 只能支持一定范围内的 CSS 样式。它对 CSS 的使用限制不在于样式条数,并不是说某个样式实现不了,而是在于样式的混用,即使支持了 500 条 CSS,但是某些属性依然不能同时使用,外层用了样式 A 内层再用样式 B 就是无效的,C 和 D 写一起就只有 C 有效。我评估 Widget 对 CSS 支持的范围上限会比现在的 ReactNative/Weex 还要大一些,能够满足大部分业务和小程序的需求,然而业务需求是会增长的,达到支持范围上限以后就很难再扩大了,就需要教育开发者了,对开发体验有影响。

https://www.w3.org/Style/CSS20/

https://una.im/CSSgram/

https://projects.lukehaas.me/css-clip-path-pokemon/

https://css-art.com/pure-css-lace/

https://codepen.io/finnhvman/pen/xJRMJp

https://flutter.cn/docs/resources/inside-flutter

https://github.com/flutter/flutter/wiki/The-Engine-architecture#threading

https://www.w3.org/Style/CSS/all-properties.html

https://www.chromestatus.com/metrics/css/popularity


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK