背景
一定规模的App开发如要引入Flutter开发体系,因某些原因如底层二、三方Native库或页面调用,不可避免需要混合开发的能力,但Flutter本身是个单容器的应用,纯粹引入SDK会遇到页面在Flutter和Native跳转无法流畅切换,没有统一的路由管理等问题。我们发布的FlutterBoost1.0能很好的解决这些问题(文档参考这里)。同时,我们也持续关注到以下痛点:
Flutter在不断的演进升级,其演进会给上层应用来到更多可能;
同时,在我们的业务应用中,FlutterBoost1.0在线上使用的过程中遇到一些如黑屏和白屏的反馈,这些历史遗留问题我们希望解决掉;
最后,社区的关注及需求是推动我们前进的动力,我们也希望借此将FlutterBoost的开源做的更完善,比如增加更多测试用例,更多文档等等。
这篇文章介绍了FlutterBoost2.0(统称1.0之后的所有适配版本,下同)针对上述问题的架构升级,并且重点介绍iOS端在升级的过程中遇到的问题和解决方式。
问题分析
背景容器的升级:渲染和引擎的解耦
大家知道FlutterBoost1.0是单页面模式,即不管你打开多少Flutter页面,其实呈现页面的FlutterViewController或者FlutterView其实仅有一个,这其实是有历史原因的。让我们从Flutter底层的架构说起。Flutter发展到现在,在Plugin, ViewController(FlutterView),Flutter Engine这三个核心对象的管理上一直在演进。1.5版本是个分水岭,终于对这三个对象做了较好的解耦。如flutter0.x版本,这三个对象的关系及我们使用API的方式是这样的(根本看不到Engine对象):
作为使用方,我们看不到Engine对象,因为Engine已经内置在FlutterViewController中,没有暴露出来。Flutter1.0做了解耦和改进,如下图:
我们可以看到虽然全局还是仅有一个FlutterViewController实例,但FlutterEngine被单独剥离出来。不过三者关系还是没理清。如,Plugin似乎应该注册到Engine上更合理,怎么会和负责渲染的FlutterViewController发生关系;Flutter Engine虽然已经暴露出来给用户直接使用,但和VC之间还是1对1的关系,按理说负责引擎和数据的Engine全局唯一,渲染层FlutterView应该可以多次切换啊。终于在1.5之后,我们看到Flutter团队努力的结果:
Engine终于完全剥离,Plugin也终于“嫁”对了人。FlutterViewController和Engine不再是同生共死的组合关系,而仅是个通过VC表现层呈现及事件获取的依赖。我们可以猜到Flutter尝试往多引擎方向发展的努力!相应的,我们也给FlutterBoost提出了适配Flutter新架构的要求。
页面白屏或黑屏
FlutterBoost1.0受限于Flutter的架构,出于节省内存的考虑,全局只有一个FlutterViewController。同时在混合页面滑动切换的时候,为了快速显示上一个页面并实现原生页面切换的效果,采用CPU截图的方式为每个页面保存了打底图。打底图通过文件和内存二级缓存的方式避免持有多张图片的内存问题。但这也带来了一些问题:在切换的过程中因需要对老页面截图及加载之前截图图片等耗时工作,会偶尔出现白屏或者黑屏问题——截图和加载都在CPU线程上进行,会影响主线程渲染;而且在页面切换的时候,截图和加载图片操作虽然处于降低内存的目的,但会带来短暂的内存飙涨(见下图),虽然持续的时间很短,但带来了OOM的abort的风险。
生命周期管理
Flutter目前的架构是单实例的,这意味着一个FlutterViewController的生命周期和整个App的生命周期AppLifecycleState是一致的。页面完全隐藏或者app切后台都会发送pause消息告知监听者告知app要暂停。但这在有多个flutter页面和原生页面共存的混合栈情形下显然不合理。针对单个Flutter Container页面,也需要有自己可见与不可见的事件通知。FlutterBoost1.0中没有解决这个问题,在闲鱼内部也导致一些小问题,比如二次打开视频播放页面后,老页面通过app在WidgetBinding中监听了pause事件就将播放器stop,但同时也将新页面本自动播放的视频也停止了。如果有单独的页面事件来分别精准控制而非依赖于整个App的事件就能解决这个问题。我们曾就这个VC页面和应用生命周期的设计和Flutter的同学讨论过,他们也觉得是个问题,但受限于当初的设计,暂没有具体的解决时间点。
开源建设
升级之前,我们在github上的issue较多,同时文档不足,升级计划也不明确,单元测试并不全面。这些短板需要我们在升级后解决。
解决方案
页面管理方案升级
新版不再维护单一的FlutterViewController(或FlutterView),而是和原生一样每次有新页面请求时就直接打开新的ViewController或者FlutterView,和管理原生的页面一毛一样。如此,自然也不需要实现截图功能,小伙伴们再也不用担心通过CPU截图导致白屏或者黑屏的困扰啦。我们看如下页面结构前后的对比。如上图,升级前全局只有一个FlutterViewAdapter(其实是FlutterViewController),负责FlutterView渲染子View并将其内嵌在每个FLBFlutterViewContainer(继承自UIViewController)中,每次新的FLBFlutterViewContainer拉起时就需要复杂的detach和attach操作来转移唯一的FlutterView,同时进行截图缓存。升级后,不再需要内嵌的FlutterViewAdapter和Screenshot缓存列表:其底层实现也变的更加简单,不再需要在detach页面的时候截图,下图是前后两个方案在页面pop和push过程中的对比。
内存及稳定性治理
在页面管理方案升级之后,白屏和黑屏问题解决了,世界就应该安静了。但我们继续做了深入的内存及稳定性治理,发现新版本在iOS下每个新的VC打开的时候虽然没有了内存飙涨的peak,但每个新页面会带来约10M内存的增量。拿FlutterBoost的官方demo做了测试,1.54这个版本就是升级后的原始版本:这个内存增量是因为什么导致的?我们升级前后内存变化做了分析,如下图(左边是升级前,右边是升级后):
发现内存的增量主要来自于Anonymous VM和IOSSurface。Android版本这类问题并不明显,暂略不表。
IOSurface的增量
什么是IOSurface?从apple的文档里了解到:
The IOSurface framework provides a framebuffer object suitable for sharing across process boundaries.
记得哪一年苹果的开发者大会上重点提过这个,主要是和iOS上OpenGL的GPU内存和CPU内存管理创新有关,如CVOpenGLESTextureCacheCreateTextureFromImage就是基于IOSurface的能力直接从图像映射为纹理,而不像OpenGL官方的glTexImage2D需要将图像从CPU传输到GPU再映射,从而提高了性能。
在Flutter里,Rasterrizer的setup和teardown会使底层系统创建和销毁IOSurface。FlutterViewController的surfaceUpdated会用于删除或创建Rasterrizer。我们review了升级后对于VC的surface的控制,的确有许多不合理之处——多次重复调用surfaceUpdated。
导致多次调用的根本原因脱离不了FlutterViewController单引擎单页面的设计。FlutterVC设计并不考虑混合栈的情形,它设想的场景是全局一个engine,只需管理其唯一持有的FlutterVC的生命周期。如此,其surfaceUpdated函数固化在view的appear和disappear事件中,并没有暴露出来。这导致混合栈在处理多FlutterVC页面切换的时候,无法重写页面事件处理函数而灵活处理何时应该创建和销毁surface,最终不可避免的重复创建surface或者销毁surface。
同时,我们发现页面present和push在iOS13下将被覆盖的页面的生命周期和新弹出的页面生命周期顺序还不一样。比如present在新页面view appear之后才会disppear老页面,并不像push的时候先disppear老页面然后再appear新页面。这也给我们处理混合栈页面的surfaceUpdate带来了困难。
为了兼容这个页面逻辑,并且尽量避免多次创建或销毁surface,我们重新梳理了页面的生命周期,只在viewDidAppear的时候重新创建surface,而在viewDisappear的时候仅对非前台VC进行删除surface。但尽管如此,受限于FlutterVC固化了surfaceUpdate的调用,这里还是难以避免会多重复一次创建和销毁surface。不过,内存略有改观,线上稳定性得到不少提升。见下图内存比较。
每个新的VC打开后,其会通过CAGLLayer渲染内容,CAGLLayer会持有后台的content。正是基于这个content,iOS系统内部对VC进行截图,在页面切换的时候才有半开半闭的动态效果。相对于FlutterBoost1.0的CPU截图,系统截图自然有许多性能的优化,但带来内存的增长难以避免。为了验证这个问题,我写了个类似Flutter的用OpenGL渲染UIView的demo。果然,每次打开新页面,内存肯定增长10M左右。这个增量似乎难以优化,只能设法避免。目前我们推荐两种方式:
通过页面栈里页面个数限制来避免过多页面导致OOM。如闲鱼这边限制了页面多次push后,仅保留最近3个页面。
避免从Flutter页面打开Flutter页面就新建FlutterVC,可重用上次的FlutterVC。FlutterBoost提供了这种能力,BoostContainer继承了Navigator,原生支持Navigator的能力。但这里需要用户自己判断何时应该使用Navigator的push,何时用FlutterBoost的open来打开页面。后期我们会增加一些这样的demo。
稳定性治理
每次底层库的升级都会带来稳定性问题。为了保障稳定性,我们通过收集线上crash日志的方式,定位到几个Engine层面的bug。这些bug或提交了issue给google,或在我们engine内部版本做了规避。如无障碍模式下FlutterSemanticObject泄漏导致crash,参考https://github.com/flutter/engine/pull/14155。在Flutter1.12下,多FlutterVC情形会触发surface空指针调用而crash,参考https://github.com/flutter/flutter/issues/52455。其他还有一些bug,我们在内部版本做了规避,并和google做了沟通,但因复现难度等原因,还未取得一致的结论。整体上,FlutterBoost2.0在闲鱼内部场景升级后,经多轮灰度及线上验证,稳定性不错,效果较好。
页面生命周期管理及其他
FlutterBoost完善了之前的ContainerLifeCycle,在Dart层能较好的支持页面的appear和disappear事件,同时能监听app切到Background或者Foreground事件。如果涉及到页面的生命周期管理,建议您使用FlutterBoost.singleton.addBoostContainerLifeCycleObserver()来注册相关事件监听程序。社区同学也给了不少建议,比如帮忙优化了hero动画的能力等。其他功能提升及使用上的建议。为了整个框架更稳定和易于回归验证,我们也补了一些单元测试。目前主要是Dart层面的用例(覆盖率达70%左右),后续会增加混合页面跳转方面的用例。同时定义了升级计划和release清单(见首页)。在这轮升级后,我们总结了目前FlutterBoost的能力对比表,供参考:
总结
综上所述,经过此次升级,flutterboost解决掉了页面切换时白屏或者闪烁等问题,同时代码也更简洁易懂。同时我们对内存及稳定性做了分析。对于内存上的问题,给出了规避的方式,稳定性上我们主要通过页面的detach和attach时序优化来解决。后续我们会继续优化代码,增加更多的测试用例,尤其是支持混合测试的用例。同时在Flutter页面跳Flutter页面上也在考虑不影响上层业务的基础上支持一致的API。路漫漫其修远兮,FlutterBoost会一直与时俱进,进化为更加完美的框架。也欢迎大家多参与到这个框架的开发中,一起讨论并改进他。最后,我们也开发了一个使用flutterboost的脚手架:flutter-boot。欢迎使用并送小星星,地址在:https://github.com/alibaba-flutter/flutter-boot
2020,感谢有你
I will be at your side
抖音:@闲鱼技术
今日头条:@闲鱼技术
● 扫码关注我们