【内存泄漏】LeakCanary

今天心血来潮想写一些内存泄露的解决方案,说起内存泄露,那么就不得不提起这个LeakCanary。

导言

基本原理描述LeakCanary是如何工作的,以及如何使用它检测和修复内存泄漏。本文档旨在帮助所有级别的开发人员,因此请不要犹豫地报告任何令人困惑的部分。

什么是内存泄露?

在基于Java的运行时,内存泄漏是编程错误,导致应用程序保留对不再需要的对象的引用。因此,分配给该对象的内存无法回收,最终导致OutOfMemoryError(OOM)崩溃。

例如,AndroidActivity实例之后不再需要实例。onDestroy()方法,并将对该实例的引用存储在静态字段中,从而防止垃圾回收。

 内存泄漏的常见原因

大多数内存泄漏是由与对象生命周期相关的错误造成的。下面是一些常见的Android错误:

  • 添加Fragment实例,而不清除该片段的视图字段。Fragment.onDestroyView()(详见这个StackOverflow的答案).
  • 存储Activity实例作为Context由于配置更改而生存下来的活动对象中的字段。
  • 注册监听器、广播接收器或RxJava订阅,这些订阅引用具有生命周期的对象,并忘记在生命周期结束时取消注册。
  • handler
  • 单例引起的内存泄漏
  • Asynctask引起的内存泄露

我们为什么要用LeakCanary

内存泄漏在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,它就会自动检测和报告内存泄漏,分四个步骤:

  1. 检测保留对象。
  2. 丢弃堆。
  3. 解析堆。
  4. 对泄漏进行分类。

 

1.检测保留对象

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在转储堆之前等待保留对象的计数达到阈值,并显示带有最新计数的通知。

【内存泄漏】LeakCanary_第1张图片

 图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立即转储堆。

2.丢弃堆

当保留对象的计数达到阈值时,LeakCanary将Java堆转储到.hprof档案(A)堆转储)存储在Android文件系统中(请参见LeakCanary在哪里存储堆转储?)。丢弃堆会使应用程序冻结一小段时间,在此期间,LeakCanary将显示以下吐司:

【内存泄漏】LeakCanary_第2张图片

 图2.LeakCanary显示Toast在倾倒堆的时候。

3.解析堆

LeakCanary分析.hprof文件并在堆转储中定位保留的对象。

【内存泄漏】LeakCanary_第3张图片

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

对于每个保留的对象,LeakCanary查找防止该保留对象被垃圾收集的引用路径:leak trace。您将在下一节中学习分析泄漏跟踪:修复内存泄漏.

【内存泄漏】LeakCanary_第4张图片

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

分析完成后,LeakCanary将显示Notification中输出的结果。logcat中很明显有两个泄漏。LeakCanary创建一个每个泄漏跟踪的签名,并将具有相同签名的泄漏(即由同一错误引起的泄漏)组合在一起。

【内存泄漏】LeakCanary_第5张图片图5.上图表示明显有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图标:

【内存泄漏】LeakCanary_第6张图片

 图6.LeakCanary为其安装的每个应用程序添加了一个启动图标。

每一行对应于具有相同签名的泄漏组。LeakCanary将一行标记为新的当应用程序第一次触发带有该签名的泄漏时。

【内存泄漏】LeakCanary_第7张图片

 图7.这4个泄漏被分组为2行,每一行都有不同的泄漏签名。

点击一个漏洞,打开一个屏幕的泄漏跟踪。您可以通过下拉方式切换在保留的对象和它们的泄漏跟踪。

【内存泄漏】LeakCanary_第8张图片

 

 

 图8.一个屏幕,显示按其常见泄漏签名分组的3个泄漏。

这个泄漏签名以红色下划线显示每个连接的散列参照系怀疑是泄漏的原因(每项参考资料):

【内存泄漏】LeakCanary_第9张图片

 图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

4.对泄漏进行分类

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在泄密清单中:

【内存泄漏】LeakCanary_第10张图片

 图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分析发生了什么。

你可能感兴趣的:(内存泄露,android)