即将推出!请参阅 flutter/devtools#3951

以下内容正在建设中。

CI

内存泄漏跟踪器

这是一个用于 Dart 和 Flutter 应用程序的内存泄漏跟踪框架。

Flutter 泄漏跟踪快速入门

Flutter 应用程序

  1. 在调用 runApp 之前,启用泄漏跟踪,并连接 Flutter 内存分配事件

import 'package:flutter/foundation.dart';
import 'package:leak_tracker/leak_tracker.dart';

...

enableLeakTracking();
MemoryAllocations.instance
      .addListener((ObjectEvent event) => dispatchObjectEvent(event.toMap()));
runApp(...
  1. 在调试模式下运行应用程序,并注意与泄漏相关的警告。如果您看到警告,请打开链接以调查泄漏。

TODO(polina-c): 实现链接并添加警告示例。

Flutter 测试

使用 withLeakTracking 包装您的测试以设置自动泄漏验证

test('...', () async {
  final leaks = await withLeakTracking(
    () async {
      ...
    },
  );

  expect(leaks, leakFree);
});

泄漏跟踪如何工作

在阅读泄漏跟踪之前,请先了解 Dart 内存概念

已处理的泄漏类型

泄漏跟踪器只能捕获特定类型的泄漏,特别是与处理和垃圾回收的时机相关的泄漏。在适当的内存管理下,该工具假设对象的处理和垃圾回收会迅速发生。也就是说,对象在不再需要后应该在下一个垃圾回收周期中被回收。

通过监视处理和垃圾回收事件,该工具可以检测不同类型的泄漏

  • 未处理但已 GC(未处理):

    • 定义:一个可处理对象在未被处理的情况下被 GC 了。这意味着对象的处理内容在使用该对象后仍在占用内存。

    • 修复:调用对象的 dispose() 来释放内存。

  • 已处理但未 GC(未 GC):

    • 定义:一个对象在经过一定次数的 GC 事件后被处理但未被 GC。这意味着对该对象的引用阻止了它在不再需要后被垃圾回收。

    • 修复:要修复此泄漏,请在处理后将对象的所有可访问引用设置为 null。

      myField.dispose();
      myField = null;
      
  • 已处理但 GC 延迟(GC 延迟):

    • 定义:一个对象被处理然后被 GC,但 GC 的时间比预期晚。这意味着保留路径在一段时间内将对象保留在内存中,但之后消失了。

    • 修复:与 **未 GC** 相同。

  • 已处理但未 GC,无路径(无 GC 路径):

    • 定义:一个对象在预期时间被处理但未被 GC,但未检测到保留路径,这意味着对象很可能在下一个 GC 周期中被 GC,并且泄漏将转换为 **GC 延迟** 泄漏。

    • 修复:如果您看到此类泄漏,请 创建问题,因为它意味着工具存在问题。

罪魁祸首和受害者

如果您有一组未 GC 的对象,其中一些(受害者)可能由于被其他对象(罪魁祸首)持有而未被 GC。通常,要修复泄漏,您只需要修复罪魁祸首。

受害者:一个泄漏的对象,工具可以找到另一个泄漏的对象,如果修复该对象,也将修复第一个泄漏。

罪魁祸首:一个泄漏的对象,未被检测为另一个对象的目标。

该工具可以检测哪些泄漏对象是罪魁祸首,因此您可以知道在哪里着力。

例如,在下面的图表中,四个未 GC 的泄漏中,只有一个是罪魁祸首,因为当对象被修复并 GC 后,它引用的受害者也将被 GC。

   flowchart TD;
      l1[leak1\nculprit]
      l2[leak2\nvictim]
      l3[leak3\nvictim]
      l4[leak4\nvictim]
      l1-->l2;
      l1-->l3;
      l2-->l4;
      l3-->l4;

限制

按跟踪类

泄漏跟踪器只能捕获仪器化对象的泄漏(有关详细信息,请参阅 概念)。

但是,好消息是

  1. 大多数可处理的 Flutter 框架类都包含仪器化。如果您的 Flutter 应用如何管理小部件导致泄漏,Flutter 将会捕获它们。

  2. 如果泄漏至少包含一个仪器化对象,则该泄漏将被捕获,所有其他对象,即使是非仪器化的,也将停止泄漏。

请参阅 仪器化指南

按构建模式

泄漏跟踪器的可用性因构建模式而异。请参阅 Dart 构建模式Flutter 构建模式

Dart 开发和 Flutter 调试

泄漏跟踪已完全可用。

Flutter Profile

泄漏跟踪可用,但监听 Flutter 仪器化对象的 MemoryAllocations 需要 开启 才能跟踪 Flutter 框架对象。

Dart Production 和 Flutter Release

泄漏跟踪已禁用。

注意:如果您有兴趣在发布模式下启用泄漏跟踪,请在此 评论

仪器化您的代码

如果您想捕获 Flutter 框架之外的对象(已经仪器化)的泄漏,您需要仪器化它们。

对于每个被跟踪的对象,库应该从您的代码中接收两个信号:(1)对象已创建,(2)对象不再使用。最方便的做法是在构造函数中给出第一个信号,在 dispose 方法中给出第二个信号。

import 'package:leak_tracker/src/leak_tracker.dart';

class InstrumentedClass {
  InstrumentedClass() {
    dispatchObjectCreated(
      library: library,
      className: '$InstrumentedClass',
      object: this,
    );
  }

  static const library = 'package:my_package/lib/src/my_lib.dart';

  void dispose() {
    dispatchObjectDisposed(object: this);
  }
}

启动/停止泄漏跟踪

要开始泄漏跟踪,请调用 enableLeakTracking();要停止,请调用 disableLeakTracking()

TODO(polina-c): 请注意,Flutter 框架默认启用泄漏跟踪,情况就是如此。

收集泄漏

泄漏收集有两个步骤:(1)获取泄漏发生的信号(泄漏摘要),(2)获取泄漏的详细信息。

默认情况下,泄漏跟踪器每秒检查一次泄漏,如果有泄漏,则将摘要输出到控制台并发送到 DevTools。然后,您可以通过从 DevTools 请求或通过以编程方式调用 collectLeaks() 来获取泄漏的详细信息。

您可以通过将自定义的 配置 传递给 enableLeakTracking() 来更改默认行为。

  1. 禁用常规泄漏检查,并通过调用 checkLeaks() 来检查泄漏。
  2. 禁用输出到控制台或 DevTools。
  3. 使用自定义处理程序监听泄漏。

有关如何与泄漏跟踪器交互,请参阅 DevTools > Memory > Leaks 指南。

TODO:添加指向 DevTools 文档的链接。

故障排除泄漏

收集调用堆栈

对象生命周期事件的堆栈跟踪可能有助于找出泄漏的根本原因。生命周期事件对于未处理的泄漏是创建,对于未 GC 的泄漏是处理。

默认情况下,泄漏跟踪器不收集堆栈跟踪,因为收集可能会影响性能和内存占用。

有一些选项可以为故障排除启用堆栈跟踪收集。

  1. 通过将 stackTraceCollectionConfig 传递给 withLeakTrackingenableLeakTracking

collect_callstack.mov

  1. 在 DevTools > Memory > Leaks 中使用交互式 UI。

TODO:添加带有解释的 DevTools 文档链接。

检查保留路径

打开 DevTools > Memory > Leaks,等待未 GC 泄漏被捕获,然后点击“分析和下载”。

TODO:添加详细信息。

性能影响

内存

泄漏跟踪器会为每个被跟踪的活动对象和每个检测到的泄漏存储一小段额外的记录,这会增加内存占用。

对于在 macos 上使用 Profile 模式的 Gallery 应用程序,泄漏跟踪使主页的内存占用增加了约 400 KB,占总内存的约 0.5%。

CPU

泄漏跟踪在两个方面影响 CPU。

  1. 每个对象跟踪。在 macos 上使用 Profile 模式时,对 Gallery 主页的总加载时间增加了约 0.05 毫秒(约 2.7%)。

  2. 对被跟踪对象的定期异步分析。在 macos 上使用 Profile 模式时,对 Gallery 主页来说,大约需要 2.5 毫秒。

GitHub

查看 Github