使用内存视图
内存视图提供有关应用程序内存分配细节的洞察,以及检测和调试特定问题的工具。
有关如何在不同 IDE 中查找 DevTools 屏幕的信息,请查阅DevTools 概览。
为了更好地理解本页面上的洞察,第一部分解释了 Dart 如何管理内存。如果您已经了解 Dart 的内存管理,可以跳到内存视图指南。
使用内存视图的原因
#使用内存视图进行预防性内存优化,或者当您的应用程序出现以下任一情况时:
- 内存不足时崩溃
- 运行变慢
- 导致设备运行缓慢或无响应
- 因超出操作系统强制执行的内存限制而关闭
- 超出内存使用限制
- 此限制可能因应用程序所针对的设备类型而异。
- 怀疑存在内存泄漏
基本内存概念
#使用类构造函数(例如,通过使用 MyClass()
)创建的 Dart 对象存储在内存中一个名为 堆 (heap) 的区域。堆中的内存由 Dart VM(虚拟机)管理。Dart VM 在对象创建时为对象分配内存,并在对象不再使用时释放(或解除分配)内存(参见Dart 垃圾回收)。
对象类型
#可释放对象
#可释放对象是任何定义了 dispose()
方法的 Dart 对象。为避免内存泄漏,当对象不再需要时,请调用 dispose
。
内存风险对象
#内存风险对象是指如果未正确释放或已释放但未被 GC(垃圾回收)的对象,可能会导致内存泄漏的对象。
根对象、保留路径和可达性
#根对象
#每个 Dart 应用程序都会创建一个 根对象,它直接或间接地引用应用程序分配的所有其他对象。
可达性
#如果在应用程序运行的某个时刻,根对象停止引用一个已分配对象,该对象将变为 不可达 (unreachable),这是垃圾回收器 (GC) 释放对象内存的信号。
保留路径
#从根对象到某个对象的引用序列称为该对象的 保留路径 (retaining path),因为它保留了对象的内存,使其不被垃圾回收。一个对象可以有多个保留路径。具有至少一个保留路径的对象称为 可达 (reachable) 对象。
示例
#以下示例说明了这些概念
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
的大小不属于父对象或子对象的浅层大小,但属于它们的保留大小。
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 中会发生内存泄漏吗?
#垃圾回收器无法阻止所有类型的内存泄漏,开发人员仍然需要关注对象以实现无泄漏的生命周期。
为什么垃圾回收器无法阻止所有泄漏?
#虽然垃圾回收器负责所有不可达对象,但应用程序有责任确保不再需要的对象不再可达(即不再被根对象引用)。
因此,如果不再需要的对象仍被引用(在全局或静态变量中,或作为长生命周期对象的字段),垃圾回收器将无法识别它们,内存分配会逐渐增长,最终导致应用程序因 内存不足
错误而崩溃。
为什么闭包需要额外注意
#一种难以捕获的泄漏模式与使用闭包有关。在以下代码中,对设计为短生命周期的 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 传递给闭包。
有状态组件 (Stateful widget) 需要额外注意。它们由两个类组成:widget 及其状态,其中 widget 是短生命周期的,状态是长生命周期的。widget 所拥有的构建上下文 (build context) 绝不应从状态的字段中引用,因为状态不会与 widget 一起被垃圾回收,并且其生命周期可能远远长于 widget。
内存泄漏与内存膨胀
#在内存泄漏中,应用程序会逐步占用内存,例如,通过重复创建侦听器而不对其进行释放。
内存膨胀 (Memory bloat) 使用的内存超出最佳性能所需,例如,使用过大的图像或在整个生命周期内保持流打开。
当泄漏和膨胀量很大时,都会导致应用程序因 内存不足
错误而崩溃。然而,泄漏更容易导致内存问题,因为即使是微小的泄漏,如果重复多次,也会导致应用程序崩溃。
内存视图指南
#DevTools 内存视图可帮助您调查内存分配(包括堆内存和外部内存)、内存泄漏、内存膨胀等。该视图具有以下功能:
- 可展开图表
- 获取内存分配的高级跟踪,并查看标准事件(如垃圾回收)和自定义事件(如图像分配)。
- 分析内存 (Profile Memory) 选项卡
- 按类和内存类型查看当前内存分配。
- 快照对比 (Diff Snapshots) 选项卡
- 检测和调查某个功能的内存管理问题。
- 跟踪实例 (Trace Instances) 选项卡
- 调查某个功能针对指定类集的内存管理。
可展开图表
#可展开图表提供以下功能:
内存剖析
#时序图可视化 Flutter 内存随时间推移的状态。图表上的每个数据点都对应于堆的测量量(y 轴)的时间戳(x 轴)。例如,捕获了使用量、容量、外部内存、垃圾回收和驻留集大小。
内存概览图
#内存概览图是收集到的内存统计数据的时序图。它以视觉方式呈现 Dart 或 Flutter 堆以及 Dart 或 Flutter 原生内存随时间变化的状态。
图表的 x 轴是事件的时间线(时序)。y 轴上绘制的所有数据都带有数据收集时的时间戳。换句话说,它每 500 毫秒显示一次内存的轮询状态(容量、已用、外部、RSS(驻留集大小)和 GC(垃圾回收))。这有助于在应用程序运行时提供内存状态的实时显示。
点击 图例 按钮,显示用于显示数据的收集测量值、符号和颜色。
内存大小比例 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 时刷新,以实时查看分配情况。
快照对比 (Diff Snapshots) 选项卡
#使用 快照对比 (Diff Snapshots) 选项卡调查某个功能的内存管理。按照选项卡上的指导,在与应用程序交互前后拍摄快照,并对比这些快照。
点击 过滤类和包 按钮,以缩小数据范围。
为了在 Google 表格或其他工具中进行更深入的分析,请以 CSV 格式下载数据。
跟踪实例 (Trace Instances) 选项卡
#使用 跟踪实例 (Trace Instances) 选项卡调查在功能执行期间哪些方法为一组类分配内存。
- 选择要跟踪的类
- 与您的应用交互以触发您感兴趣的代码
- 点击 刷新
- 选择一个已跟踪的类
- 查看收集到的数据
自下而上视图与调用树视图
#根据您的具体任务,在自下而上视图和调用树视图之间切换。
调用树视图显示每个实例的方法分配。该视图是调用栈的自上而下表示,这意味着一个方法可以展开以显示其被调用者。
自下而上视图显示了已分配这些实例的不同调用栈列表。
其他资源
#更多信息,请查阅以下资源:
- 要了解如何使用 DevTools 监控应用程序的内存使用并检测内存泄漏,请查阅一个指导性的内存视图教程。
- 要了解 Android 内存结构,请查阅Android:进程间内存分配。