Android has the ability to detect memory leaks in activities/fragments via LeakCanary, while Flutter does not have a similar open source tool. You can also use the Observatory to check for memory leaks, but it’s tedious, so you need a tool that can quickly detect memory leaks.

Based on the memory leak Monitoring article on Flutter, we have implemented a simple memory leak detection function for your reference.

1. Weak references and GC implementation

There are two main points for good memory leak detection.

  • A weak reference
  • Mandatory gc

1.1. Weak references

Although there is no class similar to WeakReference in Java in Dart, there are still weak references. _WeakProperty is a weak reference in the Dart that holds the value in 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";
}
Copy the code

Obviously, it’s a private class and you can’t use it directly. So how do you use it? With Expando. Its implementation is in expando_patch.dart as follows.

@patch
class Expando<T> {
  @patch
  Expando([String name])
      : name = name,
        _data = new List(_minSize),
        _used = 0;
        
  / /...
  @patch
  void operator[] = (Object object, T value) {
    / /...
    // Calculate idX based on the hash code and mask of object
    var idx = object._identityHashCode & mask;
    / /...
    if (_used < _limit) {
      // Create a _WeakProperty object and add it to the collection _data
      _data[idx] = new _WeakProperty(object, value);
      _used++;
      return;
    }

    // Expand the set _data
    _rehash();
    this[object] = value;
  }
  / /...
}
Copy the code

To use Expando, simply create an Expando object and use it in the way of key-value. Take Expando in the source code MethodChannel as an example.

Expando<Object> _methodChannelHandlers = Expando<Object> ();class MethodChannel {
  void setMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) {
    // The current MethodChannel object is key and the handler is value
    _methodChannelHandlers[this] = handler;
    / /...
  }
  // Get the handler from Expando with the current object
  bool checkMethodCallHandler(Future<dynamic> Function(MethodCall call)? handler) => _methodChannelHandlers[this] == handler;
  / /...
}
Copy the code

1.2. Enforce GC

Let’s take a look at Dart’s GC mechanism. Dart uses the reachability analysis algorithm to determine whether the object is alive or not, and also implements the tag clearing algorithm, copy algorithm and tag collation algorithm to recycle the object. In the Cenozoic era, from and to are two equal Spaces, and the replication algorithm is used for GC. In the old days, the default was to use the tag cleaning algorithm for collection, and when memory was low, the tag cleaning algorithm was used for GC.

Although forced GC is not available in Dart, it can be done through the getAllocationProfile in VM_service. The usage is as follows.

_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
Copy the code

There is no way to listen for GC completion in Java. But with VM_service, Dart can listen for gc completion.

  GcTrigger.init(VmService vmService) {
    _vmService = vmService;
    _vmService.streamListen("GC").then((event) {
      debugPrint("GC listener registration completed");
    });
    // Listen for the GC event stream
    _vmService.onGCEvent
        .listen(onData, onDone: onDone, onError: onError, cancelOnError: false);
  }  
Copy the code

2. Realization of memory leak detection

With weak references and active GC methods, memory leak detection can be implemented. Vm_service is required. Therefore, import the package first.

2.1. Establish a connection with VM_service

Using the getInfo method of the Service class, you can quickly get the path you need to establish a connection. The connection is established through Socket, so you need to convert the path to the Socket format, and then pass the path to vmServiceConnectUri method to successfully establish the connection. Here’s the code.

  void install() {
    const bool inProduction = const bool.fromEnvironment("dart.vm.product");
    // If this is a production environment, return directly
    if (inProduction) return;
    // Get service protocol information
    Service.getInfo().then((serviceProtocolInfo) {
      // Get the connection path
      Uri uri = serviceProtocolInfo.serverUri;
      // convert to ws//... format
      String url = convertToWebSocketUrl(serviceProtocolUrl: uri).toString();
      vmServiceConnectUri(url).then((vmService) async {
        // Get the vmService object
        / /...
      });
    });
  }
Copy the code

2.2. Use of weak references

The use of weak references is simple: create an object. And check that the object is passed to Expando as key. Here’s the code.

  Expando _state = Expando("_state");
  void addWatch(State state) {
    if (_state == null) _state = Expando();
    // State is the object to be detected. Here is the page state object
    _state[state] = true;
    / /...
  }
Copy the code

2.3 forced GC

Enforcing the GC is easy, just call the vmService’s getAllocationProfile method. Here’s the code.

// The second argument must be set to true otherwise gc will not be performed
_vmService.getAllocationProfile(findCurrentIsolateId(), gc: true);
Copy the code

2.4 Obtaining the leakage path

To determine whether the object is reclaimed, you need to get the _WeakProperty instance and determine whether the key in it is null. There is no reflection to get the _WeakProperty instance and determine the key because reflection is not available. The good news is that vm_service provides the getObject method to get an instance, which can be used to get the _WeakProperty instance in Expando.

  void afterGc(Expando expando) async {
    BoundField _dataField;
    // Get Instance according to expando
    Instance instance = await getObject(expando);
    // Release the reference
    expando = null;
    for (BoundField field in instance.fields) {
      // Check whether the attribute name is _data
      if (field.decl.name == "_data") {
        _dataField = field;
        break; }}// Get the reference to the _data property in Expando
    InstanceRef instanceRef = _dataField.value;
    // Get the instance of the _data property in Expando
    instance =
        await _vmService.getObject(findCurrentIsolateId(), instanceRef.id);
    List<dynamic> instanceRefs = instance.elements;
    // Since _data is a set, we can iterate over the set to get a reference to _WeakProperty
    for (InstanceRef instanceRef in instanceRefs) {
      / / _WeakProperty references
      if(instanceRef ! =null) {
      	/ /...}}}// Get the id of the expando object
  Future<String> getObjectId(Expando expando) async {
    return await obj2Id(_vmService, _libraryId, expando);
  }
  
  // Get the Instance of expando
  Future<Instance> getObject(Expando expando) async {
    String objectId = await getObjectId(expando);
    return await _vmService.getObject(findCurrentIsolateId(), objectId);
  }
  // Obtain the ID of the current ISOLATE
  String findCurrentIsolateId() {
    return Service.getIsolateID(dart_isolate.Isolate.current);
  }
Copy the code

The implementation of the obj2Id method comes from memory leak monitoring on Flutter.

After getting the reference to _WeakProperty, you can get the _WeakProperty instance through the getObject method. Since the propertyKey property in Instance corresponds to the key in _WeakProperty. So you can use propertyKey to determine whether an object has been reclaimed.

  void afterGc(Expando expando) async {
    / /...
    for (InstanceRef instanceRef in instanceRefs) {
      if(instanceRef ! =null) {
        print("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
        // There is a memory leak_vmService.getobject (findCurrentIsolateId(), instanceref.id).then((obj) {,if (obj is Instance) {
            Instance instance = obj;
            InstanceRef instanceRef = instance.propertyKey;
            //instanceRef is not null, indicating a memory leak
            if(instanceRef ! =null) {
              // Get the leak path, and the third parameter is the maximum number of paths
              _vmService
                  .getRetainingPath(
                      findCurrentIsolateId(), instanceRef.id, 1000)
                  .then((path) async {
                //elements represents each object in the leak path
                for (RetainingObject retainingObject in path.elements) {
                  debugPrint("RetainingObject:$retainingObject"); }}); }else {
              print("Obj is not Instance, Instance:$Instance"); }}else{... } }).catchError((error) {print("Error obtaining memory leak object after Gc collection:$error"); }); }}}Copy the code

If there is a memory leak, getRetainingPath can be used to get the leaked memory path and analyze where the leak occurred.

3, summarize

Through the above code can achieve memory leak detection function, although simple, but the core function is still in. Of course, there are also a lot of problems, which is the author’s current problems. As follows.

  1. Not listening for every pageStateThe method is so needed on every pageStatethedisposeMethod of manually adding on the currentStateObject, this is too tedious.
  2. The leakage path has not been processed twice, which leads to the tedious analysis of the leakage path at present.

A mature Flutter memory leak detection tool is not complete until the above problems are resolved.

【 References 】

Dart VM Service Protocol 3.42

Memory leak monitoring on Flutter

UME – Rich Flutter debugging tools