阅读 1193

Flutter内存泄漏检测实践(一)

Android中可以通过 leakcanary来检测Activity/Fragment中是否存在内存泄漏,而Flutter中并无类似的开源工具。虽然也可以用Observatory来查看是否存在内存泄漏,但比较繁琐,所以需要一个可以快速检测内存泄漏的工具。

根据Flutter 上的内存泄漏监控一文,笔者实现了一个简单的内存泄漏检测功能来供大家参考。

1、弱引用及gc的实现

要很好的进行内存泄漏检测,主要在于以下两点。

  • 弱引用
  • 强制gc

1.1、弱引用

Dart中虽不存在类似JavaWeakReference的类,但还是存在弱引用的。_WeakProperty就是Dart中的弱引用,它是以key-value来保存值。

@pragma("vm:entry-point")
class _WeakProperty {
  factory _WeakProperty(key, value) => _new(key, value);

  get key => _getKey();
  get value => _getValue();
  set value(value) => _setValue(value);

  static _WeakProperty _new(key, value) native "WeakProperty_new";

  _getKey() native "WeakProperty_getKey";
  _getValue() native "WeakProperty_getValue";
  _setValue(value) native "WeakProperty_setValue";
}
复制代码

很明显,它是一个私有类,无法直接使用它。那么如何使用它尼?通过Expando即可。它的实现在expando_patch.dart中,如下。

@patch
class Expando<T> {
  @patch
  Expando([String name])
      : name = name,
        _data = new List(_minSize),
        _used = 0;
        
  //...
  @patch
  void operator []=(Object object, T value) {
    //...
    //根据object的哈希码及mask计算出idx
    var idx = object._identityHashCode & mask;
    //...
    if (_used < _limit) {
      // 创建一个_WeakProperty对象并添加到集合_data中
      _data[idx] = new _WeakProperty(object, value);
      _used++;
      return;
    }

    // 对集合_data进行扩容
    _rehash();
    this[object] = value;
  }
  //...
}
复制代码

再来看Expando的使用,很简单,直接创建一个Expando对象并按照key-value的使用方式即可,以源码MethodChannel中的Expando使用为例。

Expando<Object> _methodChannelHandlers = Expando<Object>();
class MethodChannel {
  void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
    // 当前MethodChannel对象为key,handler为value
    _methodChannelHandlers[this] = handler;
    //...
  }
  // 通过当前对象从Expando中拿到handler
  bool checkMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) => _methodChannelHandlers[this] == handler;
  //...
}
复制代码

1.2、强制gc

先来了解一下Dart的gc机制。Dart中是采用可达性分析算法来判断对象是否存活的,也实现了标记清除算法复制算法标记整理算法来进行对象的回收。在新生代,fromto是相等的两块空间,采用复制算法来进行gc;在老年代,默认是采用标记清除算法来进行回收,当低内存时,采用的是标记整理算法来进行gc。

虽然强制gc在Dart中是无法实现的,但可以借助vm_service,通过其中的getAllocationProfile来进行强制gc。使用方式如下。

_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
复制代码

Java中无法对gc完成进行监听。但借助vm_serviceDart可以来监听gc是否完成。

  GcTrigger.init(VmService vmService) {
    _vmService = vmService;
    _vmService.streamListen("GC").then((event) {
      debugPrint("GC监听注册完毕");
    });
    //监听gc事件流
    _vmService.onGCEvent
        .listen(onData, onDone: onDone, onError: onError, cancelOnError: false);
  }  
复制代码

2、内存泄漏检测的实现

有了弱引用及主动gc的方法,那么就可以来实现内存泄漏的检测。由于需要借助vm_service。所以需要先导入该包。

2.1、与vm_service建立连接

根据类ServicegetInfo方法,可以很快速的获取建立连接所需要的路径。由于是通过Socket来建立连接的,所以需要将路径转成符合socket格式,然后将路径传给vmServiceConnectUri方法即可成功的建立连接。代码如下。

  void install() {
    const bool inProduction = const bool.fromEnvironment("dart.vm.product");
    //如果是生产环境,直接返回
    if (inProduction) return;
    //获取service协议信息
    Service.getInfo().then((serviceProtocolInfo) {
      //获取到连接路径
      Uri uri = serviceProtocolInfo.serverUri;
      //转换成ws//...格式
      String url = convertToWebSocketUrl(serviceProtocolUrl: uri).toString();
      vmServiceConnectUri(url).then((vmService) async {
        //拿到vmService对象
        //...
      });
    });
  }
复制代码

2.2、弱引用的使用

弱引用的使用就很简单了,创建一个对象。并要检测对象最为key传递给Expando。代码如下。

  Expando _state = Expando("_state");
  void addWatch(State state) {
    if (_state == null) _state = Expando();
    //state是要检测的对象,这里是页面的State对象
    _state[state] = true;
    //...
  }
复制代码

2.3、强制gc

强制gc就很简单了,直接调用vmServicegetAllocationProfile方法即可。代码如下。

//第二个参数必须设为true,否则不会进行gc
_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
复制代码

2.4、泄漏路径的获取

要判断对象是否被回收,需要拿到_WeakProperty实例并判断其中的key是否为null。由于无法使用反射,所以无法通过反射来拿到_WeakProperty实例并判断其中的key。好在vm_service提供了getObject方法来获取一个实例,通过该方法就可以拿到Expando中的_WeakProperty实例。

  void afterGc(Expando expando) async {
    BoundField _dataField;
    //根据expando来得到Instance
    Instance instance = await getObject(expando);
    //释放引用
    expando = null;
    for (BoundField field in instance.fields) {
      //判断属性名称是否是_data
      if (field.decl.name == "_data") {
        _dataField = field;
        break;
      }
    }
    //拿到Expando中_data属性的引用
    InstanceRef instanceRef = _dataField.value;
    //拿到Expando中_data属性的实例
    instance =
        await _vmService.getObject(findCurrentIsolateId(), instanceRef.id);
    List<dynamic> instanceRefs = instance.elements;
    //由于_data是一个集合,所以可以遍历该集合从而拿到_WeakProperty的引用
    for (InstanceRef instanceRef in instanceRefs) {
      //_WeakProperty的引用
      if (instanceRef != null) {
      	//...
      }
    }
  }
  //根据expando对象来拿到该对象的id
  Future<String> getObjectId(Expando expando) async {
    return await obj2Id(_vmService, _libraryId, expando);
  }
  
  //拿到expando对于的Instance
  Future<Instance> getObject(Expando expando) async {
    String objectId = await getObjectId(expando);
    return await _vmService.getObject(findCurrentIsolateId(), objectId);
  }
  //获取当前isolate的id
  String findCurrentIsolateId() {
    return Service.getIsolateID(dart_isolate.Isolate.current);
  }
复制代码

obj2Id方法的实现来自于Flutter 上的内存泄漏监控中。

拿到_WeakProperty的引用后,又可以通过getObject方法来拿到_WeakProperty实例。由于Instance中的propertyKey属性就对应着_WeakProperty中的key。所以可以通过propertyKey来判断是否回收了对象。

  void afterGc(Expando expando) async {
    //...
    for (InstanceRef instanceRef in instanceRefs) {
      if (instanceRef != null) {
        print("--------------------------------------------------------------");
        //存在内存泄漏
        _vmService
            .getObject(findCurrentIsolateId(), instanceRef.id)
            .then((obj) {
、          if (obj is Instance) {
            Instance instance = obj;
            InstanceRef instanceRef = instance.propertyKey;
            //instanceRef不为null,表示存在内存泄漏
            if (instanceRef != null) {
              //获取泄漏路径,第三个参数是最大路径数量
              _vmService
                  .getRetainingPath(
                      findCurrentIsolateId(), instanceRef.id, 1000)
                  .then((path) async {
                //elements表示泄漏路径中的每个对象
                for (RetainingObject retainingObject in path.elements) {
                  debugPrint("retainingObject:$retainingObject");
                }
              });
            } else {
              print("obj is not Instance,Instance:$Instance");
            }
          } else {...}
        }).catchError((error) {
          print("Gc回收后获取内存泄漏对象出错:$error");
        });
      }
    }
  }
复制代码

如果存在内存泄漏,那么通过getRetainingPath就可以拿到泄漏的内存路径,然后进行分析是哪里产生了内存泄漏。

3、总结

通过上面代码就可以实现内存泄漏检测功能,虽简单,但核心功能还是在的。当然也存在很多的问题,也是笔者目前遇到的问题。如下。

  1. 由于没有监听每个页面State的方法,所以需要在每个页面Statedispose的方法手动添加对当前State对象的监听,这样就太繁琐了。
  2. 泄漏的路径未进行二次处理,从而导致目前来分析泄漏路径是比较繁琐的。

待上面问题解决后,一个成熟的Flutter内存泄漏检测工具才算完成。

【参考资料】

Dart VM Service Protocol 3.42

Flutter 上的内存泄漏监控

UME - 丰富的Flutter调试工具

文章分类
Android
文章标签