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


    1.LeakCanary简介

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

    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'
    }
    
    • 1
    • 2
    • 3
    • 4

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

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

    D LeakCanary: LeakCanary is running and ready to detect leaks
    
    • 1

    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")
    
    • 1

    如果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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    LeakCanary在转储堆之前等待保留对象的计数达到阈值,并显示具有最新计数的通知。

    在这里插入图片描述
    图1:LeakCanary发现4个保留物体

    D LeakCanary: Rescheduling check for retained objects in 2000ms because found
      only 4 retained objects (< 5 while app visible)
    
    • 1
    • 2

    当应用程序可见时,默认阈值为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
    │
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    点击通知将启动一个提供更多详细信息的活动。稍后,点击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)
    ...
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在上述示例中,泄漏的特征将计算为:

    val leakSignature = sha1Hash(
        "com.example.leakcanary.LeakingSingleton.leakedView" +
        "java.util.ArrayList.elementData" +
        "java.lang.Object[].[x]"
    )
    println(leakSignature)
    // dbfa277d7e5624792e8b60bc950cd164190a11aa
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.2.4 泄漏分类

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

    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    0 APPLICATION LEAKS
    
    ====================================
    1 LIBRARY LEAK
    
    ...
    ┬───
    │ GC Root: Local variable in native code
    │
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.3 修复内存泄漏

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

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

    3.3.1 找到泄漏痕迹

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

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

    class Helper {
    }
    
    class Utils {
      public static Helper helper = new Helper();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    AppWatcher.objectWatcher.watch(Utils.helper)
    
    • 1

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

    ┬───
    │ 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

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

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

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

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

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

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

    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)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    然后,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    总之,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    3.3.4 修复泄漏

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

    4. 相关遇到问题的文章

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

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

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

  • 相关阅读:
    凌恩客户文献|SBB:网络分析和培养组学共同揭示参与秸秆降解的关键微生物类群
    8 位卷王!总结 1135 页 Java 核心面试手册,硬钢 BATJ 一线大厂面试官
    数据结构(C++)[B树(B-树)插入与中序遍历,效率分析]、B+树、B*树、B树系列应用
    Uniapp离线打包SDK-模块配置
    【2024秋招】小米中间件后端开发一面2023-9-13-base武汉
    数据库系统助力企业降本增效的技术要点|Meetup 回顾与预告
    PHP Web 开发基础
    ng Schematics
    Redis 分布式锁
    C++ 线程池
  • 原文地址:https://blog.csdn.net/huang3513/article/details/126127580