Flutter Image内存--强引用分析方法

简介: 概述 据了解,很多Flutter业务上线后都出现内存占用较高的问题,首当其冲的是 Image 内存占用过多。 Image 图片内存过高,可能由于 Flutter ImageCache 对内存缺房控制力导致,也有可能是被业务代码强引用,泄漏导致。如果 Image 被业务强引用,则调整 ImageCache 容量,增加 gc 次数都没有效果。 面对这种“强引用”的泄漏

概述

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

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

面对这种“强引用”的泄漏,需要定位到引用 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,
        )
    );
  }
}
AI 代码解读

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

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

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';
  ...
 }
AI 代码解读

代码中直接用 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,
      ...
    );
    ...
  }
}
AI 代码解读

 

从上面代码片段可知,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 引用链

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

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

从主要类关系看到,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的完整引用链:

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;
      }());
    }
    ...
  }
AI 代码解读

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

总之,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 被异步代码长期持有导致。

目录
相关文章
除了permission_handler插件,还有哪些方法可以实现Flutter动态申请权限?
除了permission_handler插件,还有哪些方法可以实现Flutter动态申请权限?
172 68
内存卡怎么格式化?6个格式化方法供你选
随着使用时间的增加,内存卡可能会因为数据积累、兼容性或是文件系统损坏等原因需要进行格式化。那么怎样正确格式化内存卡呢?格式化内存卡的时候需要注意什么呢?本文会给大家提供详细的步骤,帮助大家轻松完成格式化内存卡的操作。
内存卡坏了还能修吗?4种常见修复方法
内存卡出现“无法保存”或“存储异常”等问题时,不一定是硬件损坏,可能是系统错误或文件系统异常导致。本文介绍几种亲测有效的修复方法:1) 更换读卡设备排除接触问题;2) 格式化修复文件系统(需先备份数据);3) 使用DiskGenius检测坏道;4) 借助厂商工具深度修复。同时提供日常保养建议,如避免高温环境、养成数据备份习惯,延长内存卡使用寿命。通过这些方法,多数问题可轻松解决,无需更换硬件。
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
|
8月前
|
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
223 62
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
480 62
Flutter框架中的插件市场及开源资源的利用方法。内容涵盖插件市场的扩展功能、时间节省与质量保证
本文深入探讨了Flutter框架中的插件市场及开源资源的利用方法。内容涵盖插件市场的扩展功能、时间节省与质量保证,常见插件市场的介绍,选择合适插件的策略,以及开源资源的利用价值与注意事项。通过案例分析和对社区影响的讨论,展示了这些资源如何促进开发效率和技术进步,并展望了未来的发展趋势。
173 11
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
182 6
Flutter 框架提供了丰富的机制和方法来优化键盘处理和输入框体验
在移动应用开发中,Flutter 框架提供了丰富的机制和方法来优化键盘处理和输入框体验。本文深入探讨了键盘的显示与隐藏、输入框的焦点管理、键盘类型的适配、输入框高度自适应、键盘遮挡问题处理及性能优化等关键技术,结合实例分析,旨在帮助开发者提升应用的用户体验。
306 6
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问