[闲鱼技术] Release Flutter的最后一公里

avatar
@阿里巴巴集团

作者:闲鱼技术-凯航

Flutter是一个使用Dart语言开发的跨平台移动UI框架,通过自建绘制引擎,能高性能、高保真地进行Android和IOS开发。在业界还未出现过Base Flutter的大型商业应用实战验证的情况下,闲鱼技术团队在最复杂且重要的商品详情页作了相关的技术实践并取得良好的结果。现尝试通过本文向有兴趣进行类似实践的开发者或团队分享过程中的思考/实践过程。

Flutter特色

面对一系列移动开发技术:IOS、Android、Weex,RN, Kotlin,H5... Flutter究竟特色何在?

image.png

开发语言选择

了解过Flutter的都知道,它采用Dart语言进行开发,而并非Java,Javascript这类热门语言,这是Flutter团队对当前热门的10多种语言慎重评估后的选择。因为Dart囊括了多数编程语言的优点,它更符合Flutter构建界面的方式。

image.png
Dart更多优势可查看为什么Flutter会选择Dart

Flutter vs ReactNative框架对比

ReactNative Flutter
image.png
image.png

ReactNative

  • 采用Javascript开发,需学React,成本高
  • 需要JavaScript桥接器,实现JS到Native转化,性能耗损
  • 访问原生UI,频繁操作易出性能问题
  • 支持线上动态性,可有效避免频繁更新版本

Flutter

  • 采用Dart开发,可直接编译成Native代码(易学)
  • 自带UI组件和渲染器,仅依赖系统提供的Canvas(无桥接耗损)
  • 暂不支持线上动态性

Flutter更多特色可以链接为什么说Flutter是革命性的?

每个框架都是为解决特定问题而产生的,不存在最好的框架,只有最适合你团队的框架。闲鱼是个业务快速发展的App,为更多业务尝试和探索,它采用现有流行的框架,能支持线上动态化需求。但出于个性化交互以及流畅性体验(首页、商品详情、发布闲置等),主链路依旧只采用原生开发。为兼顾跨端开发及高性能需求,闲鱼经过充分调用,最终选择了Flutter。为验证Flutter的性能,闲鱼挑选重要且复杂的主链路业务(商品详情)作为首个Flutter页面实践点,以这种方式来快速暴露并解决Flutter相关问题。

闲鱼Flutter突破点

Flutter与Native混合编程方案

随着Flutter版本的不断迭代,稳定性和质量逐渐完善,市场上纯Flutter开发的App也不断涌现。闲鱼对Flutter采取“由点到面,逐一替换” 的策略,先将商品详情迁移到Flutter页面,后续逐步扩展到其他功能模块,但这样就不可避免涉及到Flutter与Native页面混合调用的场景(如下图):

image.png
对纯Flutter工程而言,它主要通过FlutterView中Navigator来管理页面间的跳转;对纯Native工程而言(如:android), 它主要通过系统中ActivityStackSupervisor类对页面切换进行管理,这样当Flutter与Native混合时,就面临浏览一组页面,两套页面管理方式(Flutter管理Flutter页面,Native管理Native页面), 若执行回退操作时,很难保证能回退到期望页面。另外,Flutter工程中界面都是一个继承自SurfaceView的FlutterView(说白了Flutter界面就一个View,不是Activity也不是Fragment),Flutter和Native组件间相互调用也不可避免。

Flutter Native(Android)
image.png
image.png

因此要混合调用就会涉及两个问题:

  • 混合栈管理
  • 组件间调用

混合栈管理

Flutter出现的目的旨在统一Android/IOS两端编程,因此完全基于Flutter开发的App,只需提供一个包含FlutterView的页面,后续页面增加/删除/跳转均在FlutterView的Navigator中进行管理。但现在闲鱼只是将部分模块修改成Flutter开发,我们不可能为统一页面栈管理而将其他所有页面用Flutter重做一次,权衡成本与风险,亟需统一管理Native页面和Flutter页面跳转交互的混合栈。为此,闲鱼提出了4种解决方案(如下图):

image.png
由于IOS有对外系统接口可以方便管理页面栈,因此主动记录页面栈信息就可以解决混合栈管理(方案1),但Android任务栈由系统管理,且融合复杂的Activity回收机制,为降低android学习成本,google并没有对外提供页面栈管理API,方案1方式行不通。为统一android/IOS混合栈管理方式,从FlutterView上着手更为可靠,以此为引,闲鱼提出两种方式:

  1. 每启动一个Activity就启动一个新的FlutterView(方案4);
  2. 抽取单一FlutterView或FlutterNativeView,后续每启动一个Activity都对FlutterView或FlutterNativeView进行复用(方案2或方案3);

考虑到每启动一个页面都新创建一套新的Flutter渲染机制,开销过重,目前闲鱼Flutter实践采用方案2,相比而言,该方案性能相对稳定且易操作,下面就是否复用FlutterView进行对比,主要观测Java内存和Native内存增加情况:

数据表明:不复用FlutterView时平均打开一个页面(空页面),Java内存增长0.02M,Native内存增长0.73M;复用FlutterView时平均打开一个页面(空页面),Java内存增长0.019M,Native内存增长0.65M,因此,复用FlutterView在内存使用上是有优势的,如需更深了解可查看Android Flutter内存初探。此外,相关方案的详细表述在 How to manage page stack in flutter/native hybrid App 以及Support multiple shells in a single process均有阐述。

组件间调用

image.png
组件间采用比较常见场景就是黑屏问题,出现该问题多数为Layer冲突。从上图(右)可知UI渲染原理:GPU的VSync信号同步到UI线程,UI线程使用Dart构建抽象的视图结构(Layer Tree),接着在GPU线程进行图层合成,且视图数据提供给Skia引擎进行渲染生成GPU数据,最终通过OpenGL或Vulkan提供给GPU,由此可以看出Flutter并不关心显示器、视频控制器以及GPU具体工作细节,它只关心发出的VSync信号,以求尽可能快地在两个VSync信号之间计算并合成视图数据并提供给GPU。Flutter开发者都知道Flutter界面渲染时,使用的是FlutterViewController.view的Layer,倘若Flutter页面跳转到Native做界面渲染相关逻辑时, Native也使用同一个Layer,这将会导致Flutter在release模式无法渲染,LayerTree合成失败即Layer冲突。不过这问题解决也很简单,只需要采用Window或独立View方式唤起Native即可。

解决了Flutter与Native混合编程所面临的问题后,接下来要处理的就是混编工程问题,出现该问题的原因还是我们的项目不是完全的Flutter工程(即:android /ios + Flutter)所致。混合工程项目结构以及Flutter产物如下图:

项目结构

image.png

Flutter产物

image.png

其实对一般Flutter工程而言,采用AndroidStudio编译Flutter与编译Native工程方式一样,当将其部署到server端采用mtl编译时,server缺少Flutter编译环境,因而导致Flutter工程无法编译。解决此问题可以采取两种方式:

  1. 在每个server端部署Flutter编译环境
  2. Native工程远程依赖Flutter编译产物

对1,对各server端都去部署Flutter环境有点不切实际(若server就那么几台也可以);对2,闲鱼的做法是将Flutter工程编译出的中间产物以AAR形式导出并上传至maven库,最后Native工程以依赖包形式将AAR打入最终apk中,这样处理后解耦了Native团队对Flutter团队的依赖。当然,具体实践过程中肯定没有这么简单,我们在编译过程中对Pod/Gradle编译脚本、engine以及flutter_tools等均有所优化,对后续集团推广Flutter奠定了基础。

阿里Flutter生态适配

将Flutter应用于闲鱼,不可避免需要使用集团提供的基础组件库,但这些组件库都是Native,考虑到为后续Flutter在集团推广,打造阿里Flutter生态圈,闲鱼团队对集团内部基础组件库做了适配支撑,后续可建立私有仓库,直接Git引用。

生态适配原理及性能

image.png
上图(左)概述了Flutter平台通道,使用MethodChannel在Client(UI)和主机(平台)之间传递消息,消息和响应异步传递以确保用户界面保持正常响应。对UI,Flutter的MethodChannel类可以发送与方法调用相对应的消息;对平台,Android端MethodChannel类和IOS端FlutterMethodChannel类可以接收方法调用并发送结果,同时方法调用还可以逆向发送,即以平台作为实现Dart方法的Client。值得注意的是Flutter Plugin开发相关原理也是如此。上图(右)还对MethodChannel吞吐量性能进行了简略表述。

多媒体解决方案

在以内容为王的时代,多媒体技术备受关注,性能的好坏直接影响用户体验,但Flutter多媒体默认功能存在以下缺陷:

  • 功能单一,如:播放器缺少滤镜,与Native淘宝播放器存在一定差距
  • 兼容性欠佳

为改善体验,优化性能,闲鱼对Flutter播放器以及图片性能作出如下改进:

Texture对接自定义视频播放器

image.png

具体方案:

image.png

图片性能优化

有过移动开发经验的都知道,图片展示是OOM出现的高频场景,而Flutter默认采用基于LRU算法的图片缓存策略,且图片缓存MaxSize=1000,占用内存较高,为此闲鱼采用以下两种策略对图片性能进行优化

image.png

Flutter 上线效果 & 性能对比 & 成熟度

解决了Flutter存在的问题,要的就是产品能够以一种性能稳定、交互流畅、界面美观的姿态呈现在用户面前,下面就以闲鱼宝贝详情线上Flutter版本为引,对Flutter应用页面展示、性能以及成熟度进行阐述。

闲鱼宝贝详情Flutter应用线上效果

image.png

Native性能对比

测试环境

  • 测试机型:iphone6
  • IOS版本:11.3
  • 闲鱼版本: 6.1.3
  • 测试方法: 在Flutter版本和Native版本各自宝贝详情页面执行相同操作:即从我发布的页面进行10个不同详情页面

注: 下图仅对Flutter和Native性能进行了粗略对比

image.png

Flutter成熟度

image.png
上图为crash收敛曲线图,可容易看出经过几个版本迭代,crash率基本趋于稳定,体感与Native可以媲美。

延展讨论

目前Flutter尚处于Preview阶段,没有经过大规模实践验证,框架成熟度及稳定性仍有待完善。上文仅仅是将闲鱼团队在实践Flutter开发时碰到部分问题及解决方案进行了简略阐述,一个产品从开发到上线所面临的问题,肯定远不及这些。随着Flutter覆盖场景的增加,难题也会不断涌现,健全有效的性能及稳定性监控体系不可或缺。为建设基于Flutter全新的一体化研发体系,提高开发效率,对动态化需求、规范Dart编码、统一中间件桥接机制、快速发版能力及完备的自动化测试建设等一系列问题亟需解决,倘若您对此感兴趣,欢迎一起交流学习~

简历投递:guicai.gyx@alibaba-inc.com