使用 Memory(内存)视图
了解如何使用 DevTools 内存视图。
内存视图提供了对应用程序内存分配细节的洞察,以及用于检测和调试特定问题的工具。
有关如何在不同 IDE 中定位 DevTools 屏幕的信息,请查看 DevTools 概览。
为了更好地理解本页面中的见解,第一部分解释了 Dart 如何管理内存。如果你已经了解 Dart 的内存管理,可以直接跳转到 内存视图指南。
使用内存视图的理由
#在进行预防性内存优化,或应用程序遇到以下任一情况时,请使用内存视图:
- 因内存不足而崩溃
- 运行速度变慢
- 导致设备变慢或无响应
- 因超过操作系统强制执行的内存限制而被关闭
- 超过内存使用限制
- 此限制可能因应用目标设备的类型而异。
- 怀疑存在内存泄漏
内存基本概念
#使用类构造函数(例如使用 MyClass())创建的 Dart 对象存在于内存的一个部分,称为堆(heap)。堆中的内存由 Dart VM(虚拟机)管理。Dart VM 在对象创建时为其分配内存,并在对象不再被使用时释放(或撤销分配)内存(参见 Dart 垃圾回收)。
对象类型
#可处置对象 (Disposable object)
#可处置对象是定义了 dispose() 方法的任何 Dart 对象。为避免内存泄漏,当对象不再需要时,请调用 dispose。
内存风险对象 (Memory-risky object)
#内存风险对象是指如果未正确处置,或者虽已处置但未被 GC(垃圾回收)回收,可能导致内存泄漏的对象。
根对象、保留路径和可达性
#根对象 (Root object)
#每个 Dart 应用程序都会创建一个根对象,它直接或间接地引用应用程序分配的所有其他对象。
可达性 (Reachability)
#如果在应用程序运行的某个时刻,根对象停止引用已分配的对象,该对象就变得不可达,这是向垃圾回收器 (GC) 发出的释放该对象内存的信号。
保留路径 (Retaining path)
#从根到对象的引用序列称为该对象的保留路径,因为它将对象的内存从垃圾回收中保留下来。一个对象可以有许多保留路径。至少有一个保留路径的对象称为可达对象。
示例
#以下示例阐述了这些概念
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 保留大小
#浅层大小 (Shallow size) 仅包含对象及其引用的自身大小,而 保留大小 (Retained size) 还包括所保留对象的大小。
根对象的 保留大小 包括所有可达的 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`)
只有最短路径的成员(d 和 e)会将 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);
…
如何修复易泄漏的代码?
#以下代码不容易发生泄漏,因为:
- 闭包没有使用大型且短生命周期的
context对象。 - (改用的)
theme对象是长生命周期的。它被创建一次并在BuildContext实例之间共享。
// GOOD
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final handler = () => apply(theme);
useHandler(handler);
…
关于 BuildContext 的一般规则
#
通常,请对 BuildContext 遵循以下规则:如果闭包的生命周期不会超过组件(widget),那么将 context 传递给闭包是可以的。
StatefulWidget 需要额外注意。它们由两个类组成:组件和组件状态,其中组件是短生命周期的,而状态是长生命周期的。由组件拥有的 build context 绝不应被状态的字段引用,因为状态不会随组件一起被垃圾回收,并且其寿命可能会显著超过组件。
内存泄漏 vs 内存膨胀
#在内存泄漏中,应用程序会逐渐增加内存使用量,例如通过重复创建监听器而不进行处置。
内存膨胀(Memory bloat)是指使用了比实现最佳性能所需的更多的内存,例如使用过大的图片或在整个生命周期中保持流(stream)开启。
如果规模较大,泄漏和膨胀都会导致应用程序出现 out-of-memory 错误并崩溃。然而,泄漏更容易导致内存问题,因为即使是很小的泄漏,如果重复多次,也会导致崩溃。
内存视图指南
#DevTools 内存视图可帮助您调查内存分配(堆内存和外部内存)、内存泄漏、内存膨胀等。该视图具有以下功能:
- 可展开图表
-
获取内存分配的高级跟踪,并查看标准事件(如垃圾回收)和自定义事件(如图片分配)。
- Profile Memory(内存分析) 标签页
-
按类和内存类型列出当前的内存分配情况。
- Diff Snapshots(快照对比) 标签页
检测并调查某个功能的内存管理问题。
- Trace Instances(实例追踪) 标签页
-
调查特定一组类的内存管理情况。
可展开图表
#可展开图表提供以下功能:
内存结构分析
#时序图可视化了 Flutter 内存随时间变化的趋势。图表上的每个数据点对应于测量量(y轴)在特定时间戳(x轴)的状态。例如,捕获了使用量、容量、外部内存、垃圾回收和常驻内存大小(RSS)。
内存概览图
#内存概览图是收集到的内存统计信息的时序图。它直观地呈现了 Dart 或 Flutter 堆以及 Dart 或 Flutter 原生内存随时间的状态。
图表的 x 轴是事件的时间轴(时序)。y 轴上绘制的所有数据都有一个收集数据时的时间戳。换句话说,它显示了每 500 毫秒轮询一次的内存状态(容量、已用、外部、RSS(常驻内存大小)和 GC(垃圾回收))。这有助于在应用程序运行时提供内存状态的实时显示。
点击 Legend(图例) 按钮可显示收集到的测量结果、符号以及用于展示数据的颜色。
Memory Size Scale(内存大小比例) y 轴会自动根据当前可见图表范围内的收集数据范围进行调整。
y 轴上绘制的数量如下:
- Dart/Flutter 堆 (Heap)
堆中的对象(Dart 和 Flutter 对象)。
- Dart/Flutter 原生内存 (Native)
-
不在 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)
-
所有 Dart 堆的总容量。这通常略大于所有堆对象的总大小。
- RSS - 常驻内存大小 (Resident Set Size)
-
常驻内存大小显示进程所占用的内存量。它不包括被交换出去的内存。它包括已加载的共享库的内存,以及所有的栈和堆内存。更多信息,请参阅 Dart VM 内部原理。
Profile Memory(内存分析)标签页
#使用 Profile Memory(内存分析) 标签页按类和内存类型查看当前的内存分配情况。如需在 Google Sheets 或其他工具中进行深度分析,请下载 CSV 格式的数据。切换 Refresh on GC(GC 时刷新),可实时查看分配情况。
Diff Snapshots(快照对比)标签页
#使用 Diff Snapshots(快照对比) 标签页调查功能的内存管理。按照标签页上的指导,在与应用程序交互前后拍摄快照,并对快照进行对比。
点击 Filter classes and packages(过滤类和包) 按钮以缩小数据范围。
如需在 Google Sheets 或其他工具中进行深度分析,请下载 CSV 格式的数据。
Trace Instances(实例追踪)标签页
#使用 Trace Instances(实例追踪) 标签页,调查在功能执行期间哪些方法为一组类分配了内存。
- 选择要追踪的类
- 与应用交互以触发您感兴趣的代码
- 点击 Refresh(刷新)
- 选择一个追踪的类
- 查看收集到的数据
自底向上 vs 调用树视图
#根据您的具体任务,在自底向上和调用树视图之间进行切换。
调用树视图显示每个实例的方法分配情况。该视图是调用栈的自顶向下表示,这意味着可以展开方法以查看其被调用的函数。
自底向上视图显示了分配这些实例的不同调用栈列表。
其他资源
#有关更多信息,请查看以下资源:
- 要了解如何使用 DevTools 监控应用的内存使用情况并检测内存泄漏,请查看 Memory View 教程。
- 要了解 Android 内存结构,请查看 Android:进程间的内存分配。