LeakCanary源码解析(一)

Posted by CoXier on July 17, 2019

本次源码解析的 commit 是:660d6742fccda2f9f3f84a8c4acc437412a262a3

一、LeakCanary 初始化

LeakCanary 是通过 ContentProvider 初始化的,目前很多第三方库比如 picasso 都采用了这种方式进行初始化,这种初始化有个好处,可以避免入侵使用方的代码。具体方式如下:

  • 自定义 ContentProvider,如 LeakSentryInstaller,然后在 onCreate 方法中执行第三方库的初始化操作
  • 在 AndroidManifest.xml 中声明 ContentProvider
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary.leaksentry"
    >

  <application>
    <provider
        android:name="leakcanary.internal.LeakSentryInstaller"
        android:authorities="${applicationId}.leak-sentry-installer"
        android:exported="false"/>
  </application>
</manifest>

通过官方可以了解到 LeakCanary 工作原理是由哪几步构成的:

  • Detecting retained instances: 探测存活的实例
  • Dumping the heap:将发生内存泄漏时的 java heap dump 到 .hprof 文件中
  • Analyzing the heap:分析 .hprof 文件,找到内存泄漏的路径
  • Grouping leaks:有一些内存泄漏是同一个原因造成,LeakCanary 会将这些归类

二、Detecting retained instances

探测 retained 实例是检查内存泄漏的第一步,探测的时机是 Activity/Fragment destroyed,负责这个功能的模块是 leaksentry,leaksentry 依赖了 watcher。ActivityDestroyWatcher 负责 Activity 的探测,AndroidOFragmentDestroyWatcher 负责 Fragment 的探测,LeakCanary 的探测都是无侵入式的,借助了 Android Framework 提供的 Activity/Fragment 生命回调。

Activity 的触发时机:

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          refWatcher.watch(activity)
        }
      }
    }

这里使用了 kotlin 的语言特性——委托代理,在之前的 Java 版本是空实现方法,然后复写 onActivityDestroyed。对比之下,kotlin 版本似乎更加的高级,kotlin 的委托是在编译层面做的,反编译 lifecycleCallbacks,可以发现 kotlin 在编译时会把委托的类改成内部成员静态代理的方式。

public final class ActivityDestroyWatcher$lifecycleCallbacks$1 implements ActivityLifecycleCallbacks {
    private final /* synthetic */ ActivityLifecycleCallbacks $$delegate_0;
    final /* synthetic */ ActivityDestroyWatcher this$0;

    public void onActivityCreated(@RecentlyNonNull Activity activity, @RecentlyNullable Bundle bundle) {
        this.$$delegate_0.onActivityCreated(activity, bundle);
    }
  
    ...
        
    ActivityDestroyWatcher$lifecycleCallbacks$1(ActivityDestroyWatcher $outer) {
        this.this$0 = $outer;
        InternalHelper internalHelper = InternalHelper.INSTANCE;
        Class javaClass$iv = ActivityLifecycleCallbacks.class;
        InvocationHandler noOpHandler$iv = InternalHelper$noOpDelegate$noOpHandler$1.INSTANCE;
        Object newProxyInstance = Proxy.newProxyInstance(javaClass$iv.getClassLoader(), new Class[]{javaClass$iv}, noOpHandler$iv);
        if (newProxyInstance != null) {
            this.$$delegate_0 = (ActivityLifecycleCallbacks) newProxyInstance;
            return;
        }
        throw new TypeCastException("null cannot be cast to non-null type android.app.Application.ActivityLifecycleCallbacks");
    }

    public void onActivityDestroyed(@NotNull Activity activity) {
        Intrinsics.checkParameterIsNotNull(activity, "activity");
        if (((Config) this.this$0.configProvider.invoke()).getWatchActivities()) {
            this.this$0.refWatcher.watch(activity);
        }
    }
}

上面都是些和 leakcanary 的题外话,下面开始正式的分析,在 Activity#onDestroy 调用时,RefWatcher 会开始进行探测。

1.1 RefWatcher#watch

  @Synchronized fun watch(
    watchedInstance: Any,
    name: String
  ) {
    if (!isEnabled()) {
      return
    }
    removeWeaklyReachableInstances()
    val key = UUID.randomUUID()
        .toString()
    val watchUptimeMillis = clock.uptimeMillis()
    val reference =
      KeyedWeakReference(watchedInstance, key, name, watchUptimeMillis, queue)
    CanaryLog.d(
        "Watching %s with key %s",
        ((if (watchedInstance is Class<*>) watchedInstance.toString() else "instance of ${watchedInstance.javaClass.name}") + if (name.isNotEmpty()) " named $name" else ""), key
    )

    watchedInstances[key] = reference
    checkRetainedExecutor.execute {
      moveToRetained(key)
    }
  }

leakcanary 中很多操作都会调用 removeWeaklyReachableInstances,这个方法主要是做了这样一件事:移除那些实例,哪些实例呢?实例是一个 WeakRef,他指向的对象被回收了。看一下实现:

  private fun removeWeaklyReachableInstances() {
    // 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) {
        watchedInstances.remove(ref.key)
      }
    } while (ref != null)
  }

从注释可以得知,当 WeakRef 指向的对象被回收以后,WeakRef 就会被加入 queue,果然看源码还是能学到不少东西的。如果发现 WeakRef 指向的对象已经被回收了,那么就从 watchedInstances 将 key 移除掉,因为已经无内存泄漏的风险。

接着往下看,构造完 reference 后就会进行对该 reference 进行延迟 moveToRetained 操作,checkRetainedExecutor 是包装的 handler,默认一个 runnable 是延迟 5s 执行。5s 后开始执行 moveToRetained:

  @Synchronized private fun moveToRetained(key: String) {
    removeWeaklyReachableInstances()
    val retainedRef = watchedInstances[key]
    if (retainedRef != null) {
      retainedRef.retainedUptimeMillis = clock.uptimeMillis()
      onInstanceRetained()
    }
  }

执行 moveToRetained 时如果发现 watchedInstances[key] 不为 null,说明 watchedInstances[key] 指向的对象没有被回收,则表明有内存泄漏的风险,然后给 retainedRef#retainedUptimeMillis 赋值供之后筛选。此时只是怀疑可能有内存风险,因为在内存不紧张的情况下,可能 5s 以内对象不会被回收,所以接下来需要进行更进一步的检测——gc回收。

1.2 HeapDumpTrigger#onReferenceRetained

当 RefWatcher 检测到可能的风险之后就会通知 HeapDumpTrigger 进行 gc 检测。主要方法是 HeapDumpTrigger#onReferenceRetained,其实也就是调用 HeapDumpTrigger#scheduleRetainedInstanceCheck,在 scheduleRetainedInstanceCheck 中最后调用的 checkRetainedInstances

  private fun checkRetainedInstances(reason: String) {
    val config = configProvider()

    var retainedReferenceCount = refWatcher.retainedInstanceCount
    // 如果有实例还未被回收,则执行 gc
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      // 执行完 gc,再看看是否还有实例存活
      retainedReferenceCount = refWatcher.retainedInstanceCount
    }

    // 校验留存的实例数量是否达到了阈值
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

    CanaryLog.d("Found %d retained references, dumping the heap", retainedReferenceCount)
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    dismissRetainedCountNotification()
    // dump file
    val heapDumpFile = heapDumper.dumpHeap()
    if (heapDumpFile == null) {
      CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
      scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
      showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
      return
    }
    lastDisplayedRetainedInstanceCount = 0
    // 移除 dump file 之前的实例,dump 之后的实例保留
    refWatcher.removeInstancesWatchedBeforeHeapDump(heapDumpUptimeMillis)
    // 分析 dumpFile
    HeapAnalyzerService.runAnalysis(application, heapDumpFile)
  }

之前说过每次去获取存活的实例时都会进行一次 removeWeaklyReachableInstances:

  val retainedInstanceCount: Int
    @Synchronized get() {
      removeWeaklyReachableInstances()
      return watchedInstances.count { it.value.retainedUptimeMillis != -1L }
    }

然后看一下 LeakCanary 的 gc 操作:

    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()
      }
    }

通过 Runtime.getRuntime().gc() 操作 gc,然后线程 sleep 100ms 等待 WeakRef 进入 queue,100ms 是个 magic number ,可能是个经验值。

  private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int
  ): Boolean {
    val countChanged = lastDisplayedRetainedInstanceCount != retainedKeysCount
    lastDisplayedRetainedInstanceCount = retainedKeysCount
    if (retainedKeysCount == 0) {
      CanaryLog.d("No retained instances")
      if (countChanged) {
        showNoMoreRetainedInstanceNotification()
      }
      return true
    }

    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        CanaryLog.d(
            "Found %d retained instances, which is less than the visible threshold of %d",
            retainedKeysCount,
            retainedVisibleThreshold
        )
        showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)
        scheduleRetainedInstanceCheck(
            "Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
  }

Dump heap 会阻塞 UI 线程,为了避免影响平常的开发,LeakCanary 设置了内存泄漏的实例数量阈值,默认是 5,如果在 App 在前台可见时,而且未达到阈值,则会再次 scheduleRetainedInstanceCheck,相当于一个周期轮训任务,在一个周期任务中不断检测内存泄漏的数量。

三、Dumping the heap

LeakCanary 使用 AndroidHeapDumper 进行 dump,使用的是 Debug#dumpHprofData,这个没啥好写的,系统提供了 Api。

四、Analyzing the heap

分析内存是通过 HeapAnalyzerService 完成的,HeapAnalyzerService 是一个前台服务,继承自 IntentService。

  override fun onHandleIntentInForeground(intent: Intent?) {
    if (intent == null) {
      CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.")
      return
    }
    // Since we're running in the main process we should be careful not to impact it.
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
    val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File

    if (!heapDumpFile.exists()) {
      throw IllegalStateException(
          "Hprof file missing due to: [${LeakDirectoryProvider.hprofDeleteReason(
              heapDumpFile
          )}] $heapDumpFile"
      )
    }

    val heapAnalyzer = HeapAnalyzer(this)
    val config = LeakCanary.config

    val exclusions = AndroidKnownReference.mapToExclusions(config.knownReferences)

    val heapAnalysis =
      heapAnalyzer.checkForLeaks(
          heapDumpFile, exclusions, config.computeRetainedHeapSize, config.objectInspectors,
          if (config.useExperimentalLeakFinders) config.objectInspectors else listOf(
              AndroidObjectInspectors.KEYED_WEAK_REFERENCE
          )
      )

    config.analysisResultListener(application, heapAnalysis)
  }

在 LeakCanary 启动 HeapAnalyzerService 时,会传入一个 File 参数(原来 File 是序列化的),最后是通过 HeapAnalyzer 完成分析的。由于分析过程较为繁琐,请看下一篇。