锦囊篇|一文摸懂LeakCanary

8,708 阅读9分钟

LeakCanary泄漏目标推测

LeakCanary想来也是我们的一个老朋友了,但是它是如何做到对我们的App进行内存泄漏分析的呢?这也是我们今天要去研究的主题了。

我们要先思考的第一个问题也就是App中已经存在泄漏了,那我们该怎么知道他泄漏了呢?

🤔🤔🤔,我么应该知道在JVM中存在这样的两种关于实例是否需要回收的算法:

  1. 引用计数法
  2. 可达性分析法

引用计数法

对于 引用计数法 而言,存在一个非常致命的循环引用问题,下面我们将用图分析一下。 类A和类B作为一个实例,那么类A和类B的计数0 -> 1,不过我们能够注意到里面还有一个叫做Instance的对象分别指向了对方的实例,即类A.Instance = 类B类B.Instance = 类A,那么这个时候类A和类B的计数为1 -> 2了,即使我们做了类A = null类B = null这样的操作,类A和类B的计数也只是从2 -> 1,并未变成0,也就导致了内存泄漏的问题。

可达性分析法

和引用计数法比较,可达性分析法多了一个叫做GC Root的概念,而这些GC Roots就是我们可达性分析法的起点,在周志明前辈的《深入理解Java虚拟机》中就已经提到过了这个概念,它主要分为几类:

  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池里的引用。
  • 在本地方法栈中JNI引用的对象。
  • 在Java虚拟机栈中引用的对象,譬如Android的主入口类ActivityThread
  • 所有被同步锁持有的对象。
  • 。。。。。

我们同样用上述循环引用的案例作为分析,看看可达性分析是否还会出现这样的内存泄漏问题。 这个时候类B = null了,那会发生什么样的状况呢?

既然类B = null,那么我们的Instance也应该等于null,这个时候也就少掉一根引用线,我们在上面说过了,可达性分析法的起点,这时候再从Roots进行可达性分析时发现类B不再存在通路到达,那类B就会被加上清理标志,等待GC的到来。

知道了我们的两种泄漏目标检查的方案,我们就看看在LeakCanary中到底是不是通过这两种方案实现?如果不是,那他的实现方式又是什么呢?

LeakCanary使用方法

看了很多使用介绍的博客,但是我用Version 2.X时,发现一个问题,全都没有LeakCanary.install(this)这样的函数调用,后来才知道是架构重构过,实现了静默加载,不需要我们手动再去调用了。

下面是我使用的最新版本:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.3'

给出一个可以跑出内存泄漏的Demo,也就是一个单例模式,你要做的是在Activity1中实现往Activity2的跳转功能,Activity2实例化单例,这样再进行返回后就能查看到LeakCanary给我们放出的内存泄漏问题了。

public class Singleton {
    private Context context;

    private Singleton(Context context){
        this.context = context;
    }

    public static class Holder{
        private static Singleton Instance;

        public static Singleton getInstance(Context context){
            if(Instance == null) {
                synchronized (Singleton.class){
                    if (Instance == null) Instance = new Singleton(context);
                }
            }
            return Instance;
        }
    }
}

发生内存泄漏时,你要去通知栏中进行查看 点击,并等待他拉取完成之后我们就可以开始从一个叫做Leaks的App中进行查看了。

能看到已经判定了instance这个实例已经发生了泄漏,原因是什么?

因为Activity2已经被销毁了,但是context依旧被持有,导致Activity2无法被清理,而我们又不会再使用到,也就发生了内存泄漏。如果你把context修改成context.getApplicationContext()也就能解决这个问题了,因为单例中的context的周期这个时候已经修改成和Application一致,那么Activity2的清理时因为context不再和单例中所保存的一致,所以不会导致泄漏的发生。

LeakCanary是如何完成任务的?

Version 1.X来看的话,观测的加载是需要LeakCanary.install()这样的代码去进行调用的,那我们就从这个角度进行切入,看看Version 2.X将他做了怎样的重构。

通过全局搜索,我们能够定位到一个类叫做AppWatcherInstaller,但是有一点奇怪的事情发生,它继承了ContentProvider这个四大组件。

/**
 * Content providers are loaded before the application class is created. [AppWatcherInstaller] is
 * used to install [leakcanary.AppWatcher] on application start.
 * Content providers比Application更早进行创建,这个类的存在是为了加载AppWatcher
 */

既然时为了去加载AppWatcher,那我们就先去看看AppWatcher这尊大佛到底能干些什么事情呢?🤔🤔🤔🤔

AppWatcher —— 内存泄漏检查的发起人

截取必要代码如下:

object AppWatcher {

    data class Config(
    // AppWatcher时刻关注对象的使能变量
    val enabled: Boolean = InternalAppWatcher.isDebuggableBuild,
    // AppWatcher时刻关注销毁Activity实例的使能变量
    val watchActivities: Boolean = true,
    // AppWatcher时刻关注销毁Fragment实例的使能变量
    val watchFragments: Boolean = true,
    // AppWatcher时刻关注销毁Fragment View实例的使能变量
    val watchFragmentViews: Boolean = true,
    // AppWatcher时刻关注销毁ViewModel实例的使能变量
    val watchViewModels: Boolean = true,
    // 对于驻留对象做出汇报时间的设置
    val watchDurationMillis: Long = TimeUnit.SECONDS.toMillis(5)
  ) {
  var config;
  // 用于监测依旧存活的对象
  val objectWatcher
    get() = InternalAppWatcher.objectWatcher

  val isInstalled
    get() = InternalAppWatcher.isInstalled
  }
}

不然发现这里面唯一作出监测动作的对象也仅仅只有一个,也就是ObjectWatcher。那我们猜测在代码中肯定会有对ObjectWatcher的使用,那我们现回归到AppWatcherInstaller中,能够发现他重写了一个叫做onCreate()的方法,并且做了这样的一件事InternalAppWatcher.install(application),那我们就进到里面去看看。

fun install(application: Application) {
    SharkLog.logger = DefaultCanaryLog()
    // 检查当前是否在主线程
    checkMainThread()
    if (this::application.isInitialized) {
      return
    }
    InternalAppWatcher.application = application

    val configProvider = { AppWatcher.config }
    ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
    FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
    onAppWatcherInstalled(application)
  }

代码中说到会加载销毁时ActivityFragment的观察者,那我们就挑选Activity的源码来进行查看。

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      // 重写生命周期中的onActivityDestroyed()方法
      // 说明在销毁时才开始调用监察
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          objectWatcher.watch(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
          )
        }
      }
    }
  
  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider)
      application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

这一个类中最关键的地方我已经写了注释,你能够看到ObjectWatcher这个类被第二次使用了,而且使用了一个叫做watch()的方法,那肯定有必要去看看🥴🥴🥴🥴。

ObjectWatcher —— 检测的执行者

class ObjectWatcher constructor(
  private val clock: Clock,
  // 通过池来检查是否还有被保留的实例
  private val checkRetainedExecutor: Executor,
  private val isEnabled: () -> Boolean = { true }
) {
  // 没有被回收的实例的监听
  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
  // 通过特定的String,也就是UUID,与弱引用关联
  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
  // 一个引用队列
  private val queue = ReferenceQueue<Any>()
  
  // 上文中提到的被调用用来做监测的方法
  @Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    // 删去弱可达的对象
    removeWeaklyReachableObjects()
    // 随机生成ID作为Key,保证了唯一性
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    // 通过activity、引用队列等构建弱引用
    val reference =
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
    SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (description.isNotEmpty()) " ($description)" else "") +
          " with key $key"
    }

    watchedObjects[key] = reference
    // 一个非常赞的设计,加入了线程池来异步处理
    checkRetainedExecutor.execute {
      moveToRetained(key) // 1-->
    }
  }
  // 1-->
  @Synchronized private fun moveToRetained(key: String) {
    // 进行了第二次的删去弱可达的对象
    // 用于二次确认,保证数据的正确性
    removeWeaklyReachableObjects()
    // 这个时候我们的数据应该需要验证是否已经为空
    val retainedRef = watchedObjects[key]
    // 如果数据不为空说明发生了泄漏
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis() 
      onObjectRetainedListeners.forEach { it.onObjectRetained() } // 2!!!
    }
  }
  // 数据已经被加载到引用队列中
  // 说明这个原有的强引用实例已经置空,并且被监测到了
  // 并且这一系列操作会在标记以及gc到来前完成
  private fun removeWeaklyReachableObjects() {
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
}

对于从上面一连串的流程分析中我们已经知道了当前的实例是否有发生泄漏,但是存在一个问题,它是如何进行报告的产生的?

是谁对这一切进行了操作呢?

对于LeakCanary来说,我分析到上文代码中注释2 的位置,知道他肯定做了事情,但是到底做了什么呢,发出通知,生成文件这些操作呢??? 🤕🤕🤕🤕

Version 1.X的源码中我们知道有这样的一个类LeakCanary,我们在Version 2.X是否还存在呢?毕竟我们上文中一直是没有提到过这样的一个类的。那就Search一下好了。

通过搜索发现是存在的,并且在文件的第一行写了这样的一句话

这是一个建立在AppWatcher上层的类,AppWatcher会对其进行通知,让他完成堆栈的分析,那他是如何完成这项操作的呢?

下游如何通知的上游

观察ObjectWatcher,我们能够发现这样的一个变量,以及他的配套方法

private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()
// 添加观察者
@Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.add(listener)
  }
// 删除观察者
@Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.remove(listener)
  }

添加和删除方法,这和什么设计模式有点像呢???

没错了,观察者模式!!,既然LeakCanary是上游,那我们就把他当成观察者,而AppWacther就是我们的主题了。

通过调用我们能够发现这样的一个问题,在InternalLeakCanaryinvoke方法中就完成了一个添加操作

AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

Heap Dump Trigger -- 报告文件生成触发者

回到我们之前已经讲过的代码onObjectRetainedListeners.forEach { it.onObjectRetained() },通过深层调用我们能够发现,他其实深层的调用的就是如下的代码

override fun onObjectRetained() {
    if (this::heapDumpTrigger.isInitialized) { 
      heapDumpTrigger.onObjectRetained() // 1 -->
    }
  }
// 1 -->
private fun scheduleRetainedObjectCheck(
    reason: String,
    rescheduling: Boolean,
    delayMillis: Long = 0L
  ) {
    // 一些打印。。。。
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects(reason) // 2-->
    }, delayMillis)
  }

我标注了注释2的位置,他希望再进行一次对象的检查操作。

private fun checkRetainedObjects(reason: String) {
    var retainedReferenceCount = objectWatcher.retainedObjectCount
    // 再进行了一次GC操作
    // 这是为了保证我们的数据不存在因为GC没有来临而强行被进行了计算的一个保证操作
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return // 1 -->
    // 。。。。
    dumpHeap(retainedReferenceCount, retry = true) // 其实这是最后进入的输出泄漏文件的位置
  }
  
// 1 -->
private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int
  ): Boolean {
    val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
    lastDisplayedRetainedObjectCount = retainedKeysCount
    // 如果发现经过GC之后实例全部已经清除,直接返回
    if (retainedKeysCount == 0) {
      SharkLog.d { "Check for retained object found no objects remaining" }
      return true
    }
    // 泄漏的数量小于5时,会发出通知栏进行提醒
    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        if (countChanged) {
          onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
        }
        showRetainedCountNotification() // 发出通知
        scheduleRetainedObjectCheck() // 再次进行检查
        return true
      }
    }
    return false
  }

当然LeakCanary也有着自己去强行进行文件生成的方案,其实这个方案我们也经常用到,就是他的泄漏数据还没有满出到设定的值,但是我们已经想去打印报告了,也就是直接去点击了通知栏要求打印出分析报告。

fun dumpHeap() = InternalLeakCanary.onDumpHeapReceived(forceDump = true) // 1 -->

// 1 -->
fun onDumpHeapReceived(forceDump: Boolean) {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onDumpHeapReceived(forceDump) // 2 -->
    }
  }

fun onDumpHeapReceived(forceDump: Boolean) {
    backgroundHandler.post {
      dismissNoRetainedOnTapNotification()
      // 和前面一样会做一次数据的保证操作
      gcTrigger.runGc()
      val retainedReferenceCount = objectWatcher.retainedObjectCount
      // 不强制打印,且泄漏数量为0 时才能不打印
      if (!forceDump && retainedReferenceCount == 0) {
        // ......
        return@post
      }
        
      SharkLog.d { "Dumping the heap because user requested it" }
      dumpHeap(retainedReferenceCount, retry = false) // 完成分析报告数据打印
    }
  }

Dump Heap —— 报告文件的生成者

说到报告文件的生成,其实这已经不是我们主要关注的内容了,但是也值得一提。

他是通过一个服务的方式存在,完成了我们的数据分析报告的打印。其实的内部还有很多有意思的地方,透明的权限请求等等。不过这一次的hprof的生成我不清楚还是不是用的第三方库,不过给我的感觉应该是自己造了一个。

总结

我们上面讲了很多关于源码的内容,重新梳理一下框架,其实无非就四个内容。

注意点

  1. 观察者模式
  2. 对象泄漏的观察方式:弱引用 + 引用队列