一、介绍
LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。帮助我们提高APP的稳定性。
官网地址:https://square.github.io/leakcanary/getting_started/
二、使用
要使用LeakCanary,请将依赖项添加到应用程序的build.gradle
文件中:
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'
}
就这样,无需更改代码!
通过过滤Logcat中的LeakCanary
标签来确认LeakCanary在启动时正在运行:
D LeakCanary: LeakCanary is running and ready to detect leaks
LeakCanary自动检测以下对象的泄漏:
- 销毁
Activity
实例 - 销毁
Fragment
实例 - 破坏片段
View
实例 - 清除
ViewModel
实例
三、LeakCanary如何运作
一旦安装了LeakCanary,它就会通过4个步骤自动检测并报告内存泄漏:
- 检测保留的对象。
- 转储堆。
- 分析堆。
- 分类泄漏。
1.检测残留物
LeakCanary可以挂接到Android生命周期中,以自动检测Activity和Fragment何时被破坏并应进行垃圾收集。这些被破坏的对象将传递给ObjectWatcher
,其中包含对它们的弱引用。LeakCanary自动检测以下对象的泄漏:
- 销毁
Activity
实例 - 销毁
Fragment
实例 - 破坏片段
View
实例 - 清除
ViewModel
实例
您可以观看不再需要的任何对象,例如分离视图或损坏的演示者:
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保留对象时,应用程序是不可见的。如果看到保留对象通知,然后将应用程序置于后台(例如,通过按“主页”按钮),则阈值从5更改为1,LeakCanary在5秒钟内转储了堆。点击通知将强制LeakCanary立即转储堆。
2.转储堆
当保留对象的数量达到阈值时,LeakCanary将Java堆转储到存储在Android文件系统上的.hprof
文件(堆转储)中(请参阅LeakCanary在何处存储堆转储?)。转储堆会使应用程序冻结一小段时间,在此期间LeakCanary会显示以下Toast:
图2. LeakCanary显示了在堆放时的Toast。
3.分析堆
LeakCanary.hprof
使用Shark解析文件,并在该堆转储中找到保留的对象。
图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
│
...
轻触通知将启动提供更多详细信息的活动。稍后点击LeakCanary启动器图标再次回到它:
图6. LeakCanary为安装的每个应用程序添加一个启动器图标。
每行对应一组具有相同签名的泄漏。LeakCanary在应用程序首次使用该签名触发泄漏时将行标记为“新建”。
图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将在应用程序中发现的泄漏分为两类:应用程序泄漏和库泄漏。一个库泄漏是由3个已知的bug泄漏RD党的代码,你没有控制权。此泄漏正在影响您的应用程序,但是很遗憾,修复泄漏可能不在您的控制范围内,因此LeakCanary会将其分离出来。
在Logcat中打印的结果中将这两类分开:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary在其泄漏列表中将一行标记为库泄漏:
图10. LeakCanary发现了Library Leak。
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