Flutter内存泄漏检测

Flutter技术在最近两年可谓是非常火热,本想着经过这几年的快速发展其生态链也越来越成熟。在前一段时间想去Dart packages 上找一个类似于Android中的leakcanary的库居然没有找到,于是自己找了一些资料探索了一番。

内存泄漏对App的影响

内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

怎么检测Flutter内存泄漏

Flutter程序运行在Dart VM中,Dart 又是以Isolate为单元管理自己的任务以及数据的,每个Isolate都有自己的独立Heap。Dart的内存管理和Java非常相似,这里借鉴Android平台leakcanary原理对Flutter内存泄漏进行探索,主要需要掌握以下三点前置知识点:

  • Dart中内管理
  • 内存泄漏检测时机
  • 聚合内存泄漏信息

Dart内存管理

Flutter程序运行在Dart VM中,在VM中Dart 又是以Isolate为单元管理自己的任务以及数据的,每个Isolate都有自己的独立Heap。Heap中有两个内存管理器分别是新生代和老年代,大量的小对象会在新生代创建和回收它采用的是复制算法效率非常高。Dart采用和Java一样的可达性作为垃圾回收的依据,当对象存活很长时间都没新生代回收器回收后就会进入来老年代,老年代采用的是标记清除法,它相对于复制算法更省空间。Flutter对垃圾回收的时机做了一些优化,可参考: Flutter垃圾回收.

GC可达性

当GC根节点持有对象的引用则对象就是存活的,静态变量,单例等全局变量都可作为GC根节点,除此之外当前方法栈中的变量也是GC的根节点:

内存泄漏检测原理

参考Android中的内存泄漏检测流程,主要步骤分为:

  • 使用弱引用引用待观测对象
  • 并在合适的时机,触发 GC
  • 然后检查弱引用的对象是否为 null。如果不为 null,说明发生了内存泄漏(java中是用的引用队列判断)

生命周期结束

Flutter中可以Navigator作为切入点去找到对标Android中Activity的onDestory方法,可以自定义一个NavigatorObserver重写didPush/didRemove方法来监听Route中Widget的创建和移除,通过Route可以获取到对应的Element和Widget

Dart中的弱引用

Dart中也提供了弱引用 WeakProperty 在GC的时候如果对象不可达就会被回收,Dart层源码位置在/engine/src/third_party/dart/sdk/lib/internal/vm/lib/weak_property.dart WeakProperty不直接提供给开发者,只能间接通过Expand 来间接使用弱引用,在Expand中有一个data数组

Expando具体的实现源Expando具体的实现在expando_patch.dart 中,它内部重载了两个操作符set/get,这里需要注意Expand中的Key值不支持String,bool,num以及null类型的数据。

Expando的使用示例

var text = Text('expando');
Expando expando = Expando();
expando[text] = true;

set方法

void operator []=(Object object, T? value) {
  ...
    // 创建_WeakProperty弱引用
      var ephemeron = new _WeakProperty();
      ephemeron.key = object;
      ephemeron.value = value;
      _data[idx] = ephemeron;
      
    ...
  }

get方法

T? operator [](Object object) {

    var idx = object._identityHashCode & mask;
    var wp = _data[idx];

    while (wp != null) {
      if (identical(wp.key, object)) {
        // 获取_WeakProperty弱引用中的value
        return unsafeCast<T?>(wp.value);
      } else if (wp.key == null) {
        // This entry has been cleared by the GC.
        _data[idx] = _deletedEntry;
      }
      idx = (idx + 1) & mask;
      wp = _data[idx];
    }

    return null;
  }

主动触发VM GC

如何主动触发Dart GC,这里参考的devtools中的实现借助于vm_service(见下文介绍), 通过getAllocationProfile方法可以让VM进行Full GC,当DevTools Memory中出现蓝色小圆点就是VM GC了

vmService.getAllocationProfile(isolateId, gc: true);

Engine层的代码

void Heap::CollectAllGarbage(GCReason reason) {
  Thread* thread = Thread::Current();

  // New space is evacuated so this GC will collect all dead objects
  // kept alive by a cross-generational pointer.
  EvacuateNewSpace(thread, reason);
  if (thread->is_marking()) {
    // If incremental marking is happening, we need to finish the GC cycle
    // and perform a follow-up GC to purge any "floating garbage" that may be
    // retained by the incremental barrier.
    CollectOldSpaceGarbage(thread, kMarkSweep, reason);
  }
  CollectOldSpaceGarbage(
      thread, reason == kLowMemory ? kMarkCompact : kMarkSweep, reason);
  WaitForSweeperTasks(thread);
}

vmService

vmService的使用可以参考 vm_service,这里简单介绍一下Vm_Service的工作原理,VM_Service是Dart Developer Service简称DDS的通信协议,在Debug环境下Dart VM会创建一个vm-service的Dart Service Isolate监控VM中的各种状态,有点类似于JVMTI。我们可以借助于VM_Service去获取DartVM运行时的数据,Debug模式下DartVM在初始化Root Isolate的时候会启动一个Dart Service Isolate,这个Service Isolate可以获取VM运行时数据 dev tools中的调试信息也是用vmService获取到VM中的各种信息,Service Isolate中会启动一个WebSocket提供外部服务。

vmService协议的工作原理

通过VM_Service获取运行时数据

VM_Service中的数据交互用的json协议 ,具体可参考接口说明。VM中会给所有的东西(对象,方法,类)都分配一个id(这些id有些是动态的回收利用)VM_Service中都是通过id去获取类,对象的信息。首先我们要想办法获取对象运行时id,这里需要借助两个额外的顶级函数通过vmService.invoke获取对象的id

VM_Service中提供的数据结构有ObjRef, Obj两种类型ObjRef 指的是引用类型,ObjRef的数据结构比较简单。而Obj数据结构很详细。

获取对象在VM中的id

参考Flutter内存泄漏监控,通过定义两个顶级函数,用vm_service去调用generateNewKey生成key的id,再用vm_service调用key2Obj函数获取对象的id,vm_service操作的也只能是id

获取对象id示例代码

int _key = 0;

/// 顶级函数,通过invoke调用获取到key的id
String generateNewKey() {
  return "${++_key}";
}

Map<String, dynamic> _objCache = Map();

/// 顶级函数,通过invoke调用获取到object的id
dynamic key2Obj(String key) {
  return _objCache[key];
}


// dart对象转vm中的id
Future<String> obj2Id(dynamic obj, {sdk.Isolate? sdkIsolate}) async {
  String isolateId = _getIsolateId(sdkIsolate: sdkIsolate);
  VmService vmService = await getVmService();
  Isolate isolate = await vmService.getIsolate(isolateId);

  LibraryRef libraryRef = isolate.libraries!
      .where(
          (element) => element.uri == 'package:flutter_leaks/object_util.dart')
      .first;

  String libraryId = libraryRef.id!;

  // 用 vm service 执行 generateNewKey 函数生成 一个key
  Response keyRef =
      await vmService.invoke(isolateId, libraryId, "generateNewKey", []);
  //获取 generateNewKey 生成的key
  String key = keyRef.json!['valueAsString'];
  //把obj存到map
  _objCache[key] = obj;

  //key在vm中对应的id
  String vmkeyId = keyRef.json!['id'];
  try {
    // 调用 key2Obj 顶级函数,获取obj的在vm中的信息 (ps:使用vmService调用有参数的函数不能直接传参数的值,需要传参数在VM中对应的id)
    Response objRef =
        await vmService.invoke(isolateId, libraryId, "key2Obj", [vmkeyId]);
    // 获取obj在vm中的id
    return objRef.json!['id'];
  } finally {
    //移除map中的值
    _objCache.remove(key);
  }
}

获取调用链信息

vmService也给我们提供了方法获取引用链的方法,通过getRetainingPath可以获取泄漏的引用链路,它返回的数据有一个数组就是到GC Root的链路

vmService.getRetainingPath(isolateId, objId, limit);

实现内存泄漏检测

基于以上原理写了一个内存泄漏检测示例,需要注意的是页面退出后不会立马触发GC。这里我们是延时一段时间手动GC一次,当发现对象还未被回收再次手动GC一次。

处理const 对象

const是编译是常量,下面的代码c1,c2都是同一个对象而且不会被GC回收,c3会重新分配堆内存。所以我们在做内存泄露统计时候应该要排除const对象的统计

main() {

  final c1 =  const ConstClosure();
  final c2 =  const ConstClosure();
  final c3 =   ConstClosure();

  print('c1 == c2 ---->  ${c1 == c2}');
  print('c1 == c3 ---->  ${c1 == c3}');

}

class ConstClosure {
  const ConstClosure();
}

上述代码会输出true,false

在getRetainingPath中对于const常量对象在创建它的地方引用是CodeRef,这里我们需要对const进行排除

聚合引用链

这里因为用到了Expando,我们在拿到检测对象后要立马将Expando置为null消除Expando对检测对象的引用避免我们通过getRetainingPath拿到的不是我们想要的泄漏链路,当对象泄漏了这条链路回到GC Root结束。

这里通过简单的聚合我们找到泄漏的链路,在实际项目中的链路可能会特别的长,需要做一些链路的删减才会直观明了

总结

通过以上实践完成了一个简单的flutter_leakcanary,在项目使用中能够准确地帮我们找到内存泄漏的点。

参考

vm_service
Flutter垃圾收集器
Flutter内存泄漏监控

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/125919470