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

目录
相关文章
|
10天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
49 1
|
5天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
15天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
103 9
|
20天前
|
机器学习/深度学习 算法 物联网
大模型进阶微调篇(一):以定制化3B模型为例,各种微调方法对比-选LoRA还是PPO,所需显存内存资源为多少?
本文介绍了两种大模型微调方法——LoRA(低秩适应)和PPO(近端策略优化)。LoRA通过引入低秩矩阵微调部分权重,适合资源受限环境,具有资源节省和训练速度快的优势,适用于监督学习和简单交互场景。PPO基于策略优化,适合需要用户交互反馈的场景,能够适应复杂反馈并动态调整策略,适用于强化学习和复杂用户交互。文章还对比了两者的资源消耗和适用数据规模,帮助读者根据具体需求选择最合适的微调策略。
|
19天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
19天前
|
缓存 监控 Java
在使用 Glide 加载 Gif 动画时避免内存泄漏的方法
【10月更文挑战第20天】在使用 Glide 加载 Gif 动画时,避免内存泄漏是非常重要的。通过及时取消加载请求、正确处理生命周期、使用弱引用、清理缓存和避免重复加载等方法,可以有效地避免内存泄漏问题。同时,定期进行监控和检测,确保应用的性能和稳定性。需要在实际开发中不断积累经验,根据具体情况灵活运用这些方法,以保障应用的良好运行。
|
1月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
45 2
|
13天前
|
缓存 JavaScript API
Flutter&鸿蒙next 状态管理框架对比分析
在 Flutter 开发中,状态管理至关重要,直接影响应用的性能和可维护性。本文对比分析了常见的状态管理框架,包括 setState()、InheritedWidget、Provider、Riverpod、Bloc 和 GetX,详细介绍了它们的优缺点及适用场景,并提供了 Provider 的示例代码。选择合适的状态管理框架需考虑应用复杂度、团队熟悉程度和性能要求。
81 0
|
1月前
|
开发者
flutter:总结所有需要用到的方法与实战 (十六)
本文介绍了Flutter中路由和顶部导航的使用方法,包括简单路由、命名路由、返回及返回根路由的实现。同时,详细讲解了顶部导航的定义与属性设置,并通过实战案例展示了复杂布局、新闻列表和页面制作的思路。最后,还提供了父类向子类传递参数的方法以及如何添加依赖库。