一直以来,跨平台开发都是困扰移动客户端开发的难题。
鹰潭ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:028-86922220(备注:SSL证书合作)期待与您的合作!
在马蜂窝旅游 App 很多业务场景里,我们尝试过一些主流的跨平台开发解决方案, 比如WebView 和 React Native,来提升开发效率和用户体验。但这两种方式也带来了新的问题。
比如使用 WebView 跨平台方式,优点确实非常明显。基于 WebView 的框架集成了当下 Web 开发的诸多优势:丰富的控件库、动态化、良好的技术社区、测试自动化等等。但是缺点也同样明显:渲染效率和 JavaScript 的执行能力都比较差,使页面的加载速度和用户体验都不尽如人意。
而使用以 React Native(简称 RN)为代表的框架时,维护又成了大难题。RN 使用类 HTML+JS 的 UI 创建逻辑,生成对应的原生页面,将页面的渲染工作交给了系统,所以渲染效率有很大的优势。但由于 RN 代码是通过 JS 桥接的方式转换为原生的控件,所以受各个系统间的差异影响非常大,虽然可以开发一套代码,但对各个平台的适配却非常的繁琐和麻烦。
为什么是 Flutter
2018 年 12 月初,Google 正式发布了开源跨平台 UI 框架 Flutter 1.0 Release 版本,马蜂窝电商客户端团队进行了调研与实践,发现Flutter能很好的帮助我们解决开发中遇到的问题。
于是,电商客户端团队决定探索 Flutter 在跨平台开发中的新可能,并率先应用于商家端 App 中。在本文中,我们将结合 Flutter 在马蜂窝商家端 App 中的应用实践,探讨 Flutter 架构的实现原理,有何优势,以及如何帮助我们解决问题。
Flutter 架构和实现原理
Flutter 使用 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 工程类型后,我们来看下官方提供的一种混编方案(https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps),即在现有工程下创建 Flutter Module 工程,以本地依赖的方式集成到现有的 Native 工程中。
官方集成方案(以 iOS 为例)
a. 在工程目录创建 FlutterModule,创建后,工程目录大致如下:
b. 在 Podfile 文件中添加以下代码:
- flutter_application_path = '../flutter_Moudule/'
- eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
该脚本主要负责:
c. 在 iOS 构建阶段 Build Phases 中注入构建时需要执行的 xcode_backend.sh (位于 FlutterSDK/packages/flutter_tools/bin) 脚本:
- "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
- "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
该脚本主要负责:
d. 与 Native 通信
以上就是官方提供的集成方案。我们最终没有选择此方案的原因,是它直接依赖于 FlutterModule 工程以及 Flutter 环境,使 Native 开发同学无法脱离 Flutter 环境开发,影响正常的开发流程,团队合作成本较大;而且会影响正常的打包流程。(目前 Flutter 团队正在重构嵌入 Native 工程的方式)
最终我们选择另一种方案来解决以上的问题:远端依赖产物。
图11 :远端依赖产物
iOS 集成方案
通过对官方混编方案的研究,我们了解到 iOS 工程最终依赖的其实是 FlutterModule 工程构建出的产物(Framework,Asset,Plugin),只需将产物导出并 push 到远端仓库,iOS 工程通过远端依赖产物即可。
依赖产物目录结构如下:
Android 集成方案
Android Nativite 集成是通过 Gradle 远程依赖 Flutter 工程产物的方式完成的,以下是具体的集成流程。
a.创建 Flutter 标准工程
- $ flutter create flutter_demo
默认使用 Java 代码,如果增加 Kotlin 支持,使用如下命令:
- $ flutter create -a kotlin flutter_demo
b. 修改工程的默认配置
在集成过程中 Flutter 依赖了三方 Plugins 后,遇到 Plugins 的代码没有被打进 Library 中的问题。通过以下配置解决(这种方式略显粗暴,后续的优化方案正在调研)。
- subprojects {
- project.buildDir = "${rootProject.buildDir}/app"
- }
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 路由的管理:
图14 :Flutter 路由管理
如果是纯 Flutter 工程,页面栈无需我们进行管理,但是引入到 Native 工程内,就需要考虑如何管理混合栈。并且需要解决以下几个问题:
1. 保证 Flutter 页面与 Native 页面之间的跳转从用户体验上没有任何差异
2. 页面资源化(马蜂窝特有的业务逻辑)
3. 保证生命周期完整性,处理相关打点事件上报
4. 资源性能问题
参考了业界内的解决方法,以及项目自身的实际场景,我们选择类似于 H5 在 Navite 中嵌入的方式,统一通过 openURL 跳转到一个 Native 页面(FlutterContainerVC),Native 页面通过 addChildViewController 方式添加 FlutterViewController(负责 Flutter 页面渲染),同时通过 channel 同步 Native 页面与 Flutter 页面。
Flutter 应用总结
Flutter 一经发布就很受关注,除了 iOS 和 Android 的开发者,很多前端工程师也都非常看好 Flutter 未来的发展前景。相信也有很多公司的团队已经投入到研究和实践中了。不过 Flutter 也有很多不足的地方,值得我们注意:
目前阿里的闲鱼开发团队已经将 Flutter 用于大型实践,并应用在了比较重要的场景(如产品详情页),为后来者提供了良好的借鉴。马蜂窝的移动客户端团队关于 Flutter 的探索才刚刚起步,前面还有很多的问题需要我们一点一点去解决。不过无论从 Google 对其的重视程度,还是我们从实践中看到的这些优点,都让我们对 Flutter 充满信心,也希望在未来我们可以利用它创造更多的价值和奇迹。
路途虽远,犹可期许。
参考文献:
Flutter's Layered Design
https://www.youtube.com/watch?v=dkyY9WCGMi0
Flutter's Rendering Pipeline
https://www.youtube.com/watch?v=UUfXWzp0-DU&t=1955s
Flutter原理与美团的实践https://juejin.im/post/5b6d59476fb9a04fe91aa778#comment
(题图来源:网络)
【本文是专栏作者马蜂窝技术的原创文章,作者微信公众号马蜂窝技术(ID:mfwtech)】
名称栏目:Flutter实现原理及在马蜂窝的跨平台开发实践
分享路径:http://www.shufengxianlan.com/qtweb/news26/231476.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联