Leakcanary源码解析

284 阅读6分钟

Leakcanary源码分析

原理利用:

当有强引用指向value内存区域时,即使进行gc,弱引用也不会被释放,对象不回被回收。

当无强引用指向value内存区域是,此时进行gc,弱引用会被释放,对象将会执行回收流程。

import java.lang.ref.WeakReference;

/**
 * 弱引用回收测试
 */
public class WeakReferenceDemo {

    public static WeakReference<String> weakReference1;
    public static WeakReference<String> weakReference2;

    public static void main(String[] args) {

        test1();
        //可以输出hello值,此时两个弱引用扔持有对象,而且未进行gc
        System.out.println("未进行gc时,只有弱引用指向value内存区域:" + weakReference1.get());

        //此时已无强一用执行"value"所在内存区域,gc时会回收弱引用
        System.gc();

        //此时输出都为nuill
        System.out.println("进行gc时,只有弱引用指向value内存区域:" + weakReference1.get());

    }

    public static void test1() {
        String hello = new String("value");

        weakReference1 = new WeakReference<>(hello);

        System.gc();
        //此时gc不会回收弱引用,因为字符串"value"仍然被hello对象强引用
        System.out.println("进行gc时,强引用与弱引用同时指向value内存区域:" + weakReference1.get());

    }
}

一、Leakcanary初始化:

1.1、分析版本:

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

1.2、初始化

image.png

如上图看MainProcessAppWatcherInstaller这个,利用ContentProvider的特性进行自动初始化。

该变量控制是否自动初始化
leak_canary_watcher_auto_install

AppWatcher.manualInstall(application)方法进行初始化

image.png

二、源码分析

2.1、分析AppWatcher.manualInstall(application)方法进行初始化

@JvmOverloads
fun manualInstall(
  application: Application,
  retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
  watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
) {
  checkMainThread()
  if (isInstalled) {
    throw IllegalStateException(
      "AppWatcher already installed, see exception cause for prior install call", installCause
    )
  }
  check(retainedDelayMillis >= 0) {
    "retainedDelayMillis $retainedDelayMillis must be at least 0 ms"
  }
  this.retainedDelayMillis = retainedDelayMillis
  if (application.isDebuggableBuild) {
    LogcatSharkLog.install()
  }
  // Requires AppWatcher.objectWatcher to be set
  LeakCanaryDelegate.loadLeakCanary(application)

  watchersToInstall.forEach {
    it.install()
  }
  // Only install after we're fully done with init.
  installCause = RuntimeException("manualInstall() first called here")
}

我们要关注的就是watchersToInstall这个参数,这个参数默认使用appDefaultWatchers()方法进行赋值,走进去看下:

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher //`ObjectWatcher`方法下面会提到
): List<InstallableWatcher> {
  return listOf(
    
    //Activity的监测
    ActivityWatcher(application, reachabilityWatcher),
    //Fragment的监测
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    //RootView的监测
    RootViewWatcher(reachabilityWatcher),
    //Service的监测
    ServiceWatcher(reachabilityWatcher)
  )
}

从上面的名称可以得知进行不同的监测。

2.2、分析activity的监测流程ActivityWatcher(application, reachabilityWatcher)


/**
 * Expects activities to become weakly reachable soon after they receive the [Activity.onDestroy]
 * callback.
 */
class ActivityWatcher(
  private val application: Application,
  private val reachabilityWatcher: ReachabilityWatcher
) : InstallableWatcher {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        reachabilityWatcher.expectWeaklyReachable(
          activity, "${activity::class.java.name} received Activity#onDestroy() callback"
        )
      }
    }

  override fun install() {
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
  }

  override fun uninstall() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks)
  }
}

上面的by noOpDelegate()方法使用了动态代理不过里面没有写任何逻辑 所以是no op 此处相当于一个适配器方法、感兴趣的可以点进去看看。

onActivityDestroyed方法就是监测Activity销毁的方法,里面实现了reachabilityWatcher.expectWeaklyReachable

reachabilityWatcher.expectWeaklyReachable(
  activity, "${activity::class.java.name} received Activity#onDestroy() callback"
)
fun interface ReachabilityWatcher {

  /**
   * Expects the provided [watchedObject] to become weakly reachable soon. If not,
   * [watchedObject] will be considered retained.
   
   翻译:期望提供的[watchdobject]很快成为弱可访问的。如果不是这样,\
    [watchdobject]将被视为保留
   */
  fun expectWeaklyReachable(
    watchedObject: Any,
    description: String
  )
}

上面该方法只有一个实现:ObjectWatcher 他在上面的appDefaultWatchers方法中提到过初始化

2.3、ObjectWatcher类的expectWeaklyReachable方法预计弱可及

1.利用弱引用WeakReference弱引用队列ReferenceQueue相绑定,被gc成功回收的对象会出现在弱引用队列ReferenceQueue中,通过方法removeWeaklyReachableObjects判断ReferenceQueue中是否存在当前对象,如果存在证明被成功回收,所以不存在泄漏,就将对象从watchedObjects观察表中移除。

2.如果gc后弱引用队列ReferenceQueue没有当前对象,则怀疑当前对象可能存在泄漏

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  if (!isEnabled()) {
    return
  }
  //注意1.1.  
  //将ReferenceQueue中出现的弱引用移除
  //这是一个出现频率很高的方法,也是内存泄漏检测的关键点之一
  removeWeaklyReachableObjects()
  
  val key = UUID.randomUUID()
    .toString()
    
 // 记下观测开始的时间
  val watchUptimeMillis = clock.uptimeMillis()
  
  //注意2.1. 这里创建了一个自定义的弱引用,且调用了基类的WeakReference<Any>(referent,   referenceQueue)构造器,
  //这样的话,弱引用被回收的话会出现在ReferenceQueue中
  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"
  }

   // 将key-reference保存到map中
  watchedObjects[key] = reference
  
  //注意3. ---主线程5秒之后执行moveToRetained(key)方法
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}

先看第一点removeWeaklyReachableObjects:

将ReferenceQueue中出现的弱引用移除 这是一个出现频率很高的方法,也是内存泄漏检测的关键点之一

就会将引用队列中出现的对象从map中移除,因为它们没有发生内存泄漏 (弱引用是不会阻止 GC 回收对象的)

注释翻译:一旦WeakReferences所指向的对象变为弱引用,WeakReferences就会被加入ReferenceQueue队列可及。
这是在最终化或垃圾收集实际发生之前

private fun removeWeaklyReachableObjects() {
  // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
  // reachable. This is before finalization or garbage collection has actually happened.
  var ref: KeyedWeakReference?
  do {
    ref = queue.poll() as KeyedWeakReference?
    if (ref != null) {
      watchedObjects.remove(ref.key)
    }
  } while (ref != null)
}

ReferenceQueue的poll方法解释说明: blog.csdn.net/u013679744/…

看第二点KeyedWeakReference

首先我们要知道 KeyedWeakReference 继承于 WeakReference,弱引用是不会阻止 GC 回收对象的,同时我们可以在构造函数中传递一个 ReferenceQueue用于对象被 GC 后存放的队列。 (意思是:弱引用对象会出现在ReferenceQueue队列中)

类注释翻译:一个弱引用,ObjectWatcher使用它来确定哪些对象是弱可及的,
哪些对象不是。ObjectWatcher使用key来跟踪还没有进入相关ReferenceQueueKeyedWeakReference实例。\
heapDumpUptimeMillis应该设置为时钟的当前时间。uptimeMillis正好在转储堆之前,
这样我们就可以在以后确定一个对象被保留了多长时间。

class KeyedWeakReference(
  referent: Any,
  val key: String,
  val description: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
  referent, referenceQueue
) {
  /**
   * Time at which the associated object ([referent]) was considered retained, or -1 if it hasn't
   * been yet.
   */
  @Volatile
  var retainedUptimeMillis = -1L

  override fun clear() {
    super.clear()
    retainedUptimeMillis = -1L
  }

  companion object {
    @Volatile
    @JvmStatic var heapDumpUptimeMillis = 0L
  }
}

所以结合上面两点我们知道: expectWeaklyReachable方法的流程如下:

所以 removeWeaklyReachableObjects() 方法的作用就是将已经被 GC 的对象从 watchedObjects 集合中删除。(因为被回收的弱引用对象会出现在ReferenceQueue中) 当我们调用 expectWeaklyReachable 方法时,使用removeWeaklyReachableObjects()先清理已经入队的弱引用对象,接着将需要观察的对象,存储为一个 KeyedWeakReference 的弱引用对象,这样的话弱引用被回收的话会出现在ReferenceQueue中,再存放到 watchedObjects 集合中,最后使用 checkRetainedExecutor 安排一次 moveToRetained 任务,这个任务是怀疑当前的对象存在泄漏。

checkRetainedExecutor 是使用 Handler 实现,默认延迟 5s 执行任务。

  1. 将要观测的对象使用WeakReference保存起来,并在构造时传入一个ReferenceQueue,这样待观测的对象在被回收之前,会出现在ReferenceQueue中。
  2. 5秒钟之后再检查一下是否出现在了引用队列中,若出现了,则没有泄露。

看第三点moveToRetained

@Synchronized private fun moveToRetained(key: String) {
 removeWeaklyReachableObjects()
 val retainedRef = watchedObjects[key]
 if (retainedRef != null) {//这个任务是怀疑当前的对象存在泄漏。加入到一个怀疑列表
   retainedRef.retainedUptimeMillis = clock.uptimeMillis()
   onObjectRetainedListeners.forEach { it.onObjectRetained() }
 }
}

小结:

5秒钟到了,如果它们没有内存泄漏,所以出现在了ReferenceQueue--queue队列中,然后将引用队列中出现的对象从map中移除--watchedObjects.remove(ref.key),。然后判断watchedObjects[key]对象是否还存在,如果在的话,说明可能发生了内存泄漏(因为对象不在前面的ReferenceQueue队列中)。此时记下内存泄漏发生的时间,即更新retainedUptimeMillis字段,然后通知所有的对象,内存泄漏发生了。

2.4、objectWatcher类中的onObjectRetainedListeners泄漏回调

@Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
  onObjectRetainedListeners.add(listener)
}
internal object InternalLeakCanary : (Application) -> Unit, OnObjectRetainedListener {//继承接口
...

//看这个
override fun onObjectRetained() = scheduleRetainedObjectCheck()


override fun invoke(application: Application) {
  _application = application

  checkRunningInDebuggableBuild()

    //添加自己
  AppWatcher.objectWatcher.addOnObjectRetainedListener(this)
  
  ...
  }
  ...
  }

InternalLeakCanary类中的invoke(application: Application)方法调用addOnObjectRetainedListener

从上可知: 看InternalLeakCanary类中的onObjectRetained()方法

scheduleRetainedObjectCheck

fun scheduleRetainedObjectCheck() {
  if (this::heapDumpTrigger.isInitialized) {
    heapDumpTrigger.scheduleRetainedObjectCheck()
  }
}

跟踪一下:HeapDumpTrigger中的scheduleRetainedObjectCheck

fun scheduleRetainedObjectCheck(
  delayMillis: Long = 0L
) {
  val checkCurrentlyScheduledAt = checkScheduledAt
  if (checkCurrentlyScheduledAt > 0) {
    return
  }
  checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
  backgroundHandler.postDelayed({
    checkScheduledAt = 0
    checkRetainedObjects()
  }, delayMillis)
}
private fun checkRetainedObjects() {
  val iCanHasHeap = HeapDumpControl.iCanHasHeap()

  val config = configProvider()

  if (iCanHasHeap is Nope) {
    if (iCanHasHeap is NotifyingNope) {
      // Before notifying that we can't dump heap, let's check if we still have retained object.
      var retainedReferenceCount = objectWatcher.retainedObjectCount

      if (retainedReferenceCount > 0) {
        gcTrigger.runGc()
        retainedReferenceCount = objectWatcher.retainedObjectCount
      }

      val nopeReason = iCanHasHeap.reason()
      val wouldDump = !checkRetainedCount(
        retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
      )

      if (wouldDump) {
        val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
        onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
        showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = uppercaseReason
        )
      }
    } else {
      SharkLog.d {
        application.getString(
          R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
        )
      }
    }
    return
  }

  var retainedReferenceCount = objectWatcher.retainedObjectCount

  if (retainedReferenceCount > 0) {
    gcTrigger.runGc()
    retainedReferenceCount = objectWatcher.retainedObjectCount
  }

  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

  val now = SystemClock.uptimeMillis()
  val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
  if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
    onRetainInstanceListener.onEvent(DumpHappenedRecently)
    showRetainedCountNotification(
      objectCount = retainedReferenceCount,
      contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
    )
    scheduleRetainedObjectCheck(
      delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
    )
    return
  }

  dismissRetainedCountNotification()
  val visibility = if (applicationVisible) "visible" else "not visible"
  dumpHeap(
    retainedReferenceCount = retainedReferenceCount,
    retry = true,
    reason = "$retainedReferenceCount retained objects, app is $visibility"
  )
}

上面的代码就是对于内存泄漏判定的代码了:

首先进入onObjectRetained方法,该方法会调用scheduleRetainedObjectCheck方法。此方法也就是在后台线程中执行checkRetainedObjects方法来检查泄漏的对象:

  1. 首先获取泄漏对象的个数,如果大于0,则GC一次之后再次获取
  2. 如果此时泄漏对象的个数大于等于5个 注:config.retainedVisibleThreshold对象=5,则继续执行下面的代码,准备dump heap
  3. 如果config里面配置的“调试时不允许dump heap”为false(默认值)且正在调试,则20s之后再试
  4. 否则可以开始dump heap:此时会先记下dump发生的时间,取消内存泄漏通知,dump heap,清除所有观测事件小于等于dump发生时间的对象(因为这些对象已经处理完毕了),最后运行HeapAnalyzerService开始分析heap。

泄漏个数计算

是如何获取泄露对象的个数的呢?我们想一下,在前面的代码中,主线程5秒之后执行了一段检测的代码,在这里面将所有泄露的对象都记下了当时的时间,存在retainedUptimeMillis字段里面。那么我们遍历所有元素,统计一下该字段不为默认值(-1)的个数即可:

注释翻译:
返回被保留的对象的数量,即被监视的对象的数量,这些对象不是弱可达的,并且被监视的时间足够长而被认为是被保留的。

Returns the number of retained objects, ie the number of watched objects that aren't weakly reachable, and have been watched for long enough to be considered retained.

val retainedObjectCount: Int
  @Synchronized get() {
    removeWeaklyReachableObjects()
    return watchedObjects.count { it.value.retainedUptimeMillis != -1L }
  }

上面提到:如果有内存泄漏的话,会调用gcTrigger.runGc()方法,这里的gcTrigger我们提到过,是GcTrigger.Default

/**
 * [GcTrigger] is used to try triggering garbage collection and enqueuing [KeyedWeakReference] into
 * the associated [java.lang.ref.ReferenceQueue]. The default implementation [Default] comes from
 * AOSP.
 */
fun interface GcTrigger {

  /**
   * Attempts to run garbage collection.
   */
  fun runGc()

  /**
   * Default implementation of [GcTrigger].
   */
  object Default : GcTrigger {
    override fun runGc() {
      // Code taken from AOSP FinalizationTest:
      // https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
      // java/lang/ref/FinalizationTester.java
      // System.gc() does not garbage collect every time. Runtime.gc() is
      // more likely to perform a gc.
      Runtime.getRuntime()
        .gc()
      enqueueReferences()
      System.runFinalization()
    }

    private fun enqueueReferences() {
      // Hack. We don't have a programmatic way to wait for the reference queue daemon to move
      // references to the appropriate queues.
      try {
        Thread.sleep(100)
      } catch (e: InterruptedException) {
        throw AssertionError()
      }
    }
  }
}

GcTrigger 上面注释代码提到:System.gc()不会每次都进行垃圾收集。Runtime.gc ()更可能执行gc。 执行一次GC操作之后,下面粗暴的等待100ms,这样有足够的时间可以让弱引用移动到合适的引用队列里面。这就是GcTrigger.Default所干的事情。

GCTrigger触发GC之后,再次判断一下发生内存泄漏的对象的个数,如果仍然还有,那么肯定是泄漏无疑了,实锤!! 随后调用checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)方法,判断泄漏对象的个数是否达到了阈值,如果达到了则直接dump heap;否则发出一个内存泄漏的通知。

我们看一下这个方法:checkRetainedCount


private fun checkRetainedCount(
  retainedKeysCount: Int,
  retainedVisibleThreshold: Int,
  nopeReason: String? = null
): Boolean {
// lastDisplayedRetainedObjectCount默认值为0,此处我们肯定有内存泄漏,因此countChanged为true
  val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
  
  // 保存下当前的内存泄漏对象的个数
  lastDisplayedRetainedObjectCount = retainedKeysCount
  
  // 如果内存泄漏个数为0,则说明已经处理了所有的内存泄漏
  if (retainedKeysCount == 0) {
    if (countChanged) {
      SharkLog.d { "All retained objects have been garbage collected" }
      onRetainInstanceListener.onEvent(NoMoreObjects)
      showNoMoreRetainedObjectNotification()
    }
    return true
  }

  val applicationVisible = applicationVisible
  val applicationInvisibleLessThanWatchPeriod = applicationInvisibleLessThanWatchPeriod

  if (countChanged) {
    val whatsNext = if (applicationVisible) {
      if (retainedKeysCount < retainedVisibleThreshold) {
        "not dumping heap yet (app is visible & < $retainedVisibleThreshold threshold)"
      } else {
        if (nopeReason != null) {
          "would dump heap now (app is visible & >=$retainedVisibleThreshold threshold) but $nopeReason"
        } else {
          "dumping heap now (app is visible & >=$retainedVisibleThreshold threshold)"
        }
      }
    } else if (applicationInvisibleLessThanWatchPeriod) {
      val wait =
        AppWatcher.retainedDelayMillis - (SystemClock.uptimeMillis() - applicationInvisibleAt)
      if (nopeReason != null) {
        "would dump heap in $wait ms (app just became invisible) but $nopeReason"
      } else {
        "dumping heap in $wait ms (app just became invisible)"
      }
    } else {
      if (nopeReason != null) {
        "would dump heap now (app is invisible) but $nopeReason"
      } else {
        "dumping heap now (app is invisible)"
      }
    }

    SharkLog.d {
      val s = if (retainedKeysCount > 1) "s" else ""
      "Found $retainedKeysCount object$s retained, $whatsNext"
    }
  }
    // 如果泄漏个数小于5个
  if (retainedKeysCount < retainedVisibleThreshold) {
    if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
      if (countChanged) {
        onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
      }
      // 展示一个内存泄漏发生的通知
      showRetainedCountNotification(
        objectCount = retainedKeysCount,
        contentText = application.getString(
          R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
        )
      )
      // 2秒钟之后再次执行检查泄漏对象的方法,看看泄漏个数是否有变化
      scheduleRetainedObjectCheck(
        delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
      )
      return true
    }
  }
  1.  // 如果泄漏个数大于等于5个,返回false,则返回后checkRetainedObjects方法会继续执行
1.  // 此时就会dump heap
  return false
}

原理:

LeakCanary 2检测内存泄漏原理: 在Activity destroy后将Activity的弱引用关联到ReferenceQueue中,这样Activity将要被GC之前,会出现在ReferenceQueue中。随后,会向主线程的抛出一个5秒后执行的Runnable,用于检测内存泄漏。 这段代码首先会将引用队列中出现的对象从观察对象数组中移除,然后再判断要观察的此对象是否存在。若不存在,则说明没有内存泄漏,结束。否则,就说明可能出现了内存泄漏,会调用Runtime.getRuntime().gc()进行GC,等待100ms后再次根据引用队列判断,若仍然出现在引用队列中,那么说明有内存泄漏,此时根据内存泄漏的个数弹出通知或者开始dump hprof。

参考:

源码分析参考:juejin.cn/post/711431…

源码分析参考:juejin.cn/post/684490…

弱引用对象:www.jianshu.com/p/cdb2ea379…

弱引用对象:www.jianshu.com/p/cdcc56f5b…

ReferenceQueue:blog.csdn.net/u013679744/…