天天看点

Flutter Image内存--强引用分析方法概述分析总结

概述

据了解,很多Flutter业务上线后都出现内存占用较高的问题,首当其冲的是 Image 内存占用过多。

Image 图片内存过高,可能由于 Flutter ImageCache 对内存缺房控制力导致,也有可能是被业务代码强引用,泄漏导致。如果 Image 被业务强引用,则调整 ImageCache 容量,增加 gc 次数都没有效果。

Flutter Image内存--强引用分析方法概述分析总结

面对这种“强引用”的泄漏,需要定位到引用 Image 的业务代码才能解决。本文主要描述怎么利用 Observatory 工具,定位强引用Image的业务代码。

分析

class ImageTestWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ImageTestWidgetState();
  }
}

class _ImageTestWidgetState extends State<ImageTestWidget> {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Image(
          image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
          width: 200.0,
        )
    );
  }
}           

以一个简单的例子来说明,怎么通过 Image,反查引用链,定位到是 ImageTestWidget 对其引用。这里的 ImagetTestWidget 就是我们的目标,从这里就能看出 Image 被哪个页面持有/泄漏。

思路是从 Image 开始反向遍历 Widget tree,定位到 ImageTestWidget。期望的引用链如下:

Flutter Image内存--强引用分析方法概述分析总结

Image 引用链

Flutter Image内存--强引用分析方法概述分析总结

Observatory 会将 instance 到 gc Root 的引用链显示在 Retained path 下。如图中红圈1所示,Image 的 Retained path 只显示 image 对象被 weak persistent handle(参考

Flutter内存分析

描述,这是一种 GC Root,gc时会被遍历,如果 dart对象 被回收,其关联的 external 内存会得到释放)直接持有,但根据这个信息显然是无法定位到我们的目标 ImageTestWidget。

为什么 Observatory 显示 Image 的引用链与上面预期的不一样?

原来这里的 Image 与我们代码中的 Image 并不是同一个类。

part of dart.ui;

class ImageInfo {
  /// Creates an [ImageInfo] object for the given [image] and [scale].
  ///
  /// Both the image and the scale must not be null.
  ///
  /// The tag may be used to identify the source of this image.
  const ImageInfo({ @required this.image, this.scale = 1.0, this.debugLabel })
    : assert(image != null),
      assert(scale != null);

  /// The raw image pixels.
  ///
  /// This is the object to pass to the [Canvas.drawImage],
  /// [Canvas.drawImageRect], or [Canvas.drawImageNine] methods when painting
  /// the image.
  final ui.Image image;
  ...
}

// Observatory 中显示的 Image 对象
class Image extends NativeFieldWrapperClass2 {
  // This class is created by the engine, and should not be instantiated
  // or extended directly.
  //
  // To obtain an [Image] object, use [instantiateImageCodec].
  @pragma('vm:entry-point')
  Image._();
  ...
  /// Returns an error message on failure, null on success.
  String? _toByteData(int format, _Callback<Uint8List?> callback) native 'Image_toByteData';

  /// Release the resources used by this object. The object is no longer usable
  /// after this method is called.
  void dispose() native 'Image_dispose';
  ...
 }           

代码中直接用 Image.network() 的 Iamge:

// 业务代码中 Image 对象
class Image extends StatefulWidget {
  ...
  Image.network();
  ...
}

class _ImageState extends State<Image> with WidgetsBindingObserver {
  @override
  Widget build(BuildContext context) {
    ...

    Widget result = RawImage(
      image: _imageInfo?.image,
      ...
    );
    ...
  }
}
           

从上面代码片段可知,Observatory 中显示的 image 是 ui.Image。分析图片解码过程可知,这个是engine中解码后的 CanvasImage(真正占用内存的地方) 在 dart 层的表示,并通过 weak persistent handle 关联起来。这个 ui.Image会至少被3个对象持有,上图圈2中:

  • ImageInfo : 解码完成后保存 ui.Image
  • RawImage : 持有 ui.Image 的 widget
  • RenderImage : 持有 ui.Image 的 renderObject

结合 _ImageState 的 build() 方法,可知 RawIamge 才是我们用 Image.network() 生成在 Widget tree 上的对象,所以我们应该从 RawImage 开始查找。

RawImage 引用链

Flutter Image内存--强引用分析方法概述分析总结

通过 Inbounce references 查看反向引用。可见引用 RawImage 有2个分支(实验场景简单,实际场景会有更多引用路径),哪个分支的路径能够到达 ImageTestWidget 呢?

这里先简单梳理下 Flutter Widget tree,Element tree 的关系:

Flutter Image内存--强引用分析方法概述分析总结

从主要类关系看到,Widget 与 Element 具有一一对应关系。

两棵树生成的过程:

  1. runApp() 时候 attachRootWidget(Widget rootWidget),root Widget 为 RenderObjectToWidgetAdapter,root Element 为 RenderObjectToWidgetElement,其 child 对应 RenderObjectToWidgetAdapter.child,即我们的app代码
  2. mount 时候会递归将 child / children 对应的 element 构建出来,并挂载到 Element tree
  3. 最后想成一棵 Element tree。

这里需要注意到是,Widget 家族是没有 child 这个成员,而 Element 家族有, 真正具有引用关系的那棵树是 Element tree,Widget tree 概念上成立,但对象间并无直接引用但关系。

业务代码通常不会直接去写 Element,Element是根据我们定义的 Widget 生成的,总之 , 反向引用链 查找的方法是:

  1. 找到第一个 Widget 对应的 Element
  2. 从这个 Element 开始反向遍历 Element 树,通过 _parent, _child 属性可遍历一条完整的引用支路
  3. Element 中 _widget, _state 可帮我们确定某个节点是否是我们目标到 Widget,例如例子中的 ImageTestWidget

下面回答上面 “哪个才是正确的引用路径” 的问题。

RawImage 对应的 element 是 LeafRenderObjectElement,其 _widget 引用了 RawImage。接着从 LeafRenderObjectElement 反向遍历 Element tree,跟踪 _child 属性引用自己的分支展开即可定位到 ImageTestWidget。如下展示了从 ui.Image 到 ImageTestWidget的完整引用链:

Flutter Image内存--强引用分析方法概述分析总结

BuildContext 泄漏

通过上面的操作已经可以定位到强引用 Image 的业务代码 Widget,也可以定位其归属的页面。根据页面是否关闭,则可确定是否泄漏。如果是泄漏,那是什么导致了业务 Widget 没有被系统回收呢?

Dart_NotifyIdle() 的逻辑可看出,Flutter gc机制是可以应付频繁创建/销毁 Widget 的这种操作的 ,它会尽量在每一帧绘制完都去执行 gc 操作,回收掉销毁的 Widget。

每一帧画完后,在 finalizeTree() 中会对 inactive elements 进行 unmount 操作

@override
  void drawFrame() {
    ...
    assert(!debugBuildingDirtyElements);
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      // 回收 elements 
      buildOwner.finalizeTree();
    } finally {
      assert(() {
        debugBuildingDirtyElements = false;
        return true;
      }());
    }
    ...
  }           

elements 被 unmount 之后,脱离 element 树,连同被它引用的 widget, state 都会后面的被 gc 回收。

Flutter Image内存--强引用分析方法概述分析总结

总之,Widget 是在 Element 回收后,关联被回收的。同样的,如果 Element 没有被回收,那被它关联的 Widget,State 都不会被回收。Element 泄漏的概率比较高,因为 Element 继承了 BuildContext,业务代码会经常传递这个参数。特别是 异步执行 的代码的场景(Feature, async/await,methodChannel),这些代码可能会长期持有传入的BuildContext,导致 element 以及关联的 widget, state 发生泄漏。

追踪具体泄漏 BuildContext 的代码,可以继续从这个 Element 反向引用继续查找,特别留意 Clouse, Completer 之类对其的引用。

总结

本文主要提炼了一种基于 Observatory 工具,从 Image 反向引用定位到业务 Widget 的方法,用于解决 Image 强引用的问题。而造成 Image 强引用的根本原因很大可能是 BuildContext 被异步代码长期持有导致。