一旦LeakCanary被安装,它自动检测和报告内存泄漏,分4步:
- 检测保留下来的对象;
- 导出堆信息;
- 分析堆信息;
- 对内存泄漏进行分类;
目录
1.检测保留下来的对象
LeakCanary通过Hook(劫持)Android生命周期去自动检测内存泄漏问题,当Activity和Fragment被销毁并且执行垃圾回收的时候;这些被销毁的对象被传递给ObjectWatcher(持有这些销毁的对象的弱引用);LeakCanary自动检测如下对象的内存泄漏:
a.已销毁的Activity实例;
b.已销毁的Fragment实例;
c.已销毁的片段View实例;
d.已经清除的ViewModel实例;
可以检测任何不在需要的对象,例如一个被移除的View或者一个销毁的Presenter:
AppWatcher.INSTANCE.getObjectWatcher().watch(textView2, "View was detached");
如果持有弱引用销毁对象的ObjectWatcher在等候5秒并且运行垃圾回收不能被清除,被观察的销毁对象可能被保留,存在潜在的内存泄漏问题;
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一直等候被保留未销毁的对象数量达到阀值(5)再导出heap堆hprof文件,并在通知栏显示最新未销毁对象的数量;
通知提示有4个未销毁的对象被保留,点击通知可以导出heap堆文件;
D LeakCanary: Rescheduling check for retained objects in 2000ms because found
only 4 retained objects (< 5 while app visible)
注意:
默认阈值为应用程序可见时5个保留对象,应用程序不可见时1个保留对象。如果您看到retained objects通知,然后将应用程序置于后台(例如按Home按钮),那么阈值将从5更改为1,LeakCanary将在5秒内导出堆文件。点击通知会强制LeakCanary立即导出堆文件。
2.导出堆文件
当未销毁对象被保留达到阀值,LeakCanary导出Java的堆信息存储到hprof文件;导出heap堆文件会短暂冻结APP,在导出堆文件时会有如下通知:
默认存储堆文件在app文件夹下的leakcanary目录下,如果设置android.permission.WRITE_EXTERNAL_STORAGE权限并授权此权限,则堆文件存储在SD卡的Download/leakcanary-com.example目录下,com.example是app的包名;
3.分析堆文件
Shark: Smart Heap Analysis Reports for Kotlin;
Shark是为LeakCanary 2提供功能强大的堆分析器。它是一个Kotlin独立堆分析库,以低内存占用率高速运行。
Shark被支持如下功能:
a.Shark Hprof:读取和写入Hprof文件中的记录。
b.Shark Graph:导航堆对象图。
c.Shark:生成堆分析报告。
d.Shark Android:Android启发式生成定制的堆分析报告。
e.Shark CLI:分析安装在连接到桌面的Android设备上的可调试应用程序堆。输出与LeakCanary的输出类似,只是您不必将LeakCanary依赖项添加到应用程序中。
LeakCanary:建在上面。它会自动监视被销毁的Activity和Fragment,触发堆存储,运行Shark Android,然后显示结果。
06-27 15:19:38.515 9186-9224/fan.fragmentdemo D/LeakCanary: Removing 1 heap dumps
06-27 15:19:41.523 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: PARSING_HEAP_DUMP
06-27 15:19:43.267 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: EXTRACTING_METADATA
06-27 15:19:43.469 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_RETAINED_OBJECTS
06-27 15:19:43.994 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_PATHS_TO_RETAINED_OBJECTS
06-27 15:19:44.500 9186-9223/fan.fragmentdemo D/LeakCanary: Setting up flushing for Thread[IntentService[HeapAnalyzerService],5,main]
06-27 15:19:47.737 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: FINDING_DOMINATORS
06-27 15:19:54.663 9186-9494/fan.fragmentdemo D/LeakCanary: Found 2 retained objects
06-27 15:19:54.663 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: COMPUTING_NATIVE_RETAINED_SIZE
06-27 15:19:55.480 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: COMPUTING_RETAINED_SIZE
06-27 15:19:55.554 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: BUILDING_LEAK_TRACES
06-27 15:19:55.558 9186-9494/fan.fragmentdemo D/LeakCanary: Found 2 paths to retained objects, down to 1 after removing duplicated paths
06-27 15:19:55.720 9186-9494/fan.fragmentdemo D/LeakCanary: Analysis in progress, working on: REPORTING_HEAP_ANALYSIS
06-27 15:19:55.737 9186-9494/fan.fragmentdemo D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
30451 bytes retained by leaking objects
Signature: f3466687f84b8cdd14a9862dcc5b72a7115e352b
┬───
│ GC Root: System class
│
├─ fan.fragmentdemo.MemoryTestActivity class
│ Leaking: NO (a class is never leaking)
│ ↓ static MemoryTestActivity.textView2
│ ~~~~~~~~~
╰→ android.support.v7.widget.AppCompatTextView instance
Leaking: YES (ObjectWatcher was watching this because View was detached and View.mContext references a destroyed activity)
key = 0f1c40a8-d5be-4253-ab5c-fdec9e64c65d
watchDurationMillis = 15965
retainedDurationMillis = 10964
mContext instance of fan.fragmentdemo.MemoryTestActivity with mDestroyed = true
View#mParent is set
View#mAttachInfo is null (view detached)
View.mID = R.id.textView2
View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 25
Build.MANUFACTURER: smartisan
LeakCanary version: 2.4
App process name: fan.fragmentdemo
Analysis duration: 14196 ms
Heap dump file path: /storage/emulated/0/Download/leakcanary-fan.fragmentdemo/2020-06-27_15-19-38_530.hprof
Heap dump timestamp: 1593242395719
====================================
以上是分析hprof文件的日志;
LeakCanary通过Shark解析hprof文件并定位在堆中无法回收被保留的对象;
以上是LeakCanary在堆堆文件找到被保留对象通知;
对于每个保留对象,LeakCanary都会找到防止该保留对象被垃圾回收的引用路径:其泄漏跟踪。下一节将学习分析泄漏跟踪:修复内存泄漏。
以上通知提示在计算被保留对象的引用路径;
分析完成后,LeakCanary会显示一个带有摘要的通知,并在Logcat中打印结果。请注意下面4个保留对象如何分组为2个不同的泄漏。LeakCanary为每个泄漏跟踪创建一个签名,并将具有相同签名的泄漏(即由相同错误引起的泄漏)组合在一起。
以上表示4个引用路径分为两种不同的泄漏签名;
====================================
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查看更详细的泄漏问题,关闭Activity可以看到LeakCanary加载图标:
以上表示增加了一个为了每个被安装的app增加了一个加载图标;
每一行显示一组有详情签名的内存泄漏问题;LeakCanary标记了一行New表示第一次出现内存泄漏问题;
以上表示4内存泄漏问题分在两行,每行有不同的泄漏签名;
点击打开带有泄漏引用路径。您可以通过下拉菜单在保留对象及其泄漏引用路径之间切换。
以上表示相同泄漏签名的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在你的app中分两类,Applications Leaks和Library Leaks;一个Library Leak是被第三方库引起的问题(超出你控制范围的);这个leak泄漏影响的应用程序,因为修改它可能不在你控制范围因此LeakCanary把它分开说明;
这两类被分开在Logcat控制台打印:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary标记一行在leaks列表中做为Library Leak:
以上表示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
5.其他
我做了什么引起内存泄漏问题?
没问题!您按照预期的方式使用了一个API,但实现中有一个导致此泄漏的bug。
如果阻止内存泄漏问题?
可能!一些库泄漏可以使用反射修复,另一些可以通过使用使泄漏消失的代码路径修复。这种类型的修复往往是黑客,所以小心!您最好的选择可能是找到bug报告或文件,并坚持bug得到修复。
既然我对这次泄漏无能为力,有没有办法让LeakCanary置之不理呢?
在堆文件对其进行分析之前,LeakCanary无法知道泄漏是否是库泄漏。如果在发现库泄漏时,LeakCanary没有显示结果通知,那么您将开始怀疑在toast之后,LeakCanary分析发生了什么。
参考 :
https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/