Leakcanary分析

简介

LeakCanary是Square公司基于MAT开源的一个工具,用于检测Android的内存泄漏。
官方地址是:https://github.com/square/leakcanary。

使用

引入:

dependencies {
    ...
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
}

如果只监控Activity,引入此库其实就已经满足要求了。
如果需要监控其他的对象,比如Fragment等,就需要在对于对象的销毁方法中手动监听。

 protected void onDestroy() {
        super.onDestroy();
        AppWatcher.INSTANCE.getObjectWatcher().watch(this,"界面泄漏了");
    }

比如上面的代码,就是主动监听Activity对象。
然后运行代码执行销毁操作,等候一段时间,Leakcanary自动生成的图标中就会展示对应抓取的泄漏的信息。如下图所示:


image.png

此时我们可以清楚的看到发生泄漏的地方,还有发生泄漏时的描述。
Leakcanary这么神奇,那么它是如何实现的呢?
下面我们分析一些Leakcanary原理。

Leakcanary原理

在使用Leakcanary2.1的时候有个疑问,为什么只需要引入依赖就可以直接监听到Activity和Fragment的泄漏了?

对比1.5版本使用:

首先我们要添加一下依赖:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

然后Application创建方法中初始化:

public class ExampleApplication extends Application {
 
  @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
    // Normal app init code...
  }
}

LeakCanary.install() 执行后,就会构建 RefWatcher 对象,开始监听 Activity.onDestroy() 回调, 通过RefWatcher.watch() 监测 Activity 引用的泄露情况。发现内存泄露之后进行 heap dump ,利用 Square 公司的另一个库 haha(已废弃)来分析 heap dump 文件,找到引用链之后通知用户。

新版本中直接引用就能监听,到底是如何完成的呢?

image.png

在编译好的apk文件的AndroidManifest.xml文件中,我们找到了AppWatcherInstaller$MainProgress的内容提供者:

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller() {
    override fun onCreate(): Boolean {
      super.onCreate()
      AppWatcher.config = AppWatcher.config.copy(enabled = false)
      return true
    }
  }

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    InternalAppWatcher.install(application)
    return true
  }
...

在这个类中,我们发现onCreate生命周期方法中,调用了InternalAppWatcher.install(application)方法,而这个方法就是初始化的方法。我们知道ContentProvider的onCreate方法在Application的onCreate方法之前调用,因此借助ContentProvider完成了LeakCanary的初始化工作。当然这种方式比较消耗性能,但是因为LeakCanary只在debug环境使用,因此没有受此影响。
接着我们看下InternalAppWatcher.install(application)做了哪些操作:

  fun install(application: Application) {
    SharkLog.logger = DefaultCanaryLog()
    SharkLog.d { "Installing AppWatcher" }
    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)
  }

在初始化方法中最核心的两个操作就是ActivityDestroyWatcher.install和FragmentDestroyWatcher.install,这两个操作是用了监听ActivityFragment泄漏的操作。这两个操作逻辑大体上一致,我们看看ActivityDestroyWatcher的逻辑。

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

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      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)
    }
  }
}

ActivityDestroyWatcher 逻辑很简单,install方法中对Application绑定Activity生命周期回调,在Activity销毁的生命周期回调方法onActivityDestroyed中调用ObjectWatcher的watch方法检查是否内存泄漏,因此我们接着看下ObjectWatcher的逻辑:

class ObjectWatcher constructor(
  private val clock: Clock,
  private val checkRetainedExecutor: Executor,

//泄漏监听
  private val onObjectRetainedListeners = mutableSetOf()

  /**
   *正在被观察的对象,此时还未泄漏
   */
  private val watchedObjects = mutableMapOf()
 
 //引用队列 配合弱引用 定位泄漏的对象。
  private val queue = ReferenceQueue()
...

通过watch传入的对象会被存放到watchedObjects 中,queue是ReferenceQueue类型的队列,配合弱引用使用,当弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。也就是说会被GC回收的对象被存放到队列queue中。

来看看watch方法:

/**
   * Watches the provided [watchedObject].
   *
   * @param description Describes why the object is watched.
   */
  @Synchronized fun watch(
    watchedObject: Any,
    description: String
  ) {
    if (!isEnabled()) {
      return
    }
    
    removeWeaklyReachableObjects() //移除队列中要被GC的引用。
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference = 
      KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue) //生成当前对象的弱引用对象,并与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) //如果当前引用未被移除,仍在watchedObject中,说明未被GC,这时代表泄露,回调onObjectRetained方法。
    }
  }

代码的逻辑还是比较清晰的,首先调用removeWeaklyReachableObjects方法,目的是移除watchedObject中即将被GC的引用。

removeWeaklyReachableObjects方法

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.
   //弱引用一旦弱可达,就会入队,此操作发生在GC之前。
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null) //移除要被GC的对象,剩余就就是可能泄露的对象。
  }

此方法会多次调用,确保将入队的将被GC掉的对象移除掉,避免重复的Heap dump操作,仍在watchedObject的引用则是代表已经泄漏的,此时会回调监听的onObjectRetained方法,这就是moveToRetained的逻辑:

 @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableObjects()
    val retainedRef = watchedObjects[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onObjectRetainedListeners.forEach { it.onObjectRetained() }
    }
  }

这个方法最终会回调到InternalLeakCanary的onObjectRetained方法:

  override fun onObjectRetained() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.onObjectRetained()
    }
  }

在此方法中调用了HeapDumpTriggeronObjectRetained方法,

  fun onObjectRetained() {
    scheduleRetainedObjectCheck(
        reason = "found new object retained",
        rescheduling = false
    )
  }

  private fun scheduleRetainedObjectCheck(
    reason: String,
    rescheduling: Boolean,
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      val scheduledIn = checkCurrentlyScheduledAt - SystemClock.uptimeMillis()
      SharkLog.d { "Ignoring request to check for retained objects ($reason), already scheduled in ${scheduledIn}ms" }
      return
    } else {
      val verb = if (rescheduling) "Rescheduling" else "Scheduling"
      val delay = if (delayMillis > 0) " in ${delayMillis}ms" else ""
      SharkLog.d { "$verb check for retained objects${delay} because $reason" }
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects(reason)//检查泄漏的引用
    }, delayMillis)
  }

checkRetainedObjects是最后一个检查是否泄露的方法,这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump,分析 dump 文件,找到引用链,最后通知用户。整体流程和老版本是一致的,但在一些细节处理,以及 dump 文件的分析上有所区别。下面还是通过源码来看看这些区别。

private fun checkRetainedObjects(reason: String) {
    val config = configProvider()
    // A tick will be rescheduled when this is turned back on.
    if (!config.dumpHeap) {
      SharkLog.d { "Ignoring check for retained objects scheduled because $reason: LeakCanary.Config.dumpHeap is false" }
      return
    }

    var retainedReferenceCount = objectWatcher.retainedObjectCount

    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    //检查泄漏次数是否超过5此,不超过不尽兴dump
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
              R.string.leak_canary_notification_retained_debugger_attached
          )
      )
      scheduleRetainedObjectCheck(
          reason = "debugger is attached",
          rescheduling = true,
          delayMillis = WAIT_FOR_DEBUG_MILLIS
      )
      return
    }

    val now = SystemClock.uptimeMillis()
    val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
    //上一次dump时间和现在时间相隔小于60秒,则使用通知提醒。
    if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
      )

      scheduleRetainedObjectCheck(
          reason = "previous heap dump was ${elapsedSinceLastDumpMillis}ms ago (< ${WAIT_BETWEEN_HEAP_DUMPS_MILLIS}ms)",
          rescheduling = true,
          delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
      )
      return
    }

    SharkLog.d { "Check for retained objects found $retainedReferenceCount objects, dumping the heap" }
    dismissRetainedCountNotification()
    //开始dumpHeap
    dumpHeap(retainedReferenceCount, retry = true)
  }

checkRetainedObjects方法的逻辑是,判定泄漏次数是否小于5次,小于则不进行dump,大于的话判定上一次dump时间和现在时间相隔是否小于60秒,小于则弹出通知提醒,大于则进行dumpHeap

private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
      if (retry) {
        SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
        scheduleRetainedObjectCheck(
            reason = "failed to dump heap",
            rescheduling = true,
            delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
        )
      } else {
        SharkLog.d { "Failed to dump heap, will not automatically retry" }
      }
      showRetainedCountNotification(
          objectCount = retainedReferenceCount,
          contentText = application.getString(
              R.string.leak_canary_notification_retained_dump_failed
          )
      )
      return
    }
    lastDisplayedRetainedObjectCount = 0
    lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

dumpHeap方法使用AndroidHeapDumper类生成dump文件,然后通过HeapAnalyzerService分析dump文件,然后找到最短的路径展示给用户。至于生成dump和分析dump的详细代码有兴趣的可以仔细查阅源码。
到这里Leakcanary的简单逻辑就分析完了。

总结:

1.Leakcanary基于 ContentProvider完成自动初始化工作。
2.Leakcanary对对象的泄漏判定原理是基于弱引用和引用队列来完成的。当弱引用弱可达时,会立刻加入引用队列,然后通过多次调用removeWeaklyReachableObjects方法,移除即将被GC的对象,从而获取到可能泄露的对象。
3.通过checkRetainedObjects方法,检测是否真的泄漏,并且针对泄漏次数是否达到5次来判定是否进行dump。
4.通过AndroidHeapDumper类生成dump文件
5.通过HeapAnalyzerService服务分析dump文件并展现最短路径给用户。

你可能感兴趣的:(Leakcanary分析)