分类
联系方式
  1. 新浪微博
  2. E-mail

Flutter 自动化内存泄漏检测

介绍

内存泄漏是软件开发中难以避免的问题,内存泄漏检测技术应运而生。其中,自动化内存泄漏检测技术,是该领域的一项高级技术,著名的业界案例有 Android 的 LeakCanary。

Flutter 使用 Dart 语言,同样存在内存泄漏问题。随着 Flutter 开发日益普及,目前业界诞生了多套 Dart/Flutter 自动化内存泄漏检测方案。

目前市面上内存泄漏检测的方案有:

  1. 基于 Expando + VM Service 的弱引用检测技术
  2. 基于 Dart VM 定制的底层弱引用检测技术
  3. 基于 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_servicegetAllocationProfile(isolateId, gc: true)

判断是否有实例泄漏,即判断 Expando 的 _data 属性是否为空,如果不为空,说明泄漏了。

获取泄漏路径:vm_service 提供了一个 API 叫 getRetainingPath(isolateId, objectId, limit),API 给出的路径有些冗长,需要进行简化缩短

监控实例实际

在 generateRoute 路由创建时对页面 Widget 进行监控。参考文献2。

State 监控,有侵入式,添加基类,方式。参考文献2。

进行检查监控

基于 NavigatorObserver 的 didPop,延时 400ms 进行检测。参考文献2。

网络资源

Flutter 上的内存泄漏监控

  • 快手:Expando + VM Service 内存泄漏检测

这可能是,Flutter 中最“强悍”的内存泄漏检测方案......

  • 阿里:定制 Flutter 引擎内存泄漏检测

Flutter内存泄露检查工具实践

  • 提供了开源方案 leak_detector
    • 这份实现质量很高值得参考
    • 使用 compute 将路径计算分发到单独 Isolate 进行