Skip to main content

使用内存视图

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

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

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

使用内存视图的原因

#

在进行预防性内存优化或应用程序遇到以下情况之一时,使用内存视图:

  • 运行时内存不足导致崩溃
  • 速度变慢
  • 导致设备速度变慢或无响应
  • 由于超过操作系统强制执行的内存限制而关闭
  • 超过内存使用限制
    • 此限制会根据您的应用目标设备类型而有所不同。
  • 怀疑内存泄漏

基本内存概念

#

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

对象类型

#

可处置对象

#

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

内存风险对象

#

内存风险对象是指如果未正确处置或已处置但未被 GC 回收,可能 会导致内存泄漏的对象。

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

#

根对象

#

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

可达性

#

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

保留路径

#

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

示例

#

以下示例说明了这些概念:

dart
class Child{}

class Parent {
  Child? child;
}

Parent parent1 = Parent();

void myFunction() {

  Child? child = Child();

  // `child` 对象已在内存中分配。
  // 它现在通过一条保留路径 (root …-> myFunction -> child) 保留免受垃圾回收。

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

  // 此时,`child` 对象有三条保留路径:
  // root …-> myFunction -> child
  // root …-> myFunction -> parent2 -> child
  // root -> parent1 -> child

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

  // 此时,`child` 实例不可达,最终将被垃圾回收。


}

浅层大小与保留大小

#

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

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

在下面的示例中,myHugeInstance 的大小不属于父级或子级的浅层大小的一部分,而是它们保留大小的一部分:

dart
class Child{
  /// 此实例是 [parent][parent.child] 保留大小的一部分。
  final myHugeInstance = MyHugeInstance();
}

class Parent {
  Child? child;
}

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

在 DevTools 计算中,如果一个对象有多个保留路径,则其大小仅分配给最短保留路径的成员作为保留大小。

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

root -> a -> b -> c -> x
root -> d -> e -> x (到 `x` 的最短保留路径)

只有最短路径的成员(de)会将其保留大小包含 x

Dart 中会发生内存泄漏吗?

#

垃圾回收器无法防止所有类型的内存泄漏,开发人员仍然需要监视对象以确保其生命周期没有泄漏。

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

#

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

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

为什么闭包需要额外注意

#

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

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

为什么 BuildContext 需要额外注意

#

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

以下代码容易出现泄漏,因为 useHandler 可能会将处理程序存储在长生命周期区域中:

dart
// 错误:不要这样做
// 此代码容易出现内存泄漏:
@override
Widget build(BuildContext context) {
  final handler = () => apply(Theme.of(context));
  useHandler(handler);

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

#

以下代码不易出现泄漏,因为:

  1. 闭包不使用大型且短生命周期的 context 对象。
  2. 使用的 theme 对象(替代)是长生命周期的。它只创建一次,并在 BuildContext 实例之间共享。
dart
// 正确
@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 轴)。例如,捕获使用情况、容量、外部、垃圾回收和驻留集大小。

内存结构页面的屏幕截图

内存概述图表

#

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

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

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

内存结构页面的屏幕截图

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

y 轴上绘制的数量如下:

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

内存分析选项卡

#

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

内存分析选项卡页面的屏幕截图

差异快照选项卡

#

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

差异选项卡页面的屏幕截图

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

过滤选项 UI 的屏幕截图

要进行更深入的 Google Sheets 或其他工具分析,请下载 CSV 格式的数据。

跟踪实例选项卡

#

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

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

跟踪选项卡的屏幕截图

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

#

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

跟踪分配的屏幕截图

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

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

其他资源

#

有关更多信息,请查看以下资源: