Flutter 实现原理及在马蜂窝的跨平台开发实践

一直以来,跨平台开发都是困扰移动客户端开发的难题。前端

在马蜂窝旅游 App 不少业务场景里,咱们尝试过一些主流的跨平台开发解决方案,好比 WebView 和 React Native,来提高开发效率和用户体验。但这两种方式也带来了新的问题。android

好比使用 WebView 跨平台方式,优势确实很是明显。基于 WebView 的框架集成了当下 Web 开发的诸多优点:丰富的控件库、动态化、良好的技术社区、测试自动化等等。可是缺点也一样明显:渲染效率和 JavaScript 的执行能力都比较差,使页面的加载速度和用户体验都不尽如人意。ios

而使用以 React Native(简称 RN)为表明的框架时,维护又成了大难题。RN 使用类 HTML+JS 的 UI 建立逻辑,生成对应的原生页面,将页面的渲染工做交给了系统,因此渲染效率有很大的优点。但因为 RN 代码是经过 JS 桥接的方式转换为原生的控件,因此受各个系统间的差别影响很是大,虽然能够开发一套代码,但对各个平台的适配却很是的繁琐和麻烦。git

为何是 Flutter

2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,马蜂窝电商客户端团队进行了调研与实践,发现 Flutter 能很好的帮助咱们解决开发中遇到的问题。github

  1. 跨平台开发针对 Android 与 iOS 的风格设计了两套设计语言的控件实现(Material & Cupertino)。这样不但可以节约人力成本,并且在用户体验上更好的适配 App 运行的平台。web

  2. 重写了一套跨平台的 UI 框架,渲染引擎是依靠 Skia 图形库实现。Flutter 中的控件树直接由渲染引擎和高性能本地 ARM 代码直接绘制,不须要经过中间对象(Web 应用中的虚拟 DOM 和真实 DOM,原生 App 中的虚拟控件和平台控件)来绘制,使它有接近原生页面的性能,帮助咱们提供更好的用户体验。xcode

  3. 同时支持 JIT 和 AOT 编译。JIT 编译方式使其在开发阶段有个备受欢迎的功能——热重载(HotReload),这样在开发时能够省去构建的过程,提升开发效率。而在 Release 运行阶段采用 AOT 的编译方式,使执行效率很是高,让 Release 版本发挥更好的性能。bash

因而,电商客户端团队决定探索 Flutter 在跨平台开发中的新可能,并率先应用于商家端 App 中。在本文中,咱们将结合 Flutter 在马蜂窝商家端 App 中的应用实践,探讨 Flutter 架构的实现原理,有何优点,以及如何帮助咱们解决问题。前端工程师

Flutter 架构和实现原理

Flutter 使用 Dart 语言开发,主要有如下几点缘由:多线程

  • Dart 通常状况下是运行 DartVM 上,可是也能够编译为 ARM 代码直接运行在硬件上。

  • Dart 同时支持 AOT 和 JIT 两种编译方式,能够更好的提升开发以及 App 的执行效率。

  • Dart 能够利用独特的隔离区(Isolate)实现多线程。并且不共享内存,能够实现无锁快速分配。

  • 分代垃圾回收,很是适合 UI 框架中常见的大量 Widgets 对象建立和销毁的优化。

  • 在为建立的对象分配内存时,Dart 是在现有的堆上移动指针,保证内存的增加是程线性的,因而就省了查找可用内存的过程。

Dart 主要由 Google 负责开发和维护。目前 Dart 最新版本已是 2.2,针对 App 和 Web 开发作了不少优化。而且对于大多数的开发者而言,Dart 的学习成本很是低。

Flutter 架构也是采用的分层设计。从下到上依次为:Embedder(嵌入器)、Engine、Framework。

图 1: Flutter 分层架构图

Embedder 是嵌入层,作好这一层的适配 Flutter 基本能够嵌入到任何平台上去; Engine 层主要包含 Skia、Dart 和 Text。Skia 是开源的二位图形库;Dart 部分主要包括 runtime、Garbage Collection、编译模式支持等;Text 是文本渲染。Framework 在最上层。咱们的应用围绕 Framework 层来构建,所以也是本文要介绍的重点。

Framework

  1. Foundation 在最底层,主要定义底层工具类和方法,以提供给其余层使用。

  2. Animation 是动画相关的类,能够基于此建立补间动画(Tween Animation)和物理原理动画(Physics-based Animation),相似 Android 的 ValueAnimator 和 iOS 的 Core Animation。

  3. Painting 封装了 Flutter Engine 提供的绘制接口,例如绘制缩放图像、插值生成阴影、绘制盒模型边框等。

  4. Gesture 提供处理手势识别和交互的功能。

  5. Rendering 是框架中的渲染库。控件的渲染主要包括三个阶段:布局(Layout)、绘制(Paint)、合成(Composite)。

从下图能够看到,Flutter 流水线包括 7 个步骤。

图 2: Flutter 流水线

首先是获取到用户的操做,而后你的应用会所以显示一些动画,接着 Flutter 开始构建 Widget 对象。

Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:

  • 布局元素:决定页面元素在屏幕上的位置和大小;

  • 绘制阶段:将页面元素绘制成它们应有的样式;

  • 合成阶段:按照绘制规则将以前两个步骤的产物组合在一块儿。

最后的光栅化由 Engine 层来完成。

在渲染阶段,控件树(widget)会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。

在布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将本身的约束传递给子节点,子节点根据接收到的约束来计算本身的大小,而后将本身的尺寸返回给父节点。整个过程当中,位置信息由父节点来控制,子节点并不关心本身所在的位置,而父节点也不关心子节点具体长什么样子。

图 3: 数据流传递方式

为了防止因子节点发生变化而致使的整个控件树重绘,Flutter 加入了一个机制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 会被自动建立,不须要开发者手动添加。

例如,控件被设置了固定大小(tight constraint)、控件忽略全部子视图尺寸对本身的影响、控件自动占满父控件所提供的空间等等。很好理解,就是控件大小不会影响其余控件时,就不必从新布局整个控件树。有了这个机制后,不管子树发生什么样的变化,处理范围都只在子树上。

图 4: Relayout Boundary 机制

在肯定每一个空间的位置和大小以后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,而后把不一样的 RenderObject 绘制到不一样的图层上。

这时有可能出现一种特殊状况,以下图所示节点 2 在绘制子节点 4 时,因为其节点 4 须要单独绘制到一个图层上(如 video),所以绿色图层上面多了个黄色的图层。以后再须要绘制其余内容(标记 5)就须要再增长一个图层(红色)。再接下来要绘制节点 1 的右子树(标记 6),也会被绘制到红色图层上。因此若是 2 号节点发生改变就会改变红色图层上的内容,所以也影响到了绝不相干的 6 号节点。

图 5: 绘制节点与图层的关系

为了不这种状况,Flutter 的设计者这里基于 Relayout Boundary 的思想增长了 Repaint Boundary。在绘制页面时候若是碰见 Repaint Boundary 就会强制切换图层。

以下图所示,在从上到下遍历控件树遇到 Repaint Boundary 会从新绘制到新的图层(深蓝色),在从下到上返回的时候又遇到 Repaint Boundary,因而又增长一个新的图层(浅蓝色)。

图 6: Repaint Boundary 机制

这样,即便发生重绘也不会对其余子树产生影响。好比在 Scrollview 上,当滚动的时候发生内容重绘,若是在 Scrollview 之外的地方不须要重绘就可使用 Repaint Boundary。Repaint Boundary 并不会像 Relayout Boundary 同样自动生成,而是须要咱们本身来加入到控件树中。

6.【Widget】控件层。全部控件的基类都是 Widget,Widget 的数据都是只读的, 不能改变。因此每次须要更新页面时都须要从新建立一个新的控件树。每个 Widget 会经过一个 RenderObjectElement 对应到一个渲染节点(RenderObject),能够简单理解为 Widget 中只存储了页面元素的信息,而真正负责布局、渲染的是 RenderObject。

在页面更新从新生成控件树时,RenderObjectElement 树会尽可能保持重用。因为 RenderObjectElement 持有对应的 RenderObject,全部 RenderObject 树也会尽量的被重用。如图所示就是三棵树之间的关系。在这张图里咱们把形状当作渲染节点的类型,颜色是它的属性,即形状不一样就是不一样的渲染节点,而颜色不一样只是同一对象的属性的不一样。

图 7: Widget、Element 和 Render 之间的关系

若是想把方形的颜色换成黄色,将圆形的颜色变成红色,因为控件是不能被修改的,须要从新生成两个新的控件 Rectangle yellow 和 Circle red。因为只是修改了颜色属性,因此 Element 和 RenderObject 都被重用,而以前的控件树会被释放回收。

图 8: 示例

那么若是把红色圆形变成三角形又会怎样呢?因为这里发生变化的是类型,因此对应的 Element 节点和 RenderObject 节点都须要从新建立。可是因为黄色方形没有发生改变,因此其对应的 Element 节点和 RenderObject 节点没有发生变化。

图 9: 示例

7. 最后是【Material】 & 【Cupertino】,这是在 Widget 层之上框架为开发者提供的基于两套设计语言实现的 UI 控件,能够帮助咱们的 App 在不一样平台上提供接近原生的用户体验。

Flutter 在马蜂窝商家端App 中的应用实践

图 10: 马蜂窝商家端使用 Flutter 开发的页面

开发方式:Flutter + Native

因为商家端已是一款成熟的 App,不可能建立一个新的 Flutter 工程所有从新开发,所以咱们选择 Native 与 Flutter 混编的方案来实现。
在了解 Native 与 Flutter 混编方案前,首先咱们须要了解在 Flutter 工程中,一般有如下 4 种工程类型:

1. Flutter Application

标准的 Flutter App 工程,包含标准的 Dart 层与 Native 平台层。

2. Flutter Module

Flutter 组件工程,仅包含 Dart 层实现,Native 平台层子工程为经过 Flutter 自动生成的隐藏工程(.ios/.android)。

3. Flutter Plugin

Flutter 平台插件工程,包含 Dart 层与 Native 平台层的实现。

4. Flutter Package

Flutter 纯 Dart 插件工程,仅包含 Dart 层的实现,每每定义一些公共 Widget。

了解了 Flutter 工程类型后,咱们来看下官方提供的一种混编方案(github.com/flutter/flu…),即在现有工程下建立 Flutter Module 工程,以本地依赖的方式集成到现有的 Native 工程中。

官方集成方案(以 iOS 为例)

a. 在工程目录建立 FlutterModule,建立后,工程目录大体以下:

b. 在 Podfile 文件中添加如下代码:

flutter_application_path = '../flutter_Moudule/'
复制代码

该脚本主要负责:

  • pod 引入 Flutter.Framework 以及 FlutterPluginRegistrant 注册入口

  • pod 引入 Flutter 第三方 plugin

  • 在每个 pod 库的配置文件中写入对 Generated.xcconfig 文件的导入

  • 修改 pod 库的 ENABLE_BITCODE = NO(由于 Flutter 如今不支持 bitcode)

c. 在 iOS 构建阶段 Build Phases 中注入构建时须要执行的 xcode_backend.sh (位于 FlutterSDK/packages/flutter_tools/bin) 脚本:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

复制代码

该脚本主要负责:

  • 构建 App.framework 以及 Flutter.framework 产物

  • 根据编译模式(debug/profile/release)导入对应的产物

  • 编译 flutter_asset 资源

  • 把以上产物 copy 到对应的构建产物中

d. 与 Native 通讯

  • 方案一:改造 AppDelegate 继承自 FlutterAppDelegate

  • 方案二:AppDelegate 实现 FlutterAppLifeCycleProvider 协议,生命周期由 FlutterPluginAppLifeCycleDelegate 传递给 Flutter

以上就是官方提供的集成方案。咱们最终没有选择此方案的缘由,是它直接依赖于 FlutterModule 工程以及 Flutter 环境,使 Native 开发同窗没法脱离 Flutter 环境开发,影响正常的开发流程,团队合做成本较大;并且会影响正常的打包流程。(目前 Flutter 团队正在重构嵌入 Native 工程的方式)

最终咱们选择另外一种方案来解决以上的问题:远端依赖产物

图 11 :远端依赖产物

iOS 集成方案

经过对官方混编方案的研究,咱们了解到 iOS 工程最终依赖的实际上是 FlutterModule 工程构建出的产物(Framework,Asset,Plugin),只需将产物导出并 push 到远端仓库,iOS 工程经过远端依赖产物便可。

依赖产物目录结构以下:

  • App.framework: Flutter 工程产物(包含 Flutter 工程的代码,Debug 模式下它是个空壳,代码在 flutter_assets 中)。

  • **Flutter.framework:*Flutter 引擎库。与编译模式(debug/profile/release)以及 CPU 架构(arm, i386, x86_64)相匹配。

  • lib.a & .h 头文件: FlutterPlugin 静态库(包含在 iOS 端的实现)。

  • flutter_assets: 包含 Flutter 工程字体,图片等资源。在 Flutter1.2 版本中,被打包到 App.framework 中。

Android 集成方案

Android Nativite 集成是经过 Gradle 远程依赖 Flutter 工程产物的方式完成的,如下是具体的集成流程。

a.建立 Flutter 标准工程

$ flutter create flutter_demo

复制代码

默认使用 Java 代码,若是增长 Kotlin 支持,使用以下命令:

$ flutter create -a kotlin flutter_demo

复制代码

b. 修改工程的默认配置

  1. 修改 app module 工程的 build.gradle 配置  apply plugin: 'com.android.application' => apply plugin: 'com.android.library',并移除 applicationId 配置

  2. 修改 root 工程的 build.gradle 配置

    在集成过程当中 Flutter 依赖了三方 Plugins 后,遇到 Plugins 的代码没有被打进 Library 中的问题。经过如下配置解决(这种方式略显粗暴,后续的优化方案正在调研)。

subprojects {
   project.buildDir = "${rootProject.buildDir}/app"
}
复制代码
  1. app module 增长 maven 打包配置
  2. c. 生成 Android Flutter 产物
$ cd android
$ ./gradlew uploadArchives
复制代码

官方默认的构建脚本在 Flutter 1.0.0 版本存在 Bug——最终的产物中会缺乏 flutter_shared/icudtl.dat 文件,致使 App Crash。目前的解决方式是将这个文件复制到工程的 assets 下( 在 Flutter 最新 1.2.1 版本中这个 Bug 已被修复,可是 1.2.1 版本又出现了一个 UI 渲染的问题,因此只能继续使用 1.0.0 版本)。

d. Android Native 平台工程集成,增长下面依赖配置便可,不会影响 Native 平台开发的同窗。

implementation 'com.mfw.app:MerchantFlutter:0.0.5-beta'
复制代码

Flutter 和 iOS、Android 的交互

使用平台通道(Platform Channels)在 Flutter 工程和宿主(Native 工程)之间传递消息,主要是经过 MethodChannel 进行方法的调用,以下图所示:

图 12 :Flutter 与 iOS、Android 交互

为了确保用户界面不会挂起,消息和响应是异步传递的,须要用 async 修饰方法,await 修饰调用语句。Flutter 工程和宿主工程经过在 Channel 构造函数中传递 Channel 名称进行关联。单个应用中使用的全部 Channel 名称必须是惟一的; 能够在 Channel 名称前加一个惟一的「域名前缀」。

Flutter 与 Native 性能对比

咱们分别使用 Native 和 Flutter 开发了两个列表页,如下是页面效果和性能对比:

iOS 对比(机型 6P 系统 10.3.3):

Flutter 页面:

iOS Native 页面:

能够看到,从使用和直观感觉都没有太大的差异。因而咱们采集了一些其余方面的数据。

Flutter 页面:

iOS Native 页面:

另外咱们还对比了商家端接入 Flutter 先后包体积的大小:39Mb→44MB

在 iOS 机型上,流畅度上没有什么差别。从数值上来看,Flutter 在 内存跟 GPU/CPU 使用率上比原生略高。Demo 中并无对 Flutter 作更多的优化,能够看出 Flutter 总体来讲仍是能够作出接近于原生的页面。

下面是 Flutter 与 Android 的性能对比

Flutter 页面:

Android Native 页面:

从以上两张对比图能够看出,不考虑其余因素,单纯从性能角度来讲,原生要优于 Flutter,可是差距并不大,并且 Flutter 具备的跨平台开发和热重载等特色极大地节省了开发效率。而且,将来的热修复特性更是值得期待。

混合栈管理

首先先介绍下 Flutter 路由的管理:

  • Flutter 管理页面有两个概念:Route 和 Navigator。

  • Navigator 是一个路由管理的 Widget(Flutter 中万物皆 Widget),它经过一个栈来管理一个路由 Widget 集合。一般当前屏幕显示的页面就是栈顶的路由。

  • 路由 (Route) 在移动开发中一般指页面(Page),这跟 web 开发中单页应用的 Route 概念意义是相同的,Route 在 Android 中一般指一个 Activity,在 iOS 中指一个 ViewController。所谓路由管理,就是管理页面之间如何跳转,一般也可被称为导航管理。这和原生开发相似,不管是 Android 仍是 iOS,导航管理都会维护一个路由栈,路由入栈 (push) 操做对应打开一个新页面,路由出栈 (pop) 操做对应页面关闭操做,而路由管理主要是指如何来管理路由栈。

图 14 :Flutter 路由管理

若是是纯 Flutter 工程,页面栈无需咱们进行管理,可是引入到 Native 工程内,就须要考虑如何管理混合栈。而且须要解决如下几个问题:

1. 保证 Flutter 页面与 Native 页面之间的跳转从用户体验上没有任何差别

2. 页面资源化(马蜂窝特有的业务逻辑)

3. 保证生命周期完整性,处理相关打点事件上报

4. 资源性能问题

参考了业界内的解决方法,以及项目自身的实际场景,咱们选择相似于 H5 在 Navite 中嵌入的方式,统一经过 openURL 跳转到一个 Native 页面(FlutterContainerVC),Native 页面经过 addChildViewController 方式添加 FlutterViewController(负责 Flutter 页面渲染),同时经过 channel 同步 Native 页面与 Flutter 页面。

  • 每一次的 push/pop 由 Native 发起,同时经过 channel 保持 Native 与 Flutter 页面同步——在 Native 中跳转 Flutter 页面与跳转原生无差别

  • 一个 Flutter 页面对应一个 Native 页面(FlutterContainerVC)——解决页面资源化

  • FlutterContainerVC 经过 addChildViewController 对单例 FlutterViewController 进行复用——保证生命周期完整性,处理相关打点事件上报

  • 因为每个 FlutterViewController(提供 Flutter 视图的实现)会启动三个线程,分别是 UI 线程、GPU 线程和 IO 线程,使用单例 FlutterViewController 能够减小对资源的占用——解决资源性能问题

Flutter 应用总结

Flutter 一经发布就很受关注,除了 iOS 和 Android 的开发者,不少前端工程师也都很是看好 Flutter 将来的发展前景。相信也有不少公司的团队已经投入到研究和实践中了。不过 Flutter 也有不少不足的地方,值得咱们注意:

  1. 虽然 1.2 版本已经发布,可是目前没有达到彻底稳定状态,1.2 发布完了就出现了控件渲染的问题。加上 Dart 语言生态小,学习资料可能不够丰富。

  2. 关于动态化的支持,目前 Flutter 还不支持线上动态性。若是要在 Android 上实现动态性相对容易些,iOS 因为审核缘由要实现动态性可能成本很高。

  3. Flutter 中目前拿来就用的能力只有 UI 控件和 Dart 自己提供能力,对于平台级别的能力还须要经过 channel 的方式来扩展。

  4. 已有工程迁移比较复杂,之前沉淀的 UI 控件,须要从新再实现一套。

  5. 最后一点比较有争议,Flutter 不会从程序中拆分出额外的模板或布局语言,如 JSX 或 XM L,也不须要单独的可视布局工具。有的人认为配合 HotReload 功能使用很是方便,但咱们发现这样代码会有很是多的嵌套,阅读起来有些吃力。

目前阿里的闲鱼开发团队已经将 Flutter 用于大型实践,并应用在了比较重要的场景(如产品详情页),为后来者提供了良好的借鉴。马蜂窝的移动客户端团队关于 Flutter 的探索才刚刚起步,前面还有不少的问题须要咱们一点一点去解决。不过不管从 Google 对其的重视程度,仍是咱们从实践中看到的这些优势,都让咱们对 Flutter 充满信心,也但愿在将来咱们能够利用它创造更多的价值和奇迹。

路途虽远,犹可期许。

本文做者:马蜂窝电商研发客户端团队。

(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)

参考文献:

关注马蜂窝技术,找到更多你想要的内容

相关文章
相关标签/搜索