闲鱼基于Flutter的移动端跨平台应用实践
作者|王树彬编辑|覃云本文来自阿里巴巴闲鱼无线技术专家王树彬在今年 GMTC 大会上的演讲。闲鱼为什么使用 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”的设计效果。
下面截图是目前闲鱼已经上线的商品详情页面:
商品详情页包含混合栈、视频、动画、原生组件、多图、留言盖楼等功能,页面较复杂,有代表性,也是闲鱼最重要的页面之一。选择商品详情页做为第一个 Flutter 页面,是闲鱼能成功快速使用起 Flutter 的重要因素。
接下来介绍一下,闲鱼的实践过程和总结。
Flutter 与 Native 混合开发实践 Flutter Hybrid 工程实践(研发时)
我们把 Flutter 和闲鱼现有的 APP 做渐进式的整合,App 中会同时有 Native、Flutter 和 H5 页面。现有的 Flutter Demo 和应用,都是独立的 Flutter 应用,而当把它和 Native 混合的时候,会碰到很多的困难。
首先是研发时的问题,怎么让 Flutter 在现有的 Native 工程中开发起来。这个要从这张图说起:
闲鱼 Flutter 工程结构如图,三个蓝色背景的目录分别是安卓工程、iOS 工程和 main.dart 入口。编译产物中以 iOS 为例,APP Framework 是 Flutter 应用页面代码,Flutter Framework 是 Flutter 引擎。
这个过程,需要重点考虑几个问题:如何基于现有工程搭建混合工程?如何支持过渡期的 Flutter 开发及纯 Native 开发的双开发模式?如何让 Flutter 与现有持续集成、构建工具集成?
首先,现有的 Native 工程并不符合 Flutter 默认的规范,两者不能完全匹配,需要修改打包脚本,甚至修改 Flutter 的打包 Tool 来解决。另外,我们通过 Submodule 将现有⼯程引入到 Flutter 父工程中。
纯 Native 开发同学,不需要引入 Flutter 工程,直接在 iOS 或 Android 工程下开发,Flutter 以产物的方式集成到 Native 中运行,Flutter 的开发同学引入 Submodule。
上图是工程上的修改点。绿色虚线部分是 Flutter 默认的结构,红色虚线是闲鱼在 Flutter 基础上做的定制。Flutter 的构建工具 gen_snapshot,会把业务代码,Flutter 框架、引擎编译成中间产物,以 so 或 Framework 的方式变成 Native 的一部分。
几个主要的改动点:
第一,构建私有的仓库,用来管理阿里私有包,如 CDN、无线网关等中间件适配 Package。
第二,构建工具和引擎的优化。
第三,跟现有的构建工具打通,混合调试等。
Flutter Hybrid 栈管理
除了上述的研发时问题,接下来就是让它跑起来,解决运行时问题。其中最重要的是实现混合栈。
混合栈的定义
在混合工程中,Native 页面,Flutter 页面之间会以多种可能的顺序混合入栈,出栈。要怎么去做?先看一下 Flutter 内部栈的管理默认下是怎么做的:
整个 Flutter 运行在一个单例的 Activity 容器里(用安卓举例),Flutter 内部的所有页面都在这个容器中管理。 对安卓来说,怎样把这样容器里面的栈与 Native 栈混合起来,直接的一个想法就要把栈自己托管起来,把这个容器在 Android 的栈中来回移动。但 Android 里想这样操作非常难。
所以解决这个事情,就主要有两个问题要考虑,首先就是混合栈要在哪里管理?是在 Hybrid 栈管理,还是在 Flutter 管理,第二个就是关于实例剥离的问题,既然移动单例很复杂,那就把单例剥离出来,在上面 Wrap 出多个实例,这样就方便管理了。 下面是两个对比方案。
这两种方案都是可选的,方案一就是把 Flutter 直接变成多例,每个 Flutter 页面重新启动一个 Flutter 的容器,每个 Flutter 页面就像通常使用 WebView 一样,这个方便我们做了实测,发现它的启动速度有影响,能感觉到一些卡顿,另外,还有一个问题,当我想在两个页面之间去复用数据的时候,那两个引擎之间是完全隔离的,最后数据不好复用。 这个方案的好处是很简单,如果喜欢隔离性,也可以变成优点。
第二种方案,就是做浅层的单例剥离,尽量多的遵守 Flutter 的标准运行方式,以最小的影响把单例剥离出来,Wrap 成多例。
这种方案是在 Flutter View 这一层剥离,关于 Flutter View 的概念看一下源码很容易理解。
这种解决方案的好处是可以实现多页面复用,因为不用每次都取一个新的实例,加载速度会更快,因为对闲鱼来讲,我们追求的就是性能,最后我们的选择就是方案二。
这个是具体的实现方式:
把下面的 View 复用,在多个 Activity 之间移动,切换到下一个页面的时候,把这个可复用的 View 从前一个 Activity 移走,放到下一个 Activity,这是它的主要的思路。
在这个思路下也会遇到一些需要解决的问题:
两个页面转场动画由于 View 在 Activity 间移动,会有一个短暂的白色闪屏,体验不好,解决闪屏的办法,就是做一个截图,从 A 页面到 B 页面的时候,对 A 页面做个截图,同时把 Flutter 自带栈的转场动画禁止掉,有这个截图,转场时就不会有闪屏的感觉了。
考虑对统一 OpenUL 支持,把 Flutter 和 Native 的 URL 统一。
由于 Flutter 容器内部有个栈管理,对这个栈需要与 Native 做同步的跟随。
到此,混合栈的方案就简单介绍完了。
基于 Texture 的自定义视频播放器
接下来,如果 Flutter 页面中想复用已有的 Native 组件,怎么办?
一种情况是视频播放器,Native 中我们做过很多优化的播放器,希望能复用到 Flutter 页面中。
首先,还是先看原理:
Flutter 内部的渲染,与通常的做法一样,有 layer。其中一种 Layer 叫 Texture Layer,可以把任何其他地方计算出来的纹理直接贴到 Flutter 的 Texture Layer 上。不管是视频,还是图片,如果有需要,都可以用 Texture Layer。
在这个实现的方式中,Flutter 侧负责展示这个播放器 UI,接收对播放器做控制交互,而 Native 侧负责视频的渲染,通过 TextureLayer 展示到 Flutter 侧。而控制协议,通过 Flutter 特有的 MethodChannel 来控制。
除了视频,还有没有其他类型的 Native 组件能复用到 Flutter 中?像下图这样,把 Native 控件放在 View/Window 中与 Flutter 混合,是可以的。但截止演讲时,Flutter 还无法做到在 Flutter 中挖个小天窗嵌入 Native 组件。不过这个方式 Google Flutter 团队已经在做尝试,未来可能做有办法支持,大家可以关注。
Flutter 通用问题实践
接下来,介绍一下 Flutter 商品详情页的页面的开发框架。
页面框架
右边边绿色的这一部分,就是整个页面的结构,整个详情页面是一个大列表,由商品的描述、图片,评论,个性化推荐等组成。这里简单概括几个特点:
通过 Server 端返回的数据驱动 UI 界面,可以一定程度上获得页面内容的动态能力。Flutter 本身不支持动态更新,无法像 JS 那样,所以这种设计方式可以一定程度上弥补这方面的短板。
Widget 树结点间(或者说页面的不同组件间)的数据如何共享?这里大家知道 InheritedWidget 这个类就好了,这是解决数据共享的很有用的类。
如果页面再复杂些,有很多交互,希望将视频、交互、数据等分离怎么办?也可以考虑引入 Redux 框架。
统一协议
Flutter 不支持 Dart 的反射(mirror),所以在开发 Flutter 页面时,解析服务端返回的数据,生成 Flutter 对象时,可能会很不习惯,需要有较多的硬编码。 Flutter 不支持反射,请大家理解,这样可以获得 tree shaking 能力,减少 Flutter 包的大小。
既然不支持反射,怎么去解决刚才说的数据转换问题?我们实现了一个统一协议层,把 Serve 端和客户端的请求接口和数据模型,都通过协议统一生成代码,避免了手工编码。
图片缓存方案
闲鱼的页面中有大量图片,但 Flutter 默认的图片缓存策略比较简单,截止演讲时,如上图所示,默认图片缓存策略是按照图片数量,以 1000 为上限,LRU 的方式置换。当大图片较多时,这会占用过多的内存,容易造成 Crash 或 Abort。
在我们只有详情页一种页面时,解决这个问题可以用简单粗暴的方式,首先把 1000 这个数量调小。一种修改方式如图所示,通过 WidgetsFlutterBinding 来修改(WidgetsFlutterBinding 是 Flutter 中很重要的一个机制,有兴趣可以深入了解)。
此外,还要注意图片尺寸自适应剪裁,支持 WebP 等,这些对节省图片内存和网络流量都很关键。
第二种解决方案,是官方正在做的优化,按照整个空间的大小来做缓存策略,具体可以关注图中的链接。
第三种方案,更加完善,加一层持久层的缓存,以实际的经验来看,闲鱼的场景下,持久层缓存时,通常可以提高缓存命中率 10% 到 30% 。
上线效果 线上 Crash 率
大家可能会关心 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...
安卓的对比:
下面两行是体现流畅度的,LPS 或者 MS 是腾讯提出的一种流畅度的表达方式,在流畅度是 OK 的,比 Native 详情页做的好,在技术的指标上也还不错。
iOS 的结果:
iOS 上,也是 Flutter 会更流畅一些,测下来,发现在 GPU 的使用率上,Flutter 会更高一些,Flutter 在这上有更进一步的优化空间。
说到这里,可能大家也会疑惑,这个对比结果,是不是因为以前 Native 写的详情页太复杂了? 确实有这种可能。但主要分享的是,两种页面是相同团队成员开发的,并且没有针对 Flutter 做专门的性能优化,这个性能测试可以确定的结论是,使用 Flutter 还是比较容易就能开发出与 Native 性能相近的页面。
最后,说一下大家可能会关于的成本问题。对于混合开发,初期接入成本是有的。如果是全新的 Flutter 独立应用,接入成本会很低。首次接入完成后,后面开始会顺利很多,可以享受跨端统一编程,一套代码带来的效率快感。另外,关于学习成本,还好,因为 Dart 语言跟 Java 很像,跟 JS 也很像,另外 Flutter 的 UI 框架遵循响应式,声明式设计原则,个人感觉,较容易上手。谢谢大家,由于水平有限,可能会有错误,请大家指正。篇幅有限本文无法对每个细节深入探讨,关于细节的深入分享,欢迎大家关注“闲鱼技术”的公众号。
作者简介
王树彬,阿里巴巴闲鱼无线技术专家,毕业于浙江大学,2009 年加入阿里巴巴,现任阿里巴巴闲鱼架构负责人,负责闲鱼从端到云的整体架构升级。有十余年互联网研发经验。曾负责移动端 LBS 技术,是淘宝位置归一、地理围栏等技术的开拓者,为个性化、O2O 等业务提供基础能力。也曾负责淘宝的商家系统,建立商家十亿级大数据下的实时在线查询、挖掘服务。
活动推荐
前端技术发展日新月异,可还是会有面对大型应用的大家各自为战的困扰。想知道前端最新趋势,可以关注 QCon 全球软件开发大会,从实践中积累的前端架构经验,典型的前端框架应用经验,新型框架与设计思路三个方面探索前端研发之路。大会9 折报名中,立减 680 元。有任何问题欢迎咨询票务经理 Hanna,电话:010-84782011,微信:qcon-0410。