使用内存视图
内存视图提供对应用程序内存分配详细信息的洞察,以及检测和调试特定问题的工具。
有关如何在不同 IDE 中找到 DevTools 屏幕的信息,请查看DevTools 概述。
为了更好地理解此页面上的见解,第一部分解释了 Dart 如何管理内存。如果您已经了解 Dart 的内存管理,可以跳到内存视图指南。
使用内存视图的理由
#在进行预先内存优化或当您的应用程序遇到以下情况之一时,使用内存视图
- 内存不足时崩溃
- 速度变慢
- 导致设备速度变慢或无响应
- 由于超出操作系统强制执行的内存限制而关闭
- 超出内存使用限制
- 此限制可能因您的应用目标设备类型而异。
- 怀疑存在内存泄漏
基本内存概念
#使用类构造函数创建的 Dart 对象(例如,使用MyClass()
)位于称为堆的内存部分。堆中的内存由 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`)
只有最短路径(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
,请使用以下规则:如果闭包的生命周期不超过小部件,则可以将上下文传递给闭包。
有状态小部件需要额外注意。它们由两个类组成:小部件和小部件状态,其中小部件是短生命周期的,而状态是长生命周期的。小部件拥有的构建上下文永远不应该从状态的字段中引用,因为状态不会与小部件一起被垃圾回收,并且可以大大超出其生命周期。
内存泄漏与内存膨胀
#在内存泄漏中,应用程序会逐渐使用内存,例如,通过重复创建侦听器但未处置它。
内存膨胀使用超出最佳性能所需的更多内存,例如,使用过大的图像或在整个生命周期内保持流打开。
当泄漏和膨胀很大时,都会导致应用程序因out-of-memory
错误而崩溃。但是,泄漏更有可能导致内存问题,因为即使是小的泄漏,如果重复多次,也会导致崩溃。
内存视图指南
#DevTools 内存视图可帮助您调查内存分配(堆和外部)、内存泄漏、内存膨胀等。该视图具有以下功能
- 可扩展图表
- 获取内存分配的高级跟踪,并查看标准事件(如垃圾回收)和自定义事件(如图像分配)。
- **内存分析**选项卡
- 按类和内存类型查看当前内存分配。
- **差异快照**选项卡
- 检测和调查功能的内存管理问题。
- **跟踪实例**选项卡
- 调查指定类集的功能的内存管理。
可扩展图表
#可扩展图表提供以下功能
内存结构
#时间序列图可视化 Flutter 内存在连续时间间隔内的状态。图表上的每个数据点对应于堆的测量量(y 轴)的时间戳(x 轴)。例如,捕获了使用情况、容量、外部、垃圾回收和驻留集大小。
内存概览图表
#内存概览图表是收集的内存统计信息的时间序列图。它以视觉方式显示了 Dart 或 Flutter 堆以及 Dart 或 Flutter 本机内存随时间的变化状态。
图表的 x 轴是事件(时间序列)的时间线。y 轴上绘制的数据都具有数据收集时间的时间戳。换句话说,它显示了每 500 毫秒内存的轮询状态(容量、已用、外部、RSS(驻留集大小)和 GC(垃圾回收))。这有助于在应用程序运行时提供内存状态的实时外观。
单击**图例**按钮将显示用于显示数据的收集测量值、符号和颜色。
**内存大小刻度**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、自定义 Flutter 引擎嵌入器、使用 Heroku 部署 Dart Web 服务器。
- 时间线
- 特定时间点(时间戳)所有收集的内存统计信息和事件的时间戳。
- 光栅缓存
- 在合成后执行最终渲染时,Flutter 引擎的光栅缓存层或图片的大小。有关更多信息,请参阅Flutter 架构概述和DevTools 性能视图。
- 已分配
- 堆的当前容量通常略大于所有堆对象的总大小。
- RSS - 驻留集大小
- 驻留集大小显示进程的内存量。它不包括已交换出的内存。它包括已加载的共享库的内存,以及所有堆栈和堆内存。有关更多信息,请参阅Dart VM 内部。
内存分析选项卡
#使用**内存分析**选项卡按类和内存类型查看当前内存分配。要进行更深入的 Google Sheets 或其他工具分析,请以 CSV 格式下载数据。切换**在 GC 时刷新**,以实时查看分配情况。
差异快照选项卡
#使用**差异快照**选项卡调查功能的内存管理。按照选项卡上的指南在与应用程序交互之前和之后获取快照,并对快照进行差异比较
点击**筛选类和包**按钮,缩小数据范围
要进行更深入的 Google Sheets 或其他工具分析,请以 CSV 格式下载数据。
跟踪实例选项卡
#使用**跟踪实例**选项卡调查在功能执行期间哪些方法为一组类分配内存
- 选择要跟踪的类
- 与您的应用交互以触发您感兴趣的代码
- 点击**刷新**
- 选择一个跟踪的类
- 查看收集到的数据
自下而上与调用树视图
#根据任务的具体情况在自下而上和调用树视图之间切换。
调用树视图显示每个实例的方法分配。该视图是调用堆栈的自上而下的表示,这意味着可以展开一个方法以显示其被调用者。
自下而上视图显示已分配实例的不同调用堆栈列表。
其他资源
#有关更多信息,请查看以下资源
- 要了解如何使用 DevTools 监视应用的内存使用情况并检测内存泄漏,请查看指导性内存视图教程。
- 要了解 Android 内存结构,请查看Android:进程间的内存分配。
除非另有说明,本网站上的文档反映了 Flutter 的最新稳定版本。页面上次更新于 2024-06-27。 查看源代码 或 报告问题.