内存视图可提供有关应用程序内存分配的详细信息,并提供检测和调试特定问题的工具。

有关如何在不同 IDE 中找到 DevTools 界面的信息,请参阅 DevTools 概述

为了更好地理解本页内容,第一部分解释了 Dart 如何管理内存。如果您已了解 Dart 的内存管理,可以直接跳至 内存视图指南

使用内存视图的原因

#

当您的应用程序出现以下任一情况时,请使用内存视图进行预防性内存优化,或用于调试此类问题:

  • 运行时因内存不足而崩溃
  • 运行缓慢
  • 导致设备变慢或无响应
  • 因超出操作系统强制执行的内存限制而被关闭
  • 超出内存使用限制
    • 此限制可能因您应用的目标设备类型而异。
  • 怀疑发生内存泄漏

基本内存概念

#

使用类构造函数(例如,通过使用 MyClass())创建的 Dart 对象存在于称为堆(heap)的内存区域中。堆中的内存由 Dart 虚拟机 (VM) 管理。Dart VM 在对象创建时为其分配内存,并在对象不再使用时释放(或取消分配)内存(请参阅 Dart 垃圾回收)。

对象类型

#

可Dispose对象

#

可Dispose对象是任何定义了 dispose() 方法的 Dart 对象。为避免内存泄漏,请在不再需要对象时调用 dispose

内存风险对象

#

内存风险对象是指可能导致内存泄漏的对象,如果它未被正确 dispose 或被 dispose 但未被 GC(垃圾回收)。

根对象、保留路径和可达性

#

根对象

#

每个 Dart 应用程序都会创建一个根对象,该对象直接或间接地引用应用程序分配的所有其他对象。

可达性

#

如果在应用程序运行的某个时刻,根对象不再引用已分配的对象,该对象就变得不可达,这是垃圾回收器 (GC) 取消分配对象内存的信号。

保留路径

#

从根到对象的引用序列称为对象的保留路径,因为它使对象内存免受垃圾回收。一个对象可以有多个保留路径。至少有一条保留路径的对象称为可达对象。

示例

#

以下示例说明了这些概念

dart
class Child{}

class Parent {
  Child? child;
}

Parent parent1 = Parent();

void myFunction() {

  Child? child = Child();

  // The `child` object was allocated in memory.
  // It's now retained from garbage collection
  // by one retaining path (root …-> myFunction -> child).

  Parent? parent2 = Parent()..child = child;
  parent1.child = child;

  // At this point the `child` object has three retaining paths:
  // root …-> myFunction -> child
  // root …-> myFunction -> parent2 -> child
  // root -> parent1 -> child

  child = null;
  parent1.child = null;
  parent2 = null;

  // At this point, the `child` instance is unreachable
  // and will eventually be garbage collected.


}

浅大小 vs 保留大小

#

浅大小仅包括对象本身及其引用的内存大小,而保留大小还包括被保留对象的大小。

根对象的保留大小包括所有可达的 Dart 对象。

在以下示例中,myHugeInstance 的大小不计入父对象或子对象的浅大小,但计入它们的保留大小。

dart
class Child{
  /// The instance is part of both [parent] and [parent.child]
  /// retained sizes.
  final myHugeInstance = MyHugeInstance();
}

class Parent {
  Child? child;
}

Parent parent = Parent()..child = Child();

在 DevTools 的计算中,如果一个对象有多条保留路径,它的内存大小仅分配给最短保留路径的成员。

在此示例中,对象 x 有两条保留路径。

root -> a -> b -> c -> x
root -> d -> e -> x (shortest retaining path to `x`)

只有最短路径(de)的成员才会将 x 的大小计入它们的保留大小。

Dart 中会发生内存泄漏吗?

#

垃圾回收器无法防止所有类型的内存泄漏,开发者仍需关注对象的生命周期以避免泄漏。

为什么垃圾回收器无法防止所有泄漏?

#

虽然垃圾回收器会处理所有不可达对象,但应用程序有责任确保不再需要使用的对象不再可达(即不再被引用)。

因此,如果未使用的对象仍然被引用(例如,在全局变量、静态变量或长生命周期对象的字段中),垃圾回收器将无法识别它们,内存分配将逐渐增加,最终导致应用程序因 out-of-memory 错误而崩溃。

为什么闭包需要特别注意

#

一种难以捕捉的泄漏模式与使用闭包有关。在以下代码中,对设计为短生命周期的 myHugeObject 的引用被隐式存储在闭包上下文中并传递给 setHandler。结果是,只要 handler 可达,myHugeObject 就不会被垃圾回收。

dart
  final handler = () => print(myHugeObject.name);
  setHandler(handler);

为什么 BuildContext 需要特别注意

#

一个可能挤入长生命周期区域并因此导致泄漏的大型短生命周期对象的示例是传递给 Flutter 的 build 方法的 context 参数。

以下代码容易发生泄漏,因为 useHandler 可能会将 handler 存储在长生命周期区域。

dart
// BAD: DO NOT DO THIS
// This code is leak prone:
@override
Widget build(BuildContext context) {
  final handler = () => apply(Theme.of(context));
  useHandler(handler);

如何修复容易泄漏的代码?

#

以下代码不易发生泄漏,因为:

  1. 闭包不使用大型短生命周期的 context 对象。
  2. (代替使用的)theme 对象是长生命周期的。它只创建一次,并在 BuildContext 实例之间共享。
dart
// GOOD
@override
Widget build(BuildContext context) {
  final theme = Theme.of(context);
  final handler = () => apply(theme);
  useHandler(handler);

BuildContext 的一般规则

#

总的来说,对于 BuildContext,请遵循以下规则:如果闭包的生命周期不长于 widget,那么将 context 传递给闭包是可以的。

StatefulWidget 需要特别注意。它们由两个类组成:widgetwidget state,其中 widget 的生命周期较短,而 state 的生命周期较长。widget 所拥有的 build context 绝不应从 state 的字段中引用,因为 state 不会随 widget 一起被垃圾回收,并且其生命周期可能远远长于 widget。

内存泄漏 vs 内存膨胀

#

在内存泄漏中,应用程序会逐渐使用内存,例如,通过反复创建监听器但不 dispose 它。

内存膨胀是指使用的内存超过了最佳性能所需的内存,例如,使用过大的图像或在流的整个生命周期中保持打开状态。

无论是泄漏还是膨胀,当规模很大时,都会导致应用程序因 out-of-memory 错误而崩溃。然而,泄漏更有可能导致内存问题,因为即使是很小的泄漏,如果重复多次,也会导致崩溃。

内存视图指南

#

DevTools 的内存视图可以帮助您调查内存分配(包括堆内和堆外内存)、内存泄漏、内存膨胀等。该视图具有以下功能:

可展开图表
获取内存分配的高层跟踪,并查看标准事件(如垃圾回收)和自定义事件(如图像分配)。
内存分析(Profile Memory)选项卡
按类和内存类型查看当前的内存分配。
差异快照(Diff Snapshots)选项卡
检测和调查功能的内存管理问题。
实例跟踪(Trace Instances)选项卡
为指定的一组类调查功能的内存管理。

可展开图表

#

可展开图表提供以下功能:

内存解剖

#

时间序列图可视化 Flutter 内存随时间推移的状态。图表上的每个数据点对应于时间戳(x 轴)上堆的测量值(y 轴)。例如,会捕获使用量、容量、外部内存、垃圾回收和常驻集大小。

Screenshot of a memory anatomy page

内存概览图

#

内存概览图是一个已收集内存统计信息的时间序列图。它直观地展示了 Dart 或 Flutter 堆以及 Dart 或 Flutter 的原生内存随时间的变化情况。

图表的 x 轴是事件(时间序列)的时间线。y 轴上的数据都具有数据收集的时间戳。换句话说,它每 500 毫秒显示一次内存的轮询状态(容量、已使用、外部、RSS(常驻集大小)和 GC(垃圾回收))。这有助于在应用程序运行时提供内存状态的实时视图。

单击图例(Legend)按钮会显示收集到的测量值、符号和用于显示数据的颜色。

Screenshot of a memory anatomy page

y 轴上的内存大小刻度(Memory Size Scale)会自动调整以适应当前可见图表范围内收集的数据。

y 轴上绘制的数量如下:

Dart/Flutter 堆
堆中的对象(Dart 和 Flutter 对象)。
Dart/Flutter 原生内存
不属于 Dart/Flutter 堆但仍构成总内存占用的内存。此内存中的对象将是原生对象(例如,从内存中读取文件,或解码的图像)。原生对象通过 Dart 嵌入器从原生操作系统(如 Android、Linux、Windows、iOS)暴露给 Dart VM。嵌入器会创建一个带有 finalizer 的 Dart 包装器,允许 Dart 代码与这些原生资源进行通信。Flutter 具有 Android 和 iOS 的嵌入器。有关更多信息,请参阅 命令行和服务器应用使用 Dart Frog 进行服务器端 Dart 开发自定义 Flutter 引擎嵌入器使用 Heroku 进行 Dart Web 服务器部署
时间线
在特定时间点(时间戳)收集的所有内存统计信息和事件的时间戳。
光栅缓存 (Raster Cache)
Flutter 引擎的光栅缓存层或图像在组合后的最终渲染过程中所占的大小。有关更多信息,请参阅 Flutter 架构概述DevTools 性能视图
已分配 (Allocated)
堆的当前容量通常略大于所有堆对象的大小之和。
RSS - 常驻集大小 (Resident Set Size)
常驻集大小显示进程的内存量。它不包括已交换出去的内存。它包括已加载的共享库的内存,以及所有栈和堆内存。有关更多信息,请参阅 Dart VM 内部机制

内存分析(Profile Memory)选项卡

#

使用内存分析(Profile Memory)选项卡按类和内存类型查看当前内存分配。要深入分析,可以下载 CSV 格式的数据到 Google Sheets 或其他工具中。切换GC 时刷新(Refresh on GC),可以实时查看分配情况。

Screenshot of the profile tab page

差异快照(Diff Snapshots)选项卡

#

使用差异快照(Diff Snapshots)选项卡来调查功能的内存管理。按照选项卡上的指南,在与应用程序交互之前和之后拍摄快照,然后进行差异比较。

Screenshot of the diff tab page

点击过滤类和包(Filter classes and packages)按钮,缩小数据范围。

Screenshot of the filter options ui

要深入分析,可以下载 CSV 格式的数据到 Google Sheets 或其他工具中。

实例跟踪(Trace Instances)选项卡

#

使用实例跟踪(Trace Instances)选项卡来调查在功能执行期间,哪些方法为一组类分配了内存。

  1. 选择要跟踪的类
  2. 与您的应用进行交互,以触发您感兴趣的代码
  3. 点击刷新(Refresh)
  4. 选择一个已跟踪的类
  5. 查看收集到的数据

Screenshot of a trace tab

自底向上 vs 调用树视图

#

根据任务的特定需求,在自底向上和调用树视图之间切换。

Screenshot of a trace allocations

调用树视图显示每个实例的方法分配。该视图是对调用堆栈的自顶向下表示,意味着可以展开一个方法来显示其调用的子方法。

自底向上视图显示已分配实例的各种调用堆栈列表。

其他资源

#

更多信息,请参阅以下资源: