使用内存视图

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

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

为了更好地理解本页中找到的见解,第一部分解释了 Dart 如何管理内存。如果您已经了解 Dart 的内存管理,则可以跳至内存视图指南

使用内存视图的原因

在进行先发制人的内存优化时或当您的应用程序遇到以下情况之一时,请使用内存视图

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

基本内存概念

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

对象类型

可处置对象

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

内存风险对象

内存风险对象是一个可能导致内存泄漏的对象,如果它未正确处置或处置但未进行 GC。

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

根对象

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

可达性

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

保留路径

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

示例

以下示例说明了这些概念

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.

  
}

浅层大小与保留大小

浅层大小仅包括对象及其引用的大小,而保留大小还包括保留对象的大小。

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

在以下示例中,myHugeInstance 的大小不属于父级或子级的浅层大小,但属于其保留大小

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 就不会被垃圾回收。

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

为什么 BuildContext 需要特别注意

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

以下代码容易泄漏,因为 useHandler 可能将处理程序存储在长期存在区域

// 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 实例之间共享。
// GOOD
@override
Widget build(BuildContext context) {
  final theme = Theme.of(context);
  final handler = () => apply(theme);
  useHandler(handler);

BuildContext 的一般规则

通常,对 BuildContext 使用以下规则:如果闭包的生存期不超过小部件,则可以将上下文传递给闭包。

有状态小部件需要特别注意。它们由两个类组成:小部件和小部件状态,其中小部件的生存期短,而状态的生存期长。由小部件拥有的构建上下文绝不应从状态的字段中引用,因为状态不会与小部件一起被垃圾回收,并且其生存期可能大大超过小部件。

内存泄漏与内存膨胀

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

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

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

内存视图指南

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

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

可扩展图表

可扩展图表提供以下功能

内存结构

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

Screenshot of a memory anatomy page

内存概览图表

内存概览图表是收集的内存统计信息的时间序列图。它直观地展示了 Dart 或 Flutter 堆以及 Dart 或 Flutter 本机内存随时间推移的状态。

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

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

Screenshot of a memory anatomy page

内存大小刻度y 轴会根据当前可见图表范围中收集的数据范围自动调整。

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 服务器
时间线
在特定时间点(timestamp)收集的所有内存统计信息和事件的 timestamp。
光栅缓存
在合成后执行最终渲染时,Flutter 引擎光栅缓存层或图片的大小。有关更多信息,请参阅Flutter 架构概览DevTools 性能视图
已分配
堆的当前容量通常略大于所有堆对象的总大小。
RSS - 驻留集大小
驻留集大小显示进程的内存量。它不包括已换出的内存。它包括已加载的共享库的内存以及所有堆栈和堆内存。有关更多信息,请参阅Dart VM 内部

分析内存选项卡

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

Screenshot of the profile tab page

差异快照选项卡

使用差异快照选项卡调查功能的内存管理。按照选项卡中的指南在与应用程序交互之前和之后拍摄快照,然后对快照进行差异化处理

Screenshot of the diff tab page

点击筛选类和包按钮,缩小数据范围

Screenshot of the filter options ui

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

跟踪实例选项卡

使用跟踪实例选项卡调查在功能执行期间为一组类分配内存的方法

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

Screenshot of a trace tab

自下而上与调用树视图

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

Screenshot of a trace allocations

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

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

其他资源

如需了解更多信息,请查看以下资源