Flutter 在饿了么的应用与沉淀

Flutter作为当前最火的跨平台研发方案,它到底好在哪里?饿了么从2018年下半年开始接触Flutter,并在多个App大量落地Flutter业务。饿了么对Flutter的期待是保质提效,赋能业务。阿里巴巴新零售淘系技术AliFlutter系列第八场直播中邀请了蜂鸟大前端资深iOS工程师李永光为大家介绍饿了么为了”保质提效,赋能业务”,选择Flutter作为跨平台研发方案的缘由,Flutter在饿了么应用与落地情况,饿了么在Flutter应用过程中的基础建设和沉淀。相信能给大家带来更多尝试使用Flutter、以及把Flutter实际用于业务开发的信心和决心。

演讲嘉宾简介:李永光,花名雍光,蜂鸟大前端资深iOS工程师。4年深耕移动端,饿了么最早的一批Flutter玩家,重点参与了Flutter在蜂鸟团队的业务开发落地、工程架构演进。

以下内容根据演讲视频以及PPT整理而成。
观看回放http://mudu.tv/watch/5817421

本次分享主要围绕以下四个方面:

            一、背景介绍  
            二、Flutter 在饿了么的应⽤  
            三、基础建设与沉淀  
            四、展望与规划  

一、为什么选择使⽤ Flutter

Flutter的三大特点及框架

打开Flutter官网,映入眼帘的是Flutter的三大特点。首先是快速开发,毫秒级的绘制,亚秒级的热加载,丰富的Widget,可以快速开发出用户界面。二是富有表现力的UI,丰富的动效接口,流畅的滑动,支持iOS和Android两种风格的Widget,可以组合出非常漂亮且灵活的界面。三是可媲美Native的运行性能,AOT模式下,Flutter代码可以被编译成ARM机器码。下图是Flutter架构图,最底层是Flutter应用的各个平台,包括iOS、Android、Windows、MacOS、Ubuntu等。各个平台分别实现Embedder平台相关,如提供绘制的画布,在Android和iOS上就是OpenGL Context, 还包含了线程设置和事件循环。再上一层是Flutter Engine层,使用了C++,包含DartVM,Skia 2D渲染等通用能力,还包含文字绘制等关键能力。最上一层是Flutter Framework层, 使用了Dart, 包含了Material和Cupertino两种风格的Widget,以及一部分渲染逻辑,当然还包含框架与引擎之间的通信接口。Flutter的渲染基本与平台无关,平台只是提供了画布,这为Flutter的UI一致性和运行一致性提供了坚实的基础。

Flutter原理

下图右侧是Flutter UI渲染的三棵树,分别包含描述节点,虚拟节点和真实节点。Widget树在运行期会生成Element树和RenderObject树,Widget树在更新时会触发Element树的比对和更新,以触发RenderObject树的最小更新。Flutter与RN等方案最大的不同就在于Flutter的RenderObject是自己实现的,而RN是使用Native控件。

Flutter 在饿了么的应用与沉淀

客户端研发方案对比

下图给出了五种不同研发方案在四个维度上的对比,包括Native、Flutter、RN、Weex和H5。1)在性能方面,H5最差,Native最优。Flutter相比于RN,在AOT编译模式下,由于没有JS虚拟机,相对运行性能更高。而且,Flutter的渲染是直接对接底层的,RN还需要操作Native控件,有着非常高的通信成本。Flutter的页面加载速度和流畅度都比RN更优,在测试中也是符合实际预期的。

在动态性方面,H5可以实时发布,所以动态性最好。RN有JS虚拟机,可以一定程度上进行代码的动态下方和执行。在AOT编译下,Flutter的动态性可以认为是与Native差不多的,都非常弱。如果有此类需求,需要借助其它方式,如DSL等。

跨端一致性可以分为UI一致性和运行一致性。H5借助于浏览器规范,其UI一致性是非常好的,运行一致性也不错。Native由于开发栈的不同,其RenderObject,API和实现都有很大的不同,因此跨端一致性非常差。Flutter实现了渲染层,在Android和iOS上RenderObject的实现是一致的。正是因为有了渲染层,平台相关性非常低,所以可以像H5一样做到一处编写,处处运行。

开发体验方面,H5开发预览调试都非常方便,Flutter的各个插件使用起来也比较方便,HotReload使得开发,调试和运行都可以节省不少时间。

总结起来,Flutter在动态性上有所诟病以外,性能可以与Native媲美,极强的跨端一致性,还可以提供非常好的开发体验。

Flutter 在饿了么的应用与沉淀

饿了么的选择

饿了么对研发方案的期望是用最少的人,搬最快的砖,用户无感知!简而言之是研发效率高,用户体验好。最后,饿了么选择了Flutter作为跨平台研发方案。Flutter具备完备好用的工具,如Pub,IDE插件,HotReload,优秀的跨端一致性,富有表现力的UI,这些特点可以为开发调试和验证工作节省很多时间,从而大大提升研发效率。跨端一致性,丰富的UI,以及高性能使得Flutter开发的页面与Native开发的页面体验无差别,而且Android与iOS可以保持高度的一致性。另外,Flutter采用的自有渲染引擎在未来可扩展的余地很多, Flutter是Google出品的,Flutter现在的社区也是非常火热的,阿里巴巴,腾讯,头条等企业的很多App都使用了Flutter,可以说,Flutter有着非常广阔的应用与技术前景。在动态性方面,目前大家已经有了很多解决方案,如DSL等。这点上还是以自己的需求出发,稍作补足即可。

Flutter 在饿了么的应用与沉淀

Flutter在饿了么的应⽤

从2018年下半年到现在,饿了么很多App上都已经使用了Flutter。其中至冠配送大概80%的页面都使用了Flutter。

Flutter 在饿了么的应用与沉淀

下图展示了饿了么已经上线的Flutter页面,涵盖了大部分常见的场景,包括残疾骑士验证页面、单量提升页面、优惠券页面、运单签收页面等。他们的页面加载速度和滑动流畅度都基本与Native页面相当。

Flutter 在饿了么的应用与沉淀

混合开发(旧)

Flutter代码主要以Module的形式关联到主工程,自然涉及到混合栈管理问题。Flutter页面与Native页面的切换主要有以下三种情况,Native切换Flutter、Flutter切换Flutter、Flutter切换Native。从2018年到2019年间,饿了么也实现了自己的混合栈管理方案,Android和iOS的理念相似,这里以iOS为例,关键在于Native侧公用多个FlutterVC的FlutterView,解决FlutterVC不释放问题。饿了么在Flutter侧做了一个LPDFRouter,记录所有与Flutter页面有关的URL。当从Native切换Flutter时,先打开Flutter Container VC,Flutter侧的Navigator会push URL。当连续打开Flutter页面时,Native容器不动,Flutter的Navigator直接push。当Flutter切换Native时,Native容器返回按键绑定Dart方法,当上一个页面是Flutter时,使用Native直接pop即可,如果上一个页面是Native页面,除了把当前URL pop掉,还需要将容器pop掉。下图的混合栈管理方案基本上满足了饿了么的需求,但是方案的一半在Native,另一半在Flutter中,不太适合Tab非常复杂的情况。
Flutter 在饿了么的应用与沉淀

混合开发(新 - boost)

后来饿了么使用了闲鱼开发的Flutter boost作为混合栈管理方案,无论从Native还是Flutter打开一个页面,都会先打开一个原生的容器,容器的生命周期ID会通过Channel传到Flutter侧,Flutter容器管理器会打开一个Flutter容器。Flutter容器管理器管理了多个Navigator,每个Navigator只有一个Flutter页面,一个Widget。Flutter容器与Native容器ID是一致的。最新的Flutter boost使用了多个Flutter View的组合,不再复用单个Flutter View+截图。可以发现Flutter boost对混合栈的管理是非常纯粹的,都在Native侧。在使用Flutter时,只需要关注Native容器的生命周期即可,适合多Tab的Flutter页面情况。

Flutter 在饿了么的应用与沉淀

研发/集成模式

Flutter工程与Native工程是如何组织的,下图中的虚线框代表需要有Flutter环境。第一种是至冠模式Teemo,饿了么最初就希望将至冠配送100% Flutter化。因此将Flutter工程与Native工程放在了同一个仓库里,Native工程通过ruby脚本关联到Flutter工程。Native工程编译时先编译Flutter工程,将产物引进来,因此工程的开发、调试、测试、打包等都需要Flutter环境。下图右侧是蜂鸟模式,蜂鸟业务非常复杂,饿了么考虑可能只有部分的同学会开发Flutter业务,因此将Flutter环境与其它业务环境隔离开来。饿了么做了一个Runner工程,同步了蜂鸟主工程的很多依赖,如登录、用户管理、安全等。在Runner工程中把Flutter业务开发完之后,通过本地和远端把Flutter产物抽取出来进行发布,如App Framework、Flutter Framework、以及对应的原生代码。发布之后,蜂鸟工程会版本tag的形式依赖Flutter的原生产物,那么只开发Native业务的同学不需要Flutter环境。不同的团队有不要的研发和集成模式,并不存在最佳实践,适合自己的才是最关键的。

Flutter 在饿了么的应用与沉淀

质量&效率结果

从2018年到现在,饿了么已经上线了很多Flutter页面,页面占比在不同团队都是不同的。Crash方面,稳定下来后在0.01%级别。在流畅度方面,使用Flutter工具或使用线上回调函数计算,都可以达到50帧以上。而最大的惊喜是在提高研发效率上,在过去一年多的时间里节省了100多开发人日。前期不是很熟练,1个人可以顶1.5个人,后期便熟练后,1个人可以顶1.8个人。

基础建设与沉淀

控件库

随着Flutter页面的开发越来越多,饿了么联合UED做了基础控件的规范和封装,包括按钮、TextFeild等。当基础的控件创建完后,后续的Flutter页面可以复用控件,进一步提高开发效率,同时使得不同页面间的一致性更好。

Flutter 在饿了么的应用与沉淀

基础插件

Flutter是一个UI的开发框架,因此也不能免于使用Native能力,如定位、持久化。饿了么在开发过程中也积累了很多基础的插件,如Crash上报、推送处理、社交分享,还桥接了Native网络库发出Flutter请求,使得性能与体验都保持一致。

Flutter 在饿了么的应用与沉淀

dna - 背景与目标

随着Channel越来越多,小到获取Native参数,大到获取Native执行功能,使得Channel使用越来越不便,其痛点主要有以下三点,一是双边硬编码。在Dart和Native两边针对Channel的名字,如方法名等参数做硬编码,一旦出错就会调用错误。二是只能单次调用,Flutter调Native时只能通过方法名匹配到一个代码块。三是创建成本高,不单要编写Channel代码,有时还需要创建Plugin。为了解决以上问题,饿了么也做了一些探索和实践,即dna Plugin。在设计和实现dna Plugin时也有几个对应的目标,首先是直接调用Native方法,不再使用Channel名或方法名等,同时Native侧不再使用硬编码。二是支持上下文调用,Dart可以直接调用Native代码。三是无需创建Channel和Plugin。

Flutter 在饿了么的应用与沉淀

dna - 使用

下图展示了dna快捷方法的使用,获取了Android,iOS对应的版本号,以及系统平台的字符串。可以发现,使用了NativeObject作为Native变量,NativeObject的生命周期与Native的生命周期是一致的。上一个方法的返回值可以作为下一个方法的参数,即上下文调用。原生调用上与Native很像,支持链式语法。返回值默认是最后一次context的的返回值。下图下层框里是假想的代码,dna执行Native方法时相当于执行了框中的方法。

Flutter 在饿了么的应用与沉淀

实际使用的dna代码相对更简单,下图展示的是拆箱解码的操作。iOS侧调用了一个类来获取实例,Android是直接调用了实例。iOS包含一个类一个方法,Android还需要一个dna method注解。

Flutter 在饿了么的应用与沉淀

dna - 原理

当上面的context中的Native Object不断的Invoke 方法时,会形成一个个的InvocationNode,InvocationNode定义了方法名,参数以及返回值。当context调用时,会转换成一个context JSON,传给Native。Native解析JSON,转换成对应的一个个Invocation,被陆续调用。每个Native Object都对应一个ID。一个Invocation里不但有调用值的ID,还调用了返回值的ID,他们都会放在一个map里,实现上下文关联。

Flutter 在饿了么的应用与沉淀

dna在调用到一个Java方法时,需要一个dna method注解,主要是因为Android的release中做了代码的混淆,无法通过类名和方法名定位。当一个方法加了Method注解时,可以扫描这个注解,生成代理类和代理方法。APT生成代理文件中包含扫描注解、生成的代理方法、存储的方法名和参数信息。dna调用时先通过运行时注解匹配到代理方法,然后通过owner调用到真正的对象方法。

Flutter 在饿了么的应用与沉淀

Channel VS. dna

dna不需要新建Plugin和Channel,而且由于dna是直接调用Native,所以所有硬编码都没有了,dna和Channel都不支持C函数,也不支持Native对象内存管理。目前dna传递JSON时还是使用Channel传递。很多情况下,只需要调用小部分Native代码,dna也无需创建Plugin及Channel,而且Native也不需要硬编码。在蜂鸟团队中,非常基础的功能是使用Channel做的,稍微涉及业务的功能都使用dna。

Flutter 在饿了么的应用与沉淀

其他实践

在饿了么自身的实践中,也做了其他的探索,如热修复,虽然还没有上线,但为Flutter boost提供了一些支持。

Flutter 在饿了么的应用与沉淀

参与共建 - 背景

AliFlutter的目标是共同打造基础设施,制定标准,复用技术。通过培育集团各个BU Flutter生态,沉淀业务与技术。顺便可以联合对外产生凝聚的影响力。

Flutter 在饿了么的应用与沉淀

共建 - Pub Server

在Pub Server中,通过get和publish可以下载一些库。AliFlutter为了Pub库的保密性,以及外部库的下载速度,也做了自己的Pub Server。与官方方案最大的不同是将Google Cloud存储换成了阿里云OSS存储,当查询外部的库时先看内部是否有缓存,没有才去下载外部的库。

Flutter 在饿了么的应用与沉淀

共建 - Pub Dev(前端检索)

Pub Dev是大家所使用的前端检索页面,可以查到已发布的Plugin或Dart库。既然有了内部的Pub Server,还需要做Pub Dev。Pub Server提供的检索功能非常有限。饿了么做了自己的元信息数据库,有一个定时任务可以定时读取和解析Pub Server的产物信息。如此Pub Dev才可以满足各类检索需求。

Flutter 在饿了么的应用与沉淀

共建 - 产物服务器

AliFlutter也做了自己的产物服务器,最初主要也是为了提高产物查询速度。与Pub Server类似,依然是先检查是否有内部的缓存,无需再去查询外部产物,从而后面同学查询的速度也更快了。

Flutter 在饿了么的应用与沉淀

共建 - 引擎工作流

同时,产物服务器也可以作为引擎工作流。AliFlutter也对引擎做了一些修改,如图片优化和机器优化,再将Android和iOS产物,如编译debu*物,上传到产物服务器中。整个引擎工作流是通过CI实现的。

Flutter 在饿了么的应用与沉淀

共建 - CI/CD

AliFlutter CI/CD的主要目标是打出包含Flutter代码的App,以及Flutter Module下的产物。在标准的App构建流程pipeline之上,饿了么加了很多Flutter编译逻辑和产物的特化逻辑。先把App Framework和Flutter Framework都打出来,还需要把Flutter Plugin的代码单独打成Framework或aar,生成postsec,最后上传并发布。

Flutter 在饿了么的应用与沉淀

展望与规划

Flutter给饿了么带来的最大的价值是业务落地的提速。在未来,饿了么也会推进更多的业务使用Flutter进行开发,包括跨业务组件的开发,以及Flutter代码的分包。围绕业务,还需保证质量,目前对Flutter性能监控和优化都是不够的,所以计划在性能监控和优化上再进一步扩展。另外,在业务的狂奔过程中,控制Crash数量,做好包体积优化。此外,饿了么也希望做进一步提高研发效率的工作,如扩大UI组件范围,抽取基础插件,通过与AliFlutter共建,做好基础设施及效率工具,如扫码开发等。最后,有剩余时间的话还可以做一些探索性的工作,如动态化UI,Flutter Web,三端一体化开发,引擎定制,同时可以结合前端做一些思考和遐想。总结起来,饿了么对Flutter的期待是保质提效,赋能业务。

Flutter 在饿了么的应用与沉淀

关注「淘系技术」微信公众号,一个有温度有内容的技术社区~

Flutter 在饿了么的应用与沉淀

上一篇:【话题】钉钉、微信沟通的当下,你有多久没登陆过QQ了?


下一篇:Trailblazer —— Rails 的扩展概念驱动开发框架