*本文转载自 BIGO技术 公众号,对BIGO技术感兴趣的同学可以关注了解更多内容~
一、前言
Flutter 是谷歌推出的一款跨平台 UI 开发框架,开发效率高效,性能媲美原生,最近一年,逐步落地到 BIGO 旗下 Hello,HelloYo,电商等应用,带来开发效率提升的同时,也引入了大量的风险和挑战,其中 Flutter 内存占用提升带来的 App 稳定性风险最为突出。
BIGO 客户端基础架构组在 Flutter 内存方面做了大量的工作:
(1)质量监控方面;在灵雀 APM 原生性能监控的基础上,研发了 Dart 内存泄露监控,Dart 内存占用分析等全新的子系统,覆盖开发与发布阶段。
(2)基础设施方面;针对 Flutter 原生图片存在的种种问题,研发了基于外接纹理的图片框架作为替代品,内存峰值降低 50% 以上,大大减少了线上 OOM 发生几率。
除此之外,还引入了主动 GC,引擎共享,长列表图片加载优化等多项优化方案。
本文将从 Flutter 内存基础知识开始讲起,介绍下 Flutter 内存现状与风险,核心场景的内存优化,线下与线上全链路监控与分析等内容。
二、背景知识
Flutter 应用的内存占用主要包括三大部分(如下图所示)
(1)Flutter 引擎和 Dart VM 本身的内存占用。
(2)Dart 对象的内存占用,这部分内存会分配在 Dart 堆内存上。和 Java 的堆内存结构类似,Dart 也将堆内存分为新生代和老年代:新生代存放生命周期较短、内存占用较小的对象;老年代存放生命周期长、内存占用大的对象。
那么如何确定 DartVM 占用的具体内存大小呢?
我们设计了一个混合编译的测试方案,从启动 Flutter 后又退回到原生页面后,仍然有大约 28 MB 的内存占用,其中包括 3.5 MB 的 Native 内存增长,7.53 MB 的 code 内存增长,6.5 MB 的 private other 内存增长,以及 10 MB 的 system 内存增长。当我们尝试调用了 Flutter 提供的引擎销毁接口(如下图所示,ANDROID_SHELL_HOLDER 只是一个共享指针,只会释放 Flutter 引擎本身运行时内存,不会释放 DartVM ),内存继续降低 6 MB 左右( Flutter 引擎本身运行时内存),基本来自 private other 部分内存的释放。
三、Flutter 内存现状和问题
除了 Flutter 相对原生组件带来的内存提升,开发过程中也遇到很多引擎内部引发的内存问题,比如 OOM,内存泄露,内存回收缓慢等,官方的 Issue 中也有很多类似的反馈。
最常见的比如 iOS 上展示 Emoji 表情带来的内存飙升(https://github.com/flutter/flutter/issues/36358),Skia 自身的内存泄露(https://github.com/flutter/flutter/issues/47108),快速滑动图片列表导致的 OOM 等。
除了内部 Flutter 引擎版本即时同步官方修改来解决问题外,BIGO 内部也针对很多场景进行了专项优化,比如长列表图片内存优化,基于外接纹理的图片管理框架,引擎共享等,下面会详细介绍。
四、Flutter 内存优化
首先来看图片组件,作为项目开发的最基础组件之一,同样也是内存消耗大户,对一个应用的质量有举足轻重的影响。在 Flutter 开发中,Image 作为 Flutter 的原生图片组件,存在很多天然的缺陷:
(1)单独资源目录,无法访问 Native 端的资源文件,混编项目可能会有同一份资源两份内存占用的情况。
(2)Flutter 本身内存缓存较为简陋,没有磁盘缓存。
(3)长列表滑动卡顿严重。
(4)图片网络下载方式不可配置。
为了进一步减少图片内存占用,复用原生端成熟的图片下载、缓存组件,我们决定采用外接纹理的方案,来代替 Flutter Image 组件,以获得更好的体验。
另外,我们也采用了几种优化策略(整体架构如下),增加内存复用,减少内存占用,降低峰值:
(1)纹理缓存:采用引用计数和 LRU 缓存来进行纹理的复用,减少内存抖动。
(2)长列表优化:主要针对之前发现的在原生 Image 组件在长列表场景下的缺陷进行优化,包括:串行队列分发,多阶段拦截,缓存策略优化。
(3)纹理复用:同一个图片使用同一个 textureID。
上述的方案均聚焦于降低 Flutter 引擎和核心组件带来的内存消耗,除此之外,我们也尝试了一些主动释放内存的方法,在适当的时机(比如 Android 的 onTrimMemory 和 onLowMemory 回调)时主动回收内存,降低 OOM 发生几率。
首先尝试了 Engine 自带的 Destroy 接口,该接口会释放除了 DartVM(被缓存)的所有内存,但是在实际的测试过程中发现,某些情况下会导致 APP 崩溃,存在一定的风险。
另外一种方式就是尝试显式调用 GC,虽然大多数场景下,开发者并不需要关心 DartVM 的 GC 时机,但是在某些极端场景下,显式调用 GC 可以有效的降低应用发生 OOM 的几率。
前面提到过,Flutter 与 Dart 扩展库并没有显式暴露任何 GC 接口,如果我们需要使用,则必须修改官方引擎代码,为后续的引擎升级带来潜在成本。对此,我们参考了 Flutter 中对 Dart 层接口进行扩展的方式,新建一个类对已有的接口进行扩展和导出(如下图所示),从物理上进行隔离,方便快速完成引擎升级。
五、Flutter 内存监控
Android/iOS 原生占用内存监控已经非常成熟,BIGO 内部主要使用已有的灵雀 APM 进行实时监控。
DartVM 的内存消耗监控却没有那么直接,官方 SDK 没有可以获取到这些信息的接口,所以我们也和扩展 GC 接口一样,扩展了获取 Dart VM 信息的接口,并采用类似 native 端的策略,来取到各个 Flutter 页面的 Dart VM 的内存增量信息。
Dart 层的内存水位监控主要包括(数据格式如下):
(1)内存总容量。
(2)已使用内存。
(3)External 内存使用量 。
在 Flutter 开发中,我们可以使用 Observatory 直接进行 Dart 内存对象的分析,但Observatory 展示的数据是所有 Dart 对象的信息,有时候并不是那么直观。我们需要一个工具来自动监控并检测内存泄露。
Dart 内存泄漏检测主要使用了弱引用 Expando+GC 的原理来判断是否有内存泄漏对象存在。
首先我们把需要检测的对象,放入到 Expando 中,Expando 会根据被检测对象的_identityHashCode 生成一个 key,并进而封装成一个 _WeakProperty 对象,然后弱引用持有key。_WeakProperty 是对 C 层 WeakProperty 对象的一个封装类,具体的弱引用实现是在 C 层实现的。之后主动触发一次 GC,再遍历 Expando 集合中的数据,如果 key 的对象不为空,就说明被检测对象仍然存在着引用链,存在 Dart 对象的内存泄漏。
由于 Expando 存储引用的 map 是一个私有变量,Dart 又不支持反射,一般无法获取。我们可以借助 Dart SDK 提供的 vm_service 服务实现这个私有变量获取,Observatory 的实现基础也是依赖这个服务。基于以上原理,最终实现效果如下图所示:
Flutter 的内存泄露除了一部分属于上述的 Dart 内存泄露之外,还存在一些原生平台 Native 内存的泄露。
以 Android 为例,主要使用了客户端基础架构组研发的 NativeLeakCanary,通过对关键内存操作的记录和分析,定位到泄露发生的场景,使用效果如下:
五、总结
内存优化只是 Flutter 基础质量建设的冰山一角,整个基础质量还有很多其它方面的工作也在不断的探索和建设,包括卡顿、启动耗时、包大小等等,以覆盖更多的业务场景,提供更好的开发和产品体验。另外我们也在提高基础设施的自动化和流程化,使开发同学可以聚焦于业务开发, 提高研发效率。
我们相信 Flutter 的 UI 跨端一致性、几乎接近原生的性能,以及不断成长的生态,会在将来的客户端开发模式中占据 C 位,使所有的业务以及开发者从中受益。
全部评论
(0) 回帖