天天看点

LeakCanary 使用及原理分析

一、基础

1、添加依赖
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
}
           
2、基本原理

2.1 什么是内存泄漏

在基于Java的runtime状态下,内存泄漏是一种变成错误,导致应用保留对无用对象引用,从而引起内存无法回收,导致OOM崩溃。

2.2 内存泄漏的常见原因

大多数内存泄漏时由与生命周期相关的错误引起的,常见Android错误:

  1. Fragment 实例添加到栈中但是没有在onDestoryView()方法中释放
  2. Activity 实例引用作为context字段引用,导致配置变更时activity重建无法释放
  3. 注册广播、监听、或者RxJava添加订阅,未根据生命周期做响应的释放
  4. 数据库查询时cursor使用完毕没有关闭导致
  5. bitmap使用完毕没有置空释放
  6. RecyclerView与NestedScrollView嵌套错误导致无法复用释放资源
3、为什么要使用LeakCanary

内存泄漏在Android应用中非常普遍,小的内存泄漏积累最终会导致OOM异常。LeakCanary可以在开发中帮助发现和修复内存泄漏。

4、LeakCanary是怎么工作的

一旦LeakCanary安装后,会自动检测和报告内存泄漏,主要分为4步:

  1. Detecting retained objects. 检测保留的对象
  2. Dumping the heap. 倾倒堆
  3. Analyzing the heap. 分析堆
  4. Categorizing leaks. 对内存泄漏进行分类

4.1 Detecting retained objects 检测保留对象

  • LeakCanary hook了Android生命周期实现自动检测Activity和Fragment的销毁,并进行垃圾收集。这些被销毁的对象被传递给一个ObjectWatcher,它持有对他们的弱引用,LeakCanary自动检测以下对象的泄漏:
  1. 销毁的Activity实例
  2. 销毁的Fragment实例
  3. 销毁的片段View实例
  4. 清除ViewModel实例

可以通过AppWatcher来对对象的回收过程进行监控

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
           

4.2 Dumping the heap 倾倒堆

当持有的对象到达阀值时,LeakCanary会倾倒Java的堆内存到.hprof文件中,并存储在Android文件系统中。在倾倒堆时会造成App短暂停止,LeakCanary会展示一个如下Toast

LeakCanary 使用及原理分析

4.3 Analyzing the heap 堆分析

  • LeakCanary通过使用Shark库来解析.hprof文件,并定位堆垃圾场中持有的对象
    LeakCanary 使用及原理分析
  • 针对每个保留的对象,LeakCanary查询阻止其被垃圾回收的引用路径,进行泄漏跟踪。分析完成后会显示带有摘要的通知,并将结果打印在logcat中。LeakCanary会为每个泄漏trace创建签名,并根据签名对其进行分组
====================================
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会为每个集成其的app在手机上添加一个启动图标
    LeakCanary 使用及原理分析
  • LeakCanary每行日志对应一组具有相同签名的泄漏。LeakCanary在应用程序第一次使用该签名触发泄漏时将一行标记为new
    LeakCanary 使用及原理分析
  • 点击每个泄漏可以通过查看详细信息
    LeakCanary 使用及原理分析
    LeakCanary 使用及原理分析

4.4 Categorizing leaks 泄漏分类

LeakCanary会将应用中的内存泄漏分为两类:
  1. 应用程序泄漏
  2. Library 泄漏

    并在logcat打印的日志中分开

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

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

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

并在泄漏列表中进行标记为Library Leak

LeakCanary 使用及原理分析
5、修复内存泄漏

内存泄漏是由于程序错误导致的APP对不再使用的对象进行引用

可以通过下面4个步骤来修复内存泄漏问题:

  1. Find the leak trace. 找到泄漏痕迹
  2. Narrow down the suspect references. 缩小可疑引用
  3. Find the reference causing the leak. 找到导致泄漏的引用
  4. Fix the leak. 修复泄漏问题

5.1 找到泄漏问题

leak trace 是最强引用路径的短名,即从垃圾回收器根路径到内存中持有的对象的路径引用链
  • 例:存储helper实例静态引用
class Helper {
}

class Utils {
  public static Helper helper = new Helper();
}
           
  • 通过LeakCanary 进行检测
AppWatcher.objectWatcher.watch(Utils.helper)
           
  • 泄漏trace
┬───
│ 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
           

通过日志我们知道是Utils中泄漏了helper实例

5.2 缩小可疑引用范围

  • 例:
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)
  }
}
           
  • leak trace
┬───
│ 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
           
┬───
│ 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
           
通过分析leak trace,显示是MainActivity 里面的textView的mContext一直持有引用,导致MainActivity销毁后无法释放

5.3 找到泄漏的引用

通过前面的分析我们知道了ArrayList.elementData和Object[].[0]是实现细节,实现中ArrayList不太可能存在错误ArrayList,因此导致泄漏的引用是唯一剩余的引用:ExampleApplication.leakedViews

┬───
│ 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
           

5.4 修复泄漏问题

通过分析我们找到了泄漏点,解决方法也很简单,直接将application中leakedViews在的destory()方法中置空就行了

参考:https://square.github.io/leakcanary/

继续阅读