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,
        )
    );
  }
}

以一个简单的例子来说明,怎么通过 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';
  ...
 }

代码中直接用 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 引用链

通过 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;
      }());
    }
    ...
  }

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

目录
相关文章
|
3月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
243 3
|
2月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
137 4
AI代理内存消耗过大?9种优化策略对比分析
|
8月前
|
存储 安全 iOS开发
内存卡怎么格式化?6个格式化方法供你选
随着使用时间的增加,内存卡可能会因为数据积累、兼容性或是文件系统损坏等原因需要进行格式化。那么怎样正确格式化内存卡呢?格式化内存卡的时候需要注意什么呢?本文会给大家提供详细的步骤,帮助大家轻松完成格式化内存卡的操作。
|
3月前
|
存储 Windows
内存卡坏了还能修吗?4种常见修复方法
内存卡出现“无法保存”或“存储异常”等问题时,不一定是硬件损坏,可能是系统错误或文件系统异常导致。本文介绍几种亲测有效的修复方法:1) 更换读卡设备排除接触问题;2) 格式化修复文件系统(需先备份数据);3) 使用DiskGenius检测坏道;4) 借助厂商工具深度修复。同时提供日常保养建议,如避免高温环境、养成数据备份习惯,延长内存卡使用寿命。通过这些方法,多数问题可轻松解决,无需更换硬件。
|
5月前
|
数据采集 开发工具 Android开发
ClkLog埋点分析系统-Flutter埋点上报攻略
近期,不少社群里的伙伴有Flutter的集成需求,为了让大家能更快、更顺利地完成集成,我们实现了本次demo给大家作为参考。 目前,我们已为主流的第三方框架提供了相应的集成demo,如果您还有其他SDK的验证需求欢迎联系小秘书,我们会尽量给大家提供实现demo。
ClkLog埋点分析系统-Flutter埋点上报攻略
|
6月前
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
|
6月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
126 2
|
10月前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
319 62
|
10月前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
229 6
|
10月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
245 1