闲鱼基于Flutter的移动端跨平台应用实践

闲鱼为什么使用 Flutter

Flutter 作为 Google 新一代的跨平台框架,有较多的优点,但跟其他跨平台解决方案相比,最吸引我们的是它的高性能,可以轻松构建更流畅的 UI。虽然各跨平台方案都有各自的特点,但 Flutter 的出现,给闲鱼、给大家都提供了一种新的可能性。

那么,Flutter 为什么会有高性能呢?

首先,Flutter 自建了一个绘制引擎,底层是由 C++ 编写的引擎,负责渲染,文本处理,Dart VM 等;上层的 Dart Framework 直接调用引擎。避免了以往 JS 解决方案的 JS Bridge、线程跳跃等问题。

第二,引擎基于 Skia 绘制,操作 OpenGL、GPU,不需要依赖原生的组件渲染框架。

第三,Dart 的引入,是 Flutter 团队做了很多思考后的决定,Dart 有 AOT 和 JIT 两种模式,线上使用时以 AOT 的方式编译成机器代码,保证了线上运行时的效率;而在开发期,Dart 代码以 JIT 的方式运行,支持代码的即时生效(HotReload),提高开发效率。

第四,Flutter 的页面和布局是基于 Widget 树的方式,看似不习惯,但这种树状结构解析简单,布局、绘制都可以单次遍历完成计算,而原生布局往往要往复多次计算,“simple is fast”的设计效果。

下面截图是目前闲鱼已经上线的商品详情页面:

图0:闲鱼基于Flutter的移动端跨平台应用实践

商品详情页包含混合栈、视频、动画、原生组件、多图、留言盖楼等功能,页面较复杂,有代表性,也是闲鱼最重要的页面之一。选择商品详情页做为第一个 Flutter 页面,是闲鱼能成功快速使用起 Flutter 的重要因素。

接下来介绍一下,闲鱼的实践过程和总结。

Flutter 与 Native 混合开发实践

Flutter Hybrid 工程实践(研发时)

我们把 Flutter 和闲鱼现有的 APP 做渐进式的整合,App 中会同时有 Native、Flutter 和 H5 页面。现有的 Flutter Demo 和应用,都是独立的 Flutter 应用,而当把它和 Native 混合的时候,会碰到很多的困难。

首先是研发时的问题,怎么让 Flutter 在现有的 Native 工程中开发起来。这个要从这张图说起:

图1:闲鱼基于Flutter的移动端跨平台应用实践

闲鱼 Flutter 工程结构如图,三个蓝色背景的目录分别是安卓工程、iOS 工程和 main.dart 入口。编译产物中以 iOS 为例,APP Framework 是 Flutter 应用页面代码,Flutter Framework 是 Flutter 引擎。

这个过程,需要重点考虑几个问题:如何基于现有工程搭建混合工程?如何支持过渡期的 Flutter 开发及纯 Native 开发的双开发模式?如何让 Flutter 与现有持续集成、构建工具集成?

图2:闲鱼基于Flutter的移动端跨平台应用实践

首先,现有的 Native 工程并不符合 Flutter 默认的规范,两者不能完全匹配,需要修改打包脚本,甚至修改 Flutter 的打包 Tool 来解决。另外,我们通过 Submodule 将现有⼯程引入到 Flutter 父工程中。

图3:闲鱼基于Flutter的移动端跨平台应用实践

纯 Native 开发同学,不需要引入 Flutter 工程,直接在 iOS 或 Android 工程下开发,Flutter 以产物的方式集成到 Native 中运行,Flutter 的开发同学引入 Submodule。

图4:闲鱼基于Flutter的移动端跨平台应用实践

上图是工程上的修改点。绿色虚线部分是 Flutter 默认的结构,红色虚线是闲鱼在 Flutter 基础上做的定制。Flutter 的构建工具 gen_snapshot,会把业务代码,Flutter 框架、引擎编译成中间产物,以 so 或 Framework 的方式变成 Native 的一部分。

几个主要的改动点:

第一,构建私有的仓库,用来管理阿里私有包,如 CDN、无线网关等中间件适配 Package。

第二,构建工具和引擎的优化。

第三,跟现有的构建工具打通,混合调试等。

图5:闲鱼基于Flutter的移动端跨平台应用实践

图6:闲鱼基于Flutter的移动端跨平台应用实践

Flutter Hybrid 栈管理

除了上述的研发时问题,接下来就是让它跑起来,解决运行时问题。其中最重要的是实现混合栈。

混合栈的定义

图7:闲鱼基于Flutter的移动端跨平台应用实践

在混合工程中,Native 页面,Flutter 页面之间会以多种可能的顺序混合入栈,出栈。要怎么去做?先看一下 Flutter 内部栈的管理默认下是怎么做的:

图8:闲鱼基于Flutter的移动端跨平台应用实践

整个 Flutter 运行在一个单例的 Activity 容器里(用安卓举例),Flutter 内部的所有页面都在这个容器中管理。 对安卓来说,怎样把这样容器里面的栈与 Native 栈混合起来,直接的一个想法就要把栈自己托管起来,把这个容器在 Android 的栈中来回移动。但 Android 里想这样操作非常难。

所以解决这个事情,就主要有两个问题要考虑,首先就是混合栈要在哪里管理?是在 Hybrid 栈管理,还是在 Flutter 管理,第二个就是关于实例剥离的问题,既然移动单例很复杂,那就把单例剥离出来,在上面 Wrap 出多个实例,这样就方便管理了。 下面是两个对比方案。

图9:闲鱼基于Flutter的移动端跨平台应用实践

这两种方案都是可选的,方案一就是把 Flutter 直接变成多例,每个 Flutter 页面重新启动一个 Flutter 的容器,每个 Flutter 页面就像通常使用 WebView 一样,这个方便我们做了实测,发现它的启动速度有影响,能感觉到一些卡顿,另外,还有一个问题,当我想在两个页面之间去复用数据的时候,那两个引擎之间是完全隔离的,最后数据不好复用。 这个方案的好处是很简单,如果喜欢隔离性,也可以变成优点。

第二种方案,就是做浅层的单例剥离,尽量多的遵守 Flutter 的标准运行方式,以最小的影响把单例剥离出来,Wrap 成多例。

这种方案是在 Flutter View 这一层剥离,关于 Flutter View 的概念看一下源码很容易理解。

这种解决方案的好处是可以实现多页面复用,因为不用每次都取一个新的实例,加载速度会更快,因为对闲鱼来讲,我们追求的就是性能,最后我们的选择就是方案二。

这个是具体的实现方式:

图10:闲鱼基于Flutter的移动端跨平台应用实践

把下面的 View 复用,在多个 Activity 之间移动,切换到下一个页面的时候,把这个可复用的 View 从前一个 Activity 移走,放到下一个 Activity,这是它的主要的思路。

在这个思路下也会遇到一些需要解决的问题:

  • 两个页面转场动画由于 View 在 Activity 间移动,会有一个短暂的白色闪屏,体验不好,解决闪屏的办法,就是做一个截图,从 A 页面到 B 页面的时候,对 A 页面做个截图,同时把 Flutter 自带栈的转场动画禁止掉,有这个截图,转场时就不会有闪屏的感觉了。
  • 考虑对统一 OpenUL 支持,把 Flutter 和 Native 的 URL 统一。
  • 由于 Flutter 容器内部有个栈管理,对这个栈需要与 Native 做同步的跟随。

到此,混合栈的方案就简单介绍完了。

基于 Texture 的自定义视频播放器

接下来,如果 Flutter 页面中想复用已有的 Native 组件,怎么办?

一种情况是视频播放器,Native 中我们做过很多优化的播放器,希望能复用到 Flutter 页面中。

首先,还是先看原理:

图11:闲鱼基于Flutter的移动端跨平台应用实践Flutter 内部的渲染,与通常的做法一样,有 layer。其中一种 Layer 叫 Texture Layer,可以把任何其他地方计算出来的纹理直接贴到 Flutter 的 Texture Layer 上。不管是视频,还是图片,如果有需要,都可以用 Texture Layer。

图12:闲鱼基于Flutter的移动端跨平台应用实践

在这个实现的方式中,Flutter 侧负责展示这个播放器 UI,接收对播放器做控制交互,而 Native 侧负责视频的渲染,通过 TextureLayer 展示到 Flutter 侧。而控制协议,通过 Flutter 特有的 MethodChannel 来控制。

除了视频,还有没有其他类型的 Native 组件能复用到 Flutter 中?像下图这样,把 Native 控件放在 View/Window 中与 Flutter 混合,是可以的。但截止演讲时,Flutter 还无法做到在 Flutter 中挖个小天窗嵌入 Native 组件。不过这个方式 Google Flutter 团队已经在做尝试,未来可能做有办法支持,大家可以关注。

图13:闲鱼基于Flutter的移动端跨平台应用实践

Flutter 通用问题实践

接下来,介绍一下 Flutter 商品详情页的页面的开发框架。

页面框架

图14:闲鱼基于Flutter的移动端跨平台应用实践

右边边绿色的这一部分,就是整个页面的结构,整个详情页面是一个大列表,由商品的描述、图片,评论,个性化推荐等组成。这里简单概括几个特点:

  • 通过 Server 端返回的数据驱动 UI 界面,可以一定程度上获得页面内容的动态能力。Flutter 本身不支持动态更新,无法像 JS 那样,所以这种设计方式可以一定程度上弥补这方面的短板。
  • Widget 树结点间(或者说页面的不同组件间)的数据如何共享?这里大家知道 InheritedWidget 这个类就好了,这是解决数据共享的很有用的类。
  • 如果页面再复杂些,有很多交互,希望将视频、交互、数据等分离怎么办?也可以考虑引入 Redux 框架。

统一协议

图15:闲鱼基于Flutter的移动端跨平台应用实践

Flutter 不支持 Dart 的反射(mirror),所以在开发 Flutter 页面时,解析服务端返回的数据,生成 Flutter 对象时,可能会很不习惯,需要有较多的硬编码。 Flutter 不支持反射,请大家理解,这样可以获得 tree shaking 能力,减少 Flutter 包的大小。

既然不支持反射,怎么去解决刚才说的数据转换问题?我们实现了一个统一协议层,把 Serve 端和客户端的请求接口和数据模型,都通过协议统一生成代码,避免了手工编码。

图片缓存方案

图16:闲鱼基于Flutter的移动端跨平台应用实践

闲鱼的页面中有大量图片,但 Flutter 默认的图片缓存策略比较简单,截止演讲时,如上图所示,默认图片缓存策略是按照图片数量,以 1000 为上限,LRU 的方式置换。当大图片较多时,这会占用过多的内存,容易造成 Crash 或 Abort。

图17:闲鱼基于Flutter的移动端跨平台应用实践

在我们只有详情页一种页面时,解决这个问题可以用简单粗暴的方式,首先把 1000 这个数量调小。一种修改方式如图所示,通过 WidgetsFlutterBinding 来修改(WidgetsFlutterBinding 是 Flutter 中很重要的一个机制,有兴趣可以深入了解)。

此外,还要注意图片尺寸自适应剪裁,支持 WebP 等,这些对节省图片内存和网络流量都很关键。

第二种解决方案,是官方正在做的优化,按照整个空间的大小来做缓存策略,具体可以关注图中的链接。

第三种方案,更加完善,加一层持久层的缓存,以实际的经验来看,闲鱼的场景下,持久层缓存时,通常可以提高缓存命中率 10% 到 30% 。

上线效果

线上 Crash 率

图18:闲鱼基于Flutter的移动端跨平台应用实践

大家可能会关心 Flutter 在生产环境的稳定性,兼容性等表现。闲鱼使用 Flutter 的前期阶段,这方面确实有很大的问题。前期在真实环境中发现了很多问题,第一次灰度测试时 Crash 率有百分之一的量级,主要的 Crash 问题包括内存、GPU、icu data、视频播放、截图接口、armv7、字体缺失等。

我们和 Google 团队一起,通过几个版本的灰度迭代,用了一个半月的时间,把问题逐步解决了,目前 Crash 率收敛稳定,达到万分之一的量级,已经达到了生产标准。

Flutter 与 Native 详情页性能对比

我们对 Flutter 与 Native 的详情页做了简单的性能对比,并不严谨,仅供参考。

测试场景:进入宝贝详情页后快速浏览到页面底部,从猜你喜欢进入第二个宝贝,重复进行访问 10 个不同宝贝详情。对比 Native 版详情页和 Flutter 版详情页。

测试机型,以低端机型为主(高端机型区分不明显):

Android 4.x, 5.x…

iPhone 5c, 6s…

安卓的对比:

图19:闲鱼基于Flutter的移动端跨平台应用实践

下面两行是体现流畅度的,LPS 或者 MS 是腾讯提出的一种流畅度的表达方式,在流畅度是 OK 的,比 Native 详情页做的好,在技术的指标上也还不错。

iOS 的结果:

图20:闲鱼基于Flutter的移动端跨平台应用实践

iOS 上,也是 Flutter 会更流畅一些,测下来,发现在 GPU 的使用率上,Flutter 会更高一些,Flutter 在这上有更进一步的优化空间。

说到这里,可能大家也会疑惑,这个对比结果,是不是因为以前 Native 写的详情页太复杂了? 确实有这种可能。但主要分享的是,两种页面是相同团队成员开发的,并且没有针对 Flutter 做专门的性能优化,这个性能测试可以确定的结论是,使用 Flutter 还是比较容易就能开发出与 Native 性能相近的页面。

最后,说一下大家可能会关于的成本问题。对于混合开发,初期接入成本是有的。如果是全新的 Flutter 独立应用,接入成本会很低。首次接入完成后,后面开始会顺利很多,可以享受跨端统一编程,一套代码带来的效率快感。另外,关于学习成本,还好,因为 Dart 语言跟 Java 很像,跟 JS 也很像,另外 Flutter 的 UI 框架遵循响应式,声明式设计原则,个人感觉,较容易上手。谢谢大家,由于水平有限,可能会有错误,请大家指正。篇幅有限本文无法对每个细节深入探讨,关于细节的深入分享,欢迎大家关注“闲鱼技术”的公众号。

作者简介

王树彬,阿里巴巴闲鱼无线技术专家,毕业于浙江大学,2009 年加入阿里巴巴,现任阿里巴巴闲鱼架构负责人,负责闲鱼从端到云的整体架构升级。有十余年互联网研发经验。曾负责移动端 LBS 技术,是淘宝位置归一、地理围栏等技术的开拓者,为个性化、O2O 等业务提供基础能力。也曾负责淘宝的商家系统,建立商家十亿级大数据下的实时在线查询、挖掘服务。

本文文字及图片出自 InfoQ

你也许感兴趣的:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注