@qinyun
2018-09-02T00:06:07.000000Z
字数 5128
阅读 3652
未分类
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和闲鱼现有的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。
第二,构建工具和引擎的优化。
第三,跟现有的构建工具打通,混合调试等。
除了上述的研发时问题,接下来就是让它跑起来,解决运行时问题。其中最重要的是实现混合栈。
在混合工程中,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做同步的跟随。
到此,混合栈的方案就简单介绍完了。
接下来,如果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商品详情页的页面的开发框架。
右边边绿色的这一部分,就是整个页面的结构,整个详情页面是一个大列表,由商品的描述、图片,评论,个性化推荐等组成。这里简单概括几个特点:
通过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%之间。
大家可能会关心Flutter在生产环境的稳定性,兼容性等表现。闲鱼使用Flutter的前期阶段,这方面确实有很大的问题。前期在真实环境中发现了很多问题,第一次灰度测试时Crash率有百分之一的量级,主要的Crash问题包括内存、GPU、icu data、视频播放、截图接口、armv7,字体缺失等。
我们和Google团队一起,通过几个版本的灰度迭代,用了一个半月的时间,把问题逐步解决了,目前Crash率收敛稳定,达到万分之一的量级,已经达到了生产标准。
我们对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框架遵循响应式,声明式设计原则,个人感觉,较容易上手。谢谢大家,由于水平有限,可能会有错误,请大家指正。篇幅有限本文无法对每个细节深入探讨,关于细节的深入分享,欢迎大家关注“闲鱼技术”的公众号。