内存视图提供有关应用程序内存分配细节的洞察,以及检测和调试特定问题的工具。

有关如何在不同 IDE 中查找 DevTools 屏幕的信息,请查阅DevTools 概览

为了更好地理解本页面上的洞察,第一部分解释了 Dart 如何管理内存。如果您已经了解 Dart 的内存管理,可以跳到内存视图指南

使用内存视图的原因

#

使用内存视图进行预防性内存优化,或者当您的应用程序出现以下任一情况时:

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

基本内存概念

#

使用类构造函数(例如,通过使用 MyClass())创建的 Dart 对象存储在内存中一个名为 堆 (heap) 的区域。堆中的内存由 Dart VM(虚拟机)管理。Dart VM 在对象创建时为对象分配内存,并在对象不再使用时释放(或解除分配)内存(参见Dart 垃圾回收)。

对象类型

#

可释放对象

#

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

内存风险对象

#

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

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

#

根对象

#

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

可达性

#

如果在应用程序运行的某个时刻,根对象停止引用一个已分配对象,该对象将变为 不可达 (unreachable),这是垃圾回收器 (GC) 释放对象内存的信号。

保留路径

#

从根对象到某个对象的引用序列称为该对象的 保留路径 (retaining path),因为它保留了对象的内存,使其不被垃圾回收。一个对象可以有多个保留路径。具有至少一个保留路径的对象称为 可达 (reachable) 对象。

示例

#

以下示例说明了这些概念

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.


}

浅层大小与保留大小

#

浅层大小 (Shallow size) 仅包括对象及其引用的大小,而 保留大小 (retained size) 还包括被保留对象的大小。

根对象的 保留大小 包括所有可达的 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 中会发生内存泄漏吗?

#

垃圾回收器无法阻止所有类型的内存泄漏,开发人员仍然需要关注对象以实现无泄漏的生命周期。

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

#

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

因此,如果不再需要的对象仍被引用(在全局或静态变量中,或作为长生命周期对象的字段),垃圾回收器将无法识别它们,内存分配会逐渐增长,最终导致应用程序因 内存不足 错误而崩溃。

为什么闭包需要额外注意

#

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

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

为什么 BuildContext 需要额外注意

#

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

以下代码容易导致内存泄漏,因为 useHandler 可能会将处理器存储在长生命周期区域中:

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 传递给闭包。

有状态组件 (Stateful widget) 需要额外注意。它们由两个类组成:widget 及其状态,其中 widget 是短生命周期的,状态是长生命周期的。widget 所拥有的构建上下文 (build context) 绝不应从状态的字段中引用,因为状态不会与 widget 一起被垃圾回收,并且其生命周期可能远远长于 widget。

内存泄漏与内存膨胀

#

在内存泄漏中,应用程序会逐步占用内存,例如,通过重复创建侦听器而不对其进行释放。

内存膨胀 (Memory bloat) 使用的内存超出最佳性能所需,例如,使用过大的图像或在整个生命周期内保持流打开。

当泄漏和膨胀量很大时,都会导致应用程序因 内存不足 错误而崩溃。然而,泄漏更容易导致内存问题,因为即使是微小的泄漏,如果重复多次,也会导致应用程序崩溃。

内存视图指南

#

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

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

可展开图表

#

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

内存剖析

#

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

Screenshot of a memory anatomy page

内存概览图

#

内存概览图是收集到的内存统计数据的时序图。它以视觉方式呈现 Dart 或 Flutter 堆以及 Dart 或 Flutter 原生内存随时间变化的状态。

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

点击 图例 按钮,显示用于显示数据的收集测量值、符号和颜色。

Screenshot of a memory anatomy page

内存大小比例 y 轴会自动调整到当前可见图表范围内收集的数据范围。

y 轴上绘制的数量如下:

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

分析内存 (Profile Memory) 选项卡

#

使用 分析内存 (Profile Memory) 选项卡按类和内存类型查看当前内存分配。为了在 Google 表格或其他工具中进行更深入的分析,请以 CSV 格式下载数据。切换 在 GC 时刷新,以实时查看分配情况。

Screenshot of the profile tab page

快照对比 (Diff Snapshots) 选项卡

#

使用 快照对比 (Diff Snapshots) 选项卡调查某个功能的内存管理。按照选项卡上的指导,在与应用程序交互前后拍摄快照,并对比这些快照。

Screenshot of the diff tab page

点击 过滤类和包 按钮,以缩小数据范围。

Screenshot of the filter options ui

为了在 Google 表格或其他工具中进行更深入的分析,请以 CSV 格式下载数据。

跟踪实例 (Trace Instances) 选项卡

#

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

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

Screenshot of a trace tab

自下而上视图与调用树视图

#

根据您的具体任务,在自下而上视图和调用树视图之间切换。

Screenshot of a trace allocations

调用树视图显示每个实例的方法分配。该视图是调用栈的自上而下表示,这意味着一个方法可以展开以显示其被调用者。

自下而上视图显示了已分配这些实例的不同调用栈列表。

其他资源

#

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