【Android】内存泄露 使用 LeakCanary 应当如何应对?最全的解决

1.LeakCanary简介

官网:LeakCanary is a memory leak detection library for Android

LeakCanary是一个用于Android的内存泄漏检测库

在这里插入图片描述
LeakCanaryAndroid框架内部的了解使其具有一种独特的能力,可以缩小每个泄漏的原因,帮助开发人员大幅减少应用程序不响应冻结和OutOfMemoryError崩溃。

2.入手指南

要使用LeakCanary,将LeakCanary -android依赖项添加到应用程序的构建中,gradle文件中即可;与之前的引入方式不一样,在LeakCanary的最新版本,可以直接引入使用,不在需要初始化代码;

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}

以上代码可以直接使用,不需要更改。在添加依赖的位置,说明:在使用LeakCanary的时候,使用的debugImplementation添加依赖,只在调试版本中运行,对应于进行打包的时,不会打包至应用中的;

查看是否集成成功–>可以通过过滤Logcat中的LeakCanary标签,确认LeakCanary在启动时运行;

D LeakCanary: LeakCanary is running and ready to detect leaks

LeakCanary自动检测以下对象的泄漏:

  1. destroyed Activity instances
  2. destroyed Fragment instances
  3. destroyed fragment View instances
  4. cleared ViewModel instances
  5. destroyed Service instance

3.基础使用

3.1介绍

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

3.1.1什么是内存泄漏

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

例如,一个Android活动实例不再需要后,其onDestroy()方法被调用,并存储对该实例的引用在一个静态字段防止它被垃圾收集.

3.1.2内存泄漏的常见原因

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

  1. Fragment. ondestroyview()中添加一个Fragment实例到回栈,而不清除Fragment的视图字段(更多细节在这个StackOverflow的答案中)
  2. Activity实例作为Context字段存储在一个对象中,该对象由于配置更改而存活下来。
  3. 注册一个引用有生命周期的对象的侦听器、广播接收器或RxJava订阅,当生命周期结束时忘记注销。

3.1.3为什么我应该使用LeakCanary

内存泄漏在Android应用程序中非常常见,小内存泄漏的积累会导致应用程序耗尽内存并在OOM中崩溃。LeakCanary将帮助您在开发期间找到并修复这些内存泄漏。当Square工程师第一次在Square Point Of Sale应用中启用LeakCanary时,他们能够修复几个漏洞,并将OOM的崩溃率降低了94%

3.2.LeakCanary的工作原理

工作原理

集成LeakCanary之后,工作分4个步骤,自动检测并报告内存泄漏:

  1. 检测保留的对象
  2. 正在转储堆
  3. 分析堆
  4. 泄漏分类

3.2.1 检测保留的对象

LeakCanaryAndroid生命周期自定绑定,自动检测活动和片段何时被销售,在什么时候应该被回收,这些对象被传递给ObjectWatcher,后者持有对它们的弱引用。LeakCanary自动检测以下对象的泄漏:

  1. destroyed Activity instances(已销毁activty实例)
  2. destroyed Fragment instances(已销毁Fragment实例)
  3. destroyed fragment View instances(已销毁feagment view实例)
  4. cleared ViewModel instances(已销毁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个保留对象,当应用程序不可见时,为1个保留对象。如果您看到保留对象通知,然后将应用程序置于后台(例如按Home按钮),则阈值从5更改为1,LeakCanary在5秒内转储堆。点击通知会迫使LeakCanary立即转储堆。

3.2.2 正在转储堆

当保留对象的数量达到阈值时,LeakCanary会将Java堆转储到存储在Android文件系统上的.hprof文件(堆转储)中(请参阅LeakCanari在哪里存储堆转储?)。转储堆会在短时间内冻结应用程序,在此期间LeakCanary会显示以下内容:
在这里插入图片描述图2:LeakCanary在倾倒垃圾堆。

3.2.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
│
...

点击通知将启动一个提供更多详细信息的活动。稍后,点击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

3.2.4 泄漏分类

LeakCanary将其在应用程序中发现的漏洞分为两类:应用程序漏洞和库漏洞。库泄漏是由您无法控制的第三方代码中的已知错误引起的泄漏。此漏洞正在影响您的应用程序,但不幸的是,修复它可能不在您的控制范围内,因此LeakCanary将其分离出来;
Logcat中打印的结果中,这两个类别是分开的:

====================================
HEAP ANALYSIS RESULT
====================================
0 APPLICATION LEAKS

====================================
1 LIBRARY LEAK

...
┬───
│ GC Root: Local variable in native code
│
...

LeakCanary在其泄漏列表中将一行标记为泄漏:

在这里插入图片描述
图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

3.3 修复内存泄漏

内存泄漏是一种编程错误,导致应用程序保留对不再需要的对象的引用。在代码的某个地方,有一个引用应该被清除,但没有被清除;
按照以下4个步骤修复内存泄漏:

  1. 找到泄漏痕迹
  2. 缩小泄漏范围
  3. 查找导致泄漏依据
  4. 修复泄漏

3.3.1 找到泄漏痕迹

泄漏跟踪是从垃圾收集根到保留对象的最佳强引用路径的较短名称,即在内存中保存对象的引用路径,因此防止其被垃圾收集;

例如,我们将助手单例存储在静态字段中:

class Helper {
}

class Utils {
  public static Helper helper = new Helper();
}

让我们告诉LeakCanary,预计单例实例将被垃圾收集:

AppWatcher.objectWatcher.watch(Utils.helper)

该单例的泄漏跟踪如下所示:

┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│    ↓ Object[].[43]
├─ com.example.Utils class
│    ↓ static Utils.helper
╰→ java.example.Helper

让我们把它分解!在顶部,PathClassLoader实例由垃圾收集(GC)根持有,更具体地说是本机代码中的局部变量。GC根是始终可访问的特殊对象,即它们不能被垃圾收集。GC根有4种主要类型:

  1. 局部变量,属于线程堆栈
  2. 活动Java线程的实例
  3. 从不卸载的系统类
  4. 本机引用,由本机代码控制
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance

以开头的行├─ 表示Java对象(类、对象数组或实例)和以开头的行│ ↓ 表示对下一行中Java对象的引用。
PathClassLoader有一个runtimeInternalObjects字段,该字段是对对象数组的引用:

├─ dalvik.system.PathClassLoader instance
│    ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array

该对象数组中位置43处的元素是对Utils类的引用

├─ java.lang.Object[] array
│    ↓ Object[].[43]
├─ com.example.Utils class

以开头的行╰→ 表示泄漏对象,即传递给AppWatcher.objectWatcher.watch()的对象。
Utils类有一个静态助手字段,该字段是对泄漏对象的引用,泄漏对象是助手单例实例:

├─ com.example.Utils class
│    ↓ static Utils.helper
╰→ java.example.Helper instance

3.3.2 缩小泄漏范围

泄漏跟踪是引用的路径。最初,该路径中的所有引用都可能导致泄漏,但LeakCanary可以自动缩小可疑引用的范围。为了理解这意味着什么,让我们手动完成这个过程。

下面是一个糟糕的Android代码示例:

class ExampleApplication : Application() {
  val leakedViews = mutableListOf<View>()
}

class MainActivity : Activity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)

    val textView = findViewById<View>(R.id.helper_text)

    val app = application as ExampleApplication
    // This creates a leak, What a Terrible Failure!
    app.leakedViews.add(textView)
  }
}

LeakCanary生成如下泄漏跟踪:

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    ↓ ExampleApplication.leakedViews
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
├─ java.lang.Object[] array
│    ↓ Object[].[0]
├─ android.widget.TextView instance
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

以下是如何读取泄漏痕迹:

FontsContract类是一个系统类(请参见GC根:系统类),它有一个sContext静态字段,该字段引用一个ExampleApplication实例,该实例有一个leakedViews字段,它引用一个ArrayList实例,该实例引用一个数组(支持数组列表实现的数组),该数组有一个元素,该元素引用一个TextView,该TextView有一个McContext字段,该域引用一个已销毁MainActivity的实例。

LeakCanary使用~~下划线突出显示所有可能导致此次泄漏的参考。最初,所有参考文献都是可疑的:

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
│                           ~~~~~~~~
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]~~~
├─ android.widget.TextView instance
│    ↓ TextView.mContext
│               ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance

然后,LeakCanary推断泄漏跟踪中对象的state和生命周期。在Android应用程序中,应用程序实例是一个从不被垃圾收集的单例,因此它从不泄漏(Leaking: NO (Application is a singleton))。由此,LeakCanary得出结论,泄漏不是由FontsContract.sContext(删除相应的~~)。以下是更新的泄漏跟踪:

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]~~~
├─ android.widget.TextView instance
│    ↓ TextView.mContext
│               ~~~~~~~~
╰→ com.example.leakcanary.MainActivity instance

TextView实例通过其mContext字段引用已销毁的MainActivity实例。视图不应在其上下文的生命周期内生存,因此LeakCanary知道此TextView实例正在泄漏(泄漏:YES(View.mContext引用已破坏的活动)),因此泄漏不是由TextView引起的。mContext(删除相应的~~)。以下是更新的泄漏跟踪:

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

总之,LeakCanary检查泄漏跟踪中对象的状态,以确定这些对象是否正在泄漏(Leaking: YES vs Leaking: NO),并利用这些信息缩小可疑引用的范围。您可以提供自定义ObjectInspector实现,以改进LeakCanary在代码库中的工作方式(请参阅识别泄漏对象和标记对象)

3.3.3 查找导致泄漏依据

在上一个示例中,LeakCanary缩小了可疑引用的范围ExampleApplication.leakedViews, ArrayList.elementData and Object[].[0]:

┬───
│ GC Root: System class
│
├─ android.provider.FontsContract class
│    ↓ static FontsContract.sContext
├─ com.example.leakcanary.ExampleApplication instance
│    Leaking: NO (Application is a singleton)
│    ↓ ExampleApplication.leakedViews
│                         ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    ↓ Object[].[0]~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
│    ↓ TextView.mContext
╰→ com.example.leakcanary.MainActivity instance

ArrayList.elementDataObject[].[0]ArrayList的实现细节,ArrayList`实现中不太可能有bug,因此导致泄漏的引用是唯一剩下的引用,ExampleApplication.leakedViews;

3.3.4 修复泄漏

一旦找到导致泄漏的引用,您需要弄清楚该引用是关于什么的,它应该在什么时候被清除,以及为什么没有被清除。有时这是显而易见的,就像前面的例子一样。有时你需要更多的信息来解决这个问题。您可以添加标签,或直接浏览hprof(请参阅如何挖掘泄漏痕迹?)

4. 相关遇到问题的文章

4.1 LeakCanary 检测到内存泄露【精确分析】

LeakCanary 检测到内存泄露【精确分析】

LeakCanary管网:https://square.github.io/leakcanary/

  • 4
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DT从零到壹

您的鼓励是我创作最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值