Flutter 自动化内存泄漏检测
介绍
内存泄漏是软件开发中难以避免的问题,内存泄漏检测技术应运而生。其中,自动化内存泄漏检测技术,是该领域的一项高级技术,著名的业界案例有 Android 的 LeakCanary。
Flutter 使用 Dart 语言,同样存在内存泄漏问题。随着 Flutter 开发日益普及,目前业界诞生了多套 Dart/Flutter 自动化内存泄漏检测方案。
目前市面上内存泄漏检测的方案有:
- 基于 Expando + VM Service 的弱引用检测技术
- 基于 Dart VM 定制的底层弱引用检测技术
- 基于 OpenGL 图形资源监控的检测技术
在本文中,我对市面上的几种方案进行统一调研,并将心得记录下来。
内存泄漏的检测原理
对于垃圾回收语言来说,内存泄露的检测原理是相同的。
内存泄漏原理
对于一个对象实例,正常情况下,生命周期结束后,应当被垃圾回收器回收,释放内存。
在问题场景下,一个生命周期已经结束的实例,仍有其它实例对其持有引用(应当被释放而没释放),这时垃圾回收器回收时,它看到该实例还有引用,没有进行释放。从垃圾回收器的机械视角,它认为一个实例还有引用指向它,那这个实例就还有用,不应该释放。如果没有引用(只有 GC Root 一个引用),那就是没用了,可以回收了。
当一个对象实例生命周期结束后,被原本不该引用的关系引用,导致垃圾回收器没有将其释放,这种情况就被称为内存泄漏。
内存泄漏检测原理
一个实例持有另一个实例,称为引用,引用关系主要可分为两种:强引用,弱引用。
强引用会增加被持有实例的引用计数,参与垃圾回收器统计。弱引用不会增加引用技术,持有的实例随时可能被回收。
内存泄漏检测技术,就是利用了弱引用的特性。
对于需要监控的实例,通过弱引用对其进行引用,当该实例生命周期结束后(由业务决定),进行一次 Full GC,通过弱引用再去访问该实例,如果还在,说明泄漏了,如果为空,说明没有发生泄漏问题。
内存泄漏检测的难点在于时机的把控,弱引用建立时机、判断生命周期结束的时机。
如果时机选择不对,比如一个对象刚创建实例,就去检测,发现弱引用实例还在,爆出虚假内存泄漏,属于误报。检测的难点之一就是做到不误报。
方案一:Expando + VM Service 内存泄漏检测
Expando + VM Service 是目前采用最多的 Flutter 内存泄漏检测方案,因为这种方案对底层代码无侵入,并且开发起来比较简单。
这里推荐开源项目:leak_detector,给出了一套比较高质量的实现方案,这里以该项目作为参考。
基础知识
Expando
如何判断一个实例有没有泄漏?弱引用是一种方法,如果 Full GC 之后,弱引用还在,说明这个实例被泄漏了。
在 Dart 中,有一个类 Expando(实现文件 expando_path.dart),他赋值时会以弱引用方式持有:
class Expando<T> {
external T operator [](Object object);
external void operator []=(Object object, T value);
}
Expando 有一个问题,它只提供了 setter,没有提供 getter。
没有 getter 情况下如何进行访问?利用 VM Service。
VM Service
VM Service 是一个用于开发时的虚拟机调试服务。对应的调用 SDK。
核心概念:
- ObjRef:引用类型
- Obj:实例类型
- id:id 是对象在 vm_service 里的标识符
访问 Expando
通过 getObject 获取 Expando 的 _data 私有成员,能够像反射一样访问内部属性。
具体实现方案,参考文献1。
检测原理
内存泄漏分析分为以下流程:
内存泄漏检测的一个关键是确定检测的时机,这个时机是跟着被检测对象的生命周期走的。
什么是被检测对象的生命周期?是从业务角度预先得知的该对象什么时候有用,什么时候没用。并且因开发领域的不同,生命周期的概念是不同的。比如:
- 前端角度:以页面维度进行资源监控,页面打开时是生命周期的开始,页面结束后是生命周期的结束,按理说,页面退出之后,与之伴随创建的资源都应当被释放,恢复到页面打开前的水平
- 后端角度:以接口维度进行资源监控,请求响应时是生命周期的开始,请求结束后是生命周期的结束,按理说,请求结束之后,与之伴随创建的资源都应当被释放,恢复到请求发生前的水平
Flutter 属于前端场景,对于前端场景而言,通常以页面维度进行监控。在 Flutter 中,没有一个确切的页面实体,通常以 Navigator 过场中,传入 Route 的 Widget 作为一个页面。
检测内存泄漏
首先要触发 Full GC:参考 DevTools 实现,Dev Tools 是调用了 vm_service
的 getAllocationProfile(isolateId, gc: true)
判断是否有实例泄漏,即判断 Expando 的 _data 属性是否为空,如果不为空,说明泄漏了。
获取泄漏路径:vm_service
提供了一个 API 叫 getRetainingPath(isolateId, objectId, limit)
,API 给出的路径有些冗长,需要进行简化缩短
监控实例实际
在 generateRoute 路由创建时对页面 Widget 进行监控。参考文献2。
State 监控,有侵入式,添加基类,方式。参考文献2。
进行检查监控
基于 NavigatorObserver 的 didPop,延时 400ms 进行检测。参考文献2。
网络资源
- 快手:Expando + VM Service 内存泄漏检测
这可能是,Flutter 中最“强悍”的内存泄漏检测方案......
- 阿里:定制 Flutter 引擎内存泄漏检测
- 提供了开源方案 leak_detector
- 这份实现质量很高值得参考
- 使用 compute 将路径计算分发到单独 Isolate 进行