Flutter:移动端跨平台技术演进之路

2,417 阅读16分钟

1. 导读

本文约4688字,阅读可能需要15分钟。

最早的跨平台开发(摘自《Apache Cordova移动应用开发实战》王亚飞,王洪飞编著)

Snipaste 2019 11 08 20 13 54

从广义上来说,跨平台技术早于移动端的出现。因此,本文标题前面也加上了一个定语:“移动端”。而由上图也可窥见一二:移动端跨平台技术几乎和移动端本身的历史一样长。跨平台技术之所以生命力如此强大,个人认为有以下几个原因:

  1. 开发效率 : 这也是跨平台技术出现的初衷,理想状态下,一次开发,多端运行,组件复用,提升效率。

    • 对于管理者,跨平台可以降低用人成本,避免了同时养两个(Android/iOS)开发团队的现状

    • 对于开发者,跨平台可以降低学习成本,只需要了解一套框架,就可以实现双端开发,提升了自我价值

  2. 业务价值 : 跨平台开发成本更低,适合产品的快速验证,待功能稳定后再进行性能体验上的优化

  3. 二级生态 : 举一个例子,JVM在操作系统上建立了自己的二级生态,所有的Java开发者只需要面向JVM编程和优化,可以忽视操作系统的存在。在原生、底层的平台上做一层封装,以很小的性能损失为代价,为开发者带来巨大的效率提升,这在软件工业是屡见不鲜的。二级生态有重要的战略意义,掌握了二级生态,就掌握了话语权和影响力。所以Facebook和Google都在发力。

  4. 平台能力 : 乘上第三点,跨平台还有一个好处就是拥有了自己的生态就可以开放自己的能力,制定游戏规则让其他开发者参与,典型有小程序(微信/QQ/支付宝)、快应用等

跨平台好处颇多,但挑战也不少,主要集中在以下四个方面:

  1. 研发效率

    • 工具支持程度:补全、提示、构建管理等

    • Debug是否方便,错误日志是否详细

    • 文档完备、项目活跃

    • 隐藏平台差异,如React Native需要大量桥接工作

    • 开发语言生态,js生态庞大,开发者众,Dart则名不见经传

    • 等等

  2. 动态化

    • iOS禁止,但国内平台普遍需要

  3. 多端一致性

    • Web方案无法还原体验

  4. 性能

    • Web方案UI绘制效率低,网络流量消耗高

    • 游戏引擎耗电严重,不能应用在普通应用开发中

    • SDK引入导致的安装包增量

2. 历史行程

在过去的十多年间,主流的(不考虑一些小众、没有取得成功的方案)移动端跨平台技术经历了三次变革:

  • Hybrid,代表有:Ionic/Cordova

  • OEM Wrapper,代表有:React Native/Weex

  • 自渲染,代表有:Flutter

从时间上看,这三种方案不是孤立的,既有对前人不足之处的改进(如UI绘制策略),也有对优秀思想的继承(如React的思想)。如果站在更高的高度上,我们会看到这些方案并不是在移动端独立演进的。在移动端普及之前,PC端已经积累了很多成熟的方案,对于移动端的探索起到了指导作用,仔细比较一下会发现每一种方案都能找到已有方案的影子,只不过结合了移动端的特点做了定制。 无论哪种跨平台方案,都要回答两个问题:

  1. UI如何绘制

  2. 逻辑(包括用户交互的逻辑和与宿主系统通讯的逻辑)如何响应

对于这两个问题,Hybrid给出的答案是webview+js,OEM Wrapper给出的是VirtualDOM转Native组件+js,自渲染(Flutter)给出的答案是Skia+Dart。下文开始详述。

2.1. Hybrid

texingfenxi

架构图

hybrid

Hybrid是客户端跨平台技术的第一个阶段,核心原理是
将原生的接口封装后暴露给 JavaScript,可以运行在系统自带的 WebView中或者其他内核中。这种方案在上文提到的评价体系里表现如下:

  1. 开发效率

    • 对前端开发者友好,背靠前端庞大的JavaScript生态

    • 涉及到Native调用的部分不可避免要熟悉Android/iOS

    • 能力受限于桥接层,扩展性弱

    • 在移动端开发,调试和错误日志并不是很友好

  2. 动态化

    • Web天生自带动态能力

  3. 多端一致性

    • 浏览器内核的渲染独立于系统组件,无法保证原生体验

    • 涉及宿主的问题,需要开发者处理,做不到完全屏蔽

  4. 性能

    • 受限于网络环境,比Native更加消耗流量

    • 受限于浏览器、系统平台特性

    • 渲染性能 ,Webview性能差

    • 特别指出:对于列表的支持差,移动端几乎全是列表(feed流)

评价:Hybrid是矛盾的结合体,HTML/CSS 过于复杂导致性能问题,但其实这正是 Web 最大的优势所在,因为 Web 最初的目的就是显示文档,如果你想显示丰富的图文排版,虽然 iOS/Android 都有富文本组件,但比起 CSS 差太远了,所以在很多 Native 应用中是不可避免要嵌 Web 的(比如很多运营活动的页面,存在周期短,开发时间短,样式丰富繁多,适合H5开发)。

既然Web强大的的绘制能力限制了其在移动端的性能,那么能不能对此进行优化? 这是一个很重要的问题,很多看起来无关的方案都是基于这种思想发源来的:

  • React Native使用系统组件封装,可以认为是把原来的浏览器内核换成了一个简化版的内核:一个不能做物理渲染,只能转换成有限原生组件的内核。

  • Flutter的 Engine 模块也可以认为是一个浏览器内核的角色!事实上Flutter的前身Sky就是打算基于一个精简的Chromium内核来实现跨平台。

2.2. OEM Wrapper

架构图

oem

React Native架构图

Snipaste 2019 11 16 16 15 28

大概到了2015年,经历了各种Hybrid方案割据混战长达数年后,Facebook推出了React Native,这种方案迎合了大前端的趋势,一经推出就备受关注。核心改变是抛弃了低效的浏览器内核渲染,转而使用自己的DSL生成中间格式,进而映射到对应的平台。
其实这个方案其实也算不上什么创举,在PC时代,The Standard Widget Toolkit采用的就是这种方案(当然要做到React Native这种水平,非Facebook级别的公司不能为也):

From SWT官网

React Native 在评价体系表现如下:

  1. 开发效率

    • 在Web基础上引进了React等能力,符合前端大趋势

    • 版本变动频繁,需要开发者自己优化,工作量大

    • 与Native交互需要开发者自己支持,维护成本高

    • 文档不完善、调试信息、错误日志提示不够友好

  2. 动态化

    • 可以支持

  3. 多端一致性

    • 渲染成各自平台的组件,可以保证Native的体验

    • 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后

    • 其控件系统也会受到原生UI系统限制,例如,在Android中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手

  4. 性能

    • 稍差于Native,但远好于Hybrid

    • 渲染时需要JavaScript和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿

    • JavaScript为脚本语言,执行时需要JIT,执行效率和AOT代码仍有差距。

评价:使用类前端的语法,但又不在浏览器内核直接绘制,而是转成Native控件,交由系统绘制。这样既保留了前端这套开发体系,又最大限度保证了渲染的性能。咋一看,React Native解决了 Hybrid 技术的痛点:渲染性能,又充分发挥了Hybrid 的优势:前端技术栈。但就在2018年,Airbnb和Udacity相继宣布弃用React Native,Facebook也宣布要大规模重构React Native,导致其前景堪忧,比起React Native的美好愿景,其在开发过程中需要踩的坑更多,长期的维护成本也很高,反而降低了开发效率,此外,库的增量也不容忽视。

2.3. 自渲染

架构图

flutter

Flutter 在评价体系表现如下:

  1. 开发效率

    • 开发工具完备,提供了VS Code(最流行的编辑器),Intellij IDEA 插件

    • Google背书,文档完备,社区较完备

    • Dart语言本身有上手成本,没有前端的生态,但Dart语言本身是及其优秀的

  2. 动态化

    • 动态性不足,为了保证UI绘制性能,自绘UI系统一般都会采用AOT模式编译其发布包

    • 可能涉及安全政策,Flutter Release目前不支持

    • 国内开发者正在做积极探索

  3. 多端一致性

    • 自绘制UI,提供了Material Design和Cupertino两种风格的Widget

  4. 性能

    • 性能和Native绘制一样

评价:Flutter站在前人的肩膀上,取长(React的状态管理、Web的自绘制UI、React Native的HotReload等)补短(与Native通信的Channel机制、自渲染、完备的开发工具链),并且有Google作为作为支撑,在跨平台领域后发制人,是目前最被看好的方案。

关于Dart,在开发者踩了十几年坑之后,Google和Microsoft两大巨头似乎看清了需要一种新的、更现代、更适合UI开发的编程语言来重新建立秩序。

Snipaste 2019 11 16 20 22 00

谷歌要推Dart,微软要推Rust,这两门语言的年龄比很多开发者的从业年龄都要小,大概是要以牺牲一代开发者为代价换取一个没有历史包袱(做梦)的新生态吧(手动狗头)。

3. Flutter初探

3.1. Flutter技术架构

FlutterSystemArchitecture01

在Framework层,有以下内容

  • Foundation 底层库

  • 负责UI绘制和交互处理的 AnimationPaint 等模块

  • 基于上面的接口实现的基础组件 Widgets

  • 基于组件实现的 Material 风格组件(Google推荐)和 Cupertino 风格组件(iOS风格)

在Engine层,有以下内容

  • Skia 图形处理库

  • Dart 运行时

  • Text 文字排版引擎

这张图和Android自身的架构很像

android stack 2x

这种设计符合Unix哲学的 正交性 原则(计算机网络中的OSI模型也是),可以很好地屏蔽每一个层的细节,接下来我们基于这个架构图讨论几个问题

  • Flutter如何绘制UI

  • 为什么选择Dart

  • 如何和Native交互

  • HotReload与动态化

3.2. GUI Framework

在开始下文之前,需要介绍一下现代GUI框架的模型,无论是Web、PC、移动端、游戏开发,都是基于这种框架工作

Snipaste 2019 11 17 13 51 11

  • Widgets(控件,也叫 View Tree),它是用于描述用户界面原始数据的树状结构。通常这一层根本不关心绘制,它只关心用户对数据的操作。

  • Render Tree,它是一种更为抽象的树状数据结构,一般来说它是和上一步的 View Tree结构相同,并且它不关心原始数据,只关心控件的布局和大小。通过这一步计算出控件布局后才能真正地确定控件的外观。

  • Layer Tree 跟 Render Tree是相对应的,这一步会主动触发 Render Tree中每个元素的外观渲染,在已知控件大小和位置的情况下决定每个控件的真正外观。但 Layer Tree的树状结构不是和 Render Tree一一对应的,Layer Tree有可能因为 Layer合并优化导致一层的 Render Tree叶子节点最终只对应一个 Layer。

  • 在已经决定好控件的大小位置以及长相后,剩下的工作就需要把这些东西组合起来显示到屏幕上。这一步原理比较简单,就是将前一步的 Layer合并成一张 Bitmap,这是一种最简单的图像存储形式。将 Bitmap光栅化后便可以提交给 GPU渲染。

比如Android开发都很熟悉的 measure layout draw 就和前三层吻合,有了原始数据还不够,屏幕只关心每个像素点的值,所以最后要进行一次光栅化,历史已经证明这种模型是目前来说相对高效的GUI方案。

3.3. Flutter如何绘制UI

有了以上背景,我们来看Flutter的UI绘制过程。

Flutter绘制流程1

FlutterSystemArchitecture02

Flutter绘制流程2

FlutterSystemArchitecture03

  • GPU发出 Vsync 信号,Dart捕获后就开始一帧的绘制。

  • Throttle: 用来做节流,防止短时间内重复调用,提高性能。

  • Compositor: 这一步进行 Layer合成,决定某一块具体显示哪一个 Layer的数据,可以额外的计算开支。

  • GL or Vulkan: 这一阶段过后得到的将是一份矢量图数据,在进行光栅化后提交给 GPU执行渲染即可。

3.4. 为什么选择Dart

官方解释:

  • Developer productivity

    • JIT + AOT

    • Dart开发团队对于Flutter支持粒度很大

  • Object-orientation

  • Predictable, high performance

  • Fast allocation.

其他声音:

  • Dart 的性能更好,对高帧率下的视图数据计算很有帮助。

  • 多生代无锁垃圾回收器,专门为UI框架中常见的大量Widgets对象创建和销毁优化

  • Native Binding,在 Android上,v8的 Native Binding可以很好地实现,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基础框架的代码模式就很难统一了。而 Dart的 Native Binding可以很好地通过 Dart Lib实现。

  • Fuchsia OS(谷歌的野心:5G + IOT) 

  • Dart是类型安全的语言,拥有完善的包管理和诸多特性。

关于IOT,国内已经有开发者尝试在树莓派上使用Flutter了。

总的来说,想要挑战JavaScript的地位还是很难的,JavaScript虽然有很多缺陷,但庞大的生态已经为自己建立了稳固的堡垒。

有一个关于Lisp的笑话,我觉得也可以改个Dart版本的:某小偷偷了美国国防部机密软件的源码的最后几页打算拿回去好好研究,但等他真的打开时发现是这样

Snipaste 2019 11 16 20 32 51

3.5. 如何和Native交互

Flutter Platform Channel Demo

通过 Platform Channel机制进行通信,常用的 Platform Channel主要有三种

  • BasicMessageChannel:传递字符串和半结构化数据

  • MethodChannel:方法调用

  • EventChannel:数据流的通信

每种Channel具有三个重要的成员变量:

  • name: String类型,代表Channel的名字,也是其唯一标识符。

  • messager:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具。

  • codec: MessageCodec类型或MethodCodec类型,代表消息的编解码器。

对常见的Channel进一步封装成Plugin

Snipaste 2019 11 17 18 23 39

3.6. HotReload与动态化

Flutter Hot Demo

Flutter的HotReload确实让人耳目一新,但这东西可能只是对于客户端开发比较稀奇,从Instant Run到freeline,开发者一直希望能提高项目构建速度,更快地看到代码改动的结果,从原理上来说,只要这门语言及其所在平台支持解释执行(or JIT)和增量编译就可以做到很好的HotReload效果,flutter的这一个特性算是填上了之前Android开发的一个坑,创举肯定是算不上的。 至于动态化,可以算是国内开发者的刚需了,Flutter之前移除了对动态化的支持,可能是担心iOS的政策原因,目前不少开发者也对Flutter展开了研究,后面应该有一批魔改方案。
Flutter的HotReload过程:

  1. 首先会扫描代码,找到上次编译之后有变化的Dart代码;

  2. 将这些变化的Dart代码转化为增量的Dart Kernel文件;

  3. 将增量的Dart Kernel文件发送到正在移动设备上运行的Dart VM;

  4. 触发widgets树的重新建立、重新布局、重新绘制

不能使用的场景(所以对这个功能不能过于乐观,业务复杂之后能用的场景就不会太多):

  1. 代码出现编译错误的不能使用 Hot Reload

  2. Widget的状态更改不能使用 Hot Reload

  3. 在 Flutter 中,全局变量和静态字段被视为状态,因此在 Hot Reload 期间不会重新初始化。

  4. 修改通用类型声明时

4. 评价

从Hybrid到Flutter,突破性的创新是不会有的,每一个特性、功能都是扎扎实实演进过来的,这也是标题的本意,Flutter能否成为大前端的答案尚未可知,谷歌布的局又有多大也不清楚,核心还是要把握住发展脉络,对新事物保持警惕和清醒。浪潮退去才知道谁在裸泳,有一天Flutter的替代者来了,才知道谁是API boy(哭泣脸)。

5. 参考