今天心血来潮想写一些内存泄露的解决方案,说起内存泄露,那么就不得不提起这个LeakCanary。
基本原理描述LeakCanary是如何工作的,以及如何使用它检测和修复内存泄漏。本文档旨在帮助所有级别的开发人员,因此请不要犹豫地报告任何令人困惑的部分。
在基于Java的运行时,内存泄漏是编程错误,导致应用程序保留对不再需要的对象的引用。因此,分配给该对象的内存无法回收,最终导致OutOfMemoryError(OOM)崩溃。
例如,AndroidActivity实例之后不再需要实例。onDestroy()方法,并将对该实例的引用存储在静态字段中,从而防止垃圾回收。
大多数内存泄漏是由与对象生命周期相关的错误造成的。下面是一些常见的Android错误:
Fragment
实例,而不清除该片段的视图字段。Fragment.onDestroyView()
(详见这个StackOverflow的答案).Activity
实例作为Context
由于配置更改而生存下来的活动对象中的字段。内存泄漏在Android应用程序中非常常见,小内存泄漏的积累会导致应用程序耗尽内存,并导致OOM崩溃。LeakCanary将帮助您在开发期间找到并修复这些内存泄漏。当Square工程师首次启用Square Point of Sale应用程序中的LeakCanary时,他们能够修复几个漏洞,并将OOM的崩溃率降低了94%.
Your crash reporting tool might not correctly report OOMs. When memory is low because of memory leak accumulation, an OOM can be thrown from anywhere in the app code, which means that every OOM has a different stacktrace. So instead of one crash entry with a 1000 crashes, OOMs get reported as 1000 distinct crashes and hide in the long tail of low occuring crashes.
一旦安装了LeakCanary,它就会自动检测和报告内存泄漏,分四个步骤:
LeakCanary将链接到Android生命周期中,以自动检测活动和碎片何时被销毁并应该被垃圾回收。这些被销毁的对象被传递给ObjectWatcher
,它能容纳弱引用。LeakCanary自动检测下列对象的泄漏:
Activity
实例Fragment
实例View
实例ViewModel
实例您可以查看不再需要的任何对象,例如独立视图或已销毁的演示程序:
AppWatcher.objectWatcher.watch(myDetachedView, "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在转储堆之前等待保留对象的计数达到阈值,并显示带有最新计数的通知。
图1.LeakCanary发现了4件。
D LeakCanary: Rescheduling check for retained objects in 2000ms because found
only 4 retained objects (< 5 while app visible)
信息
应用程序中是可见5个保留对象默认阈值,不可见一个保留对象。如果您看到保留的对象通知,然后将应用程序放在后台(例如,按Home按钮),则阈值从5更改为1,LeakCanary在5秒内转储堆。点击通知会迫使LeakCanary立即转储堆。
当保留对象的计数达到阈值时,LeakCanary将Java堆转储到.hprof
档案(A)堆转储)存储在Android文件系统中(请参见LeakCanary在哪里存储堆转储?)。丢弃堆会使应用程序冻结一小段时间,在此期间,LeakCanary将显示以下吐司:
图2.LeakCanary显示Toast在倾倒堆的时候。
LeakCanary分析.hprof
文件并在堆转储中定位保留的对象。
图3.LeakCanary在堆转储中找到保留的对象。
对于每个保留的对象,LeakCanary查找防止该保留对象被垃圾收集的引用路径:leak trace。您将在下一节中学习分析泄漏跟踪:修复内存泄漏.
图4.LeakCanary计算每个保留对象的泄漏跟踪。
分析完成后,LeakCanary将显示Notification中输出的结果。logcat中很明显有两个泄漏。LeakCanary创建一个每个泄漏跟踪的签名,并将具有相同签名的泄漏(即由同一错误引起的泄漏)组合在一起。
====================================
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.有三条泄密。
这些可疑引用的下划线为~~~
当泄漏跟踪作为文本共享时:
...
│
├─ 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
LeakCanary将它在应用程序中发现的漏洞分为两类:APPLICATION LEAK和LIBRARY LEAK
。一个APPLICATION LEAK是由3中已知的bug引起的泄漏。RD您无法控制的政党代码。此漏洞正在影响您的应用程序,但不幸的是,修复它可能不在您的控制范围内,因此LeakCanary将其分离。
中打印的结果将这两个类别分开。logcat:
====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS
====================================
1 LIBRARY LEAK
...
┬───
│ GC Root: Local variable in native code
│
...
LeakCanary将一行标记为LIBRARY LEAK在泄密清单中:
图10.LeakCanary发现了APPLICATION的漏洞。
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得到修复。
既然我不能很好地处理这个漏洞,有什么办法可以让LeakCanary忽略它吗?
LeakCanary无法在转储堆并对其进行分析之前知道泄漏是否为库泄漏。如果LeakCanary在发现库泄漏时没有显示结果通知,那么您就会开始怀疑倾倒吐司后LeakCanary分析发生了什么。