跳到正文
🤔 文档有问题?报告编辑

LeakCanary 工作原理

安装 LeakCanary 后,它会自动检测和报告内存泄漏,分为 4 个步骤:

  1. 检测被保留的对象。
  2. 转储堆。
  3. 分析堆。
  4. 对泄漏进行分类。

1. 检测被保留的对象

LeakCanary 挂钩到 Android 生命周期中,以自动检测 Activity 和 Fragment 何时被销毁并应进行垃圾回收。这些被销毁的对象会被传递给一个 ObjectWatcher,它持有对它们的弱引用。LeakCanary 会自动检测以下对象的泄漏:

  • 被销毁的 Activity 实例
  • 被销毁的 Fragment 实例
  • 被销毁的 Fragment View 实例
  • 被清空的 ViewModel 实例

你可以观察任何不再需要的对象,例如一个已分离的 View 或一个被销毁的 Presenter

AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")

如果在等待 5 秒并运行垃圾回收后,ObjectWatcher 持有的弱引用仍未被清除,那么被观察的对象就被认为是被保留的,并可能发生泄漏。LeakCanary 会将此信息记录到 Logcat 中

D LeakCanary: Watching instance of com.example.leakcanary.MainActivity
  (Activity received Activity#onDestroy() callback) 

... 5 seconds later ...

D LeakCanary: Scheduling check for retained objects because found new object
  retained

LeakCanary 会等待被保留对象的数量达到某个阈值后再转储堆,并显示一个包含最新数量的通知。

notification 图 1. LeakCanary 发现 4 个被保留的对象。

D LeakCanary: Rescheduling check for retained objects in 2000ms because found
  only 4 retained objects (< 5 while app visible)

信息

当应用可见时,默认阈值为5 个被保留对象;当应用不可见时,默认阈值为1 个被保留对象。如果你看到了被保留对象的通知,然后将应用置于后台(例如按下 Home 键),阈值就会从 5 变为 1,LeakCanary 会在 5 秒内转储堆。点击通知会强制 LeakCanary 立即转储堆。

2. 转储堆

当被保留对象的数量达到阈值时,LeakCanary 会将 Java 堆转储为一个 .hprof 文件(一个堆转储文件),存储在 Android 文件系统上(参见 LeakCanary 将堆转储文件存储在哪里?)。转储堆会使应用短暂停顿,在此期间 LeakCanary 会显示以下 toast 提示:

toast 图 2. LeakCanary 在转储堆时显示一个 toast 提示

3. 分析堆

LeakCanary 使用 Shark 解析 .hprof 文件,并在该堆转储文件中定位被保留的对象。

done 图 3. LeakCanary 在堆转储文件中找到被保留的对象。

对于每个被保留的对象,LeakCanary 会找到阻止该被保留对象被垃圾回收的引用路径:即它的泄漏跟踪。你将在下一节学习如何分析泄漏跟踪:修复内存泄漏

done 图 4. LeakCanary 计算每个被保留对象的泄漏跟踪。

分析完成后,LeakCanary 会显示一个带有摘要的通知,并将结果打印到 Logcat 中。注意看下面,4 个被保留对象是如何被归类为2 个不同的泄漏的。LeakCanary 会为每个泄漏跟踪创建签名,并将具有相同签名的泄漏(即由同一个 bug 引起的泄漏)分组在一起。

done 图 5. 4 个泄漏跟踪变成了 2 个不同的泄漏签名。

====================================
HEAP ANALYSIS RESULT
====================================
2 APPLICATION LEAKS

Displaying only 1 leak trace out of 2 with the same signature
Signature: ce9dee3a1feb859fd3b3a9ff51e3ddfd8efbc6
┬───
│ GC Root: Local variable in native code
│
...

点击通知会启动一个 Activity,提供更多详情。稍后可以通过点击 LeakCanary 启动器图标再次查看。

toast 图 6. LeakCanary 会为安装它的每个应用添加一个启动器图标。

每一行对应一组具有相同签名的泄漏。当应用首次触发具有该签名的泄漏时,LeakCanary 会将该行标记为 New (新增)

toast 图 7. 4 个泄漏被分为 2 行,每行对应一个不同的泄漏签名。

点击一个泄漏项会打开一个显示泄漏跟踪的屏幕。你可以通过下拉菜单在被保留对象和其泄漏跟踪之间切换。

toast 图 8. 一个屏幕显示了按其共同泄漏签名分组的 3 个泄漏。

泄漏签名被怀疑导致泄漏的每个引用串联后的哈希值,即每个被红色波浪线下划线显示的引用

toast 图 9. 一个包含 3 个可疑引用的泄漏跟踪。

当泄漏跟踪以文本形式共享时,这些同样的可疑引用会用 ~~~ 下划线标记

...
│  
├─ com.example.leakcanary.LeakingSingleton class
│    Leaking: NO (a class is never leaking)
│    ↓ static LeakingSingleton.leakedViews
│                              ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    ↓ Object[].[0]
│               ~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
...

在上面的例子中,泄漏的签名将计算为:

val leakSignature = sha1Hash(
    "com.example.leakcanary.LeakingSingleton.leakedView" +
    "java.util.ArrayList.elementData" +
    "java.lang.Object[].[x]"
)
println(leakSignature)
// dbfa277d7e5624792e8b60bc950cd164190a11aa

4. 对泄漏进行分类

LeakCanary 将在其应用中发现的泄漏分为两类:应用泄漏库泄漏库泄漏是由你无法控制的第三方代码中的已知 bug 引起的泄漏。虽然此泄漏会影响你的应用,但不幸的是,你可能无法控制其修复,因此 LeakCanary 会将其分开。

这两类泄漏在打印到 Logcat 的结果中是分开显示的

====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS

====================================
1 LIBRARY LEAK

...
┬───
│ GC Root: Local variable in native code
│
...

LeakCanary 在其泄漏列表中将一行标记为 Library Leak (库泄漏)

Library Leak 图 10. LeakCanary 发现了一个库泄漏。

LeakCanary 内置了一个已知泄漏数据库,它通过匹配引用名称来识别这些泄漏。例如:

Leak pattern: instance field android.app.Activity$1#this$0
Description: Android Q added a new IRequestFinishCallback$Stub class [...]
┬───
│ GC Root: Global variable in native code
│
├─ android.app.Activity$1 instance
│    Leaking: UNKNOWN
│    Anonymous subclass of android.app.IRequestFinishCallback$Stub
│    ↓ Activity$1.this$0
│                 ~~~~~~
╰→ com.example.MainActivity instance

我做了什么导致了这个泄漏?

你什么都没做错!你只是按照预期的方式使用了 API,但其实现中存在导致此泄漏的 bug。

我能做些什么来阻止它吗?

或许可以!有些库泄漏可以使用反射来修复,有些可以通过执行某个代码路径来消除泄漏。这类修复往往比较取巧,所以要小心!最好的选择可能是查找或提交 bug 报告,并坚持要求修复这个 bug。

既然我对此泄漏无能为力,有没有办法让 LeakCanary 忽略它?

在转储堆并对其进行分析之前,LeakCanary 无法知道某个泄漏是否为库泄漏。如果 LeakCanary 在发现库泄漏时不显示结果通知,你可能会在看到转储 toast 提示后,开始疑惑 LeakCanary 的分析结果去了哪里。

你可以在 AndroidReferenceMatchers 类中查看已知泄漏的完整列表。如果你发现未被识别的 Android SDK 泄漏,请报告。你还可以自定义已知库泄漏的列表

下一步是什么?学习如何修复内存泄漏