LeakCanary 工作原理
安装 LeakCanary 后,它会自动检测和报告内存泄漏,分为 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 会等待被保留对象的数量达到某个阈值后再转储堆,并显示一个包含最新数量的通知。
图 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 提示:
图 2. LeakCanary 在转储堆时显示一个 toast 提示。
3. 分析堆¶
LeakCanary 使用 Shark 解析 .hprof
文件,并在该堆转储文件中定位被保留的对象。
图 3. LeakCanary 在堆转储文件中找到被保留的对象。
对于每个被保留的对象,LeakCanary 会找到阻止该被保留对象被垃圾回收的引用路径:即它的泄漏跟踪。你将在下一节学习如何分析泄漏跟踪:修复内存泄漏。
图 4. LeakCanary 计算每个被保留对象的泄漏跟踪。
分析完成后,LeakCanary 会显示一个带有摘要的通知,并将结果打印到 Logcat 中。注意看下面,4 个被保留对象是如何被归类为2 个不同的泄漏的。LeakCanary 会为每个泄漏跟踪创建签名,并将具有相同签名的泄漏(即由同一个 bug 引起的泄漏)分组在一起。
图 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 启动器图标再次查看。
图 6. LeakCanary 会为安装它的每个应用添加一个启动器图标。
每一行对应一组具有相同签名的泄漏。当应用首次触发具有该签名的泄漏时,LeakCanary 会将该行标记为 New (新增)。
图 7. 4 个泄漏被分为 2 行,每行对应一个不同的泄漏签名。
点击一个泄漏项会打开一个显示泄漏跟踪的屏幕。你可以通过下拉菜单在被保留对象和其泄漏跟踪之间切换。
图 8. 一个屏幕显示了按其共同泄漏签名分组的 3 个泄漏。
泄漏签名是被怀疑导致泄漏的每个引用串联后的哈希值,即每个被红色波浪线下划线显示的引用
图 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 (库泄漏)
图 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 泄漏,请报告。你还可以自定义已知库泄漏的列表。
下一步是什么?学习如何修复内存泄漏!