抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

Android 内存泄露工具 leakcanary 2.6 浅析

前言

偶然看到一篇文章,思路清晰写得很好,但有些细节和现有版本不太一样,自己看了一遍做了一些调整与补充,加深印象。本文字数达6k,大部分为源码,请配合源码食用,否则没有什么意义(PS:性能不好的设备可能展示不完整,请及时跟上时代)。

2019 年 11 月,LeakCanary2 正式版发布,和 LeakCanary1 相比,LeakCanary2 有以下改动:

  • 完全使用 Kotlin 重写
  • 使用新的 Heap 分析工具 Shark,替换了之前的 haha,按官方的说法,内存占用减少了 10 倍
  • 泄露类型分组

其中,将 Heap 分析模块作为一个独立的模块,是一个非常不错的改动。这意味着,可以基于 Shark 来做很多有意思的事情,比如,用于线上分析或者开发一个”自己”的 LeakCanary。

整体架构

在分析源码之前,我们先看 LeakCanary 的整体结构,这有助于我们对项目整体设计上有一定理解。LeakCanary2 有以下几个模块:

  • leakcanary-android
    集成入口模块,提供 LeakCanary 安装,公开 API 等能力
  • leakcanary-android-core
    核心模块
  • leakcanary-object-watcher, leakcanary-object-watcher-android, leakcanary-object-watcher-androidx, leakcanary-object-watcher-support-fragments
    对象实例观察模块,在 Activity,Fragment 等对象的生命周期中,注册对指定对象实例的观察,有 Activity,Fragment,ViewModel,Service 等
  • shark-android
    提供特定于 Android 平台的分析能力。例如设备的信息,Android 版本,已知的内存泄露问题等
  • shark
    hprof 文件解析与分析的入口模块
  • shark-graph
    分析堆中对象的关系图模块
  • shark-hprof
    解析 hprof 文件模块
  • shark-log
    日志模块

集成方式

首先,我们从集成方式入手,LeakCanary1 的依赖为:

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

接着在 Application 中调用 LeakCanary.install() 方法。而 LeakCanary2 集成则要简单不少,只需要增加以下依赖即可:

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

也就是说 LeakCanary2 实现了自动调用 install() 方法,实现方式可能大部分人都能猜到,就是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。

AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。

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

AppWatcherInstaller 有两个实现类:

一个是 MainProcess,当我们使用 leakcanary-android 模块时,会默认使用这个,表示在当前 App 进程中使用 LeakCanary。

另外一个类为 LeakCanaryProcess,当使用 leakcanary-android-process 模块代替 leakcanary-android 模块时,则会使用这个类,我们可以看下 leakcanary-object-watcher-android 的 Manifest 文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.squareup.leakcanary.objectwatcher" >
    <application>
        <provider
            android:name="leakcanary.internal.AppWatcherInstaller$MainProcess"
            android:authorities="${applicationId}.leakcanary-installer"
            android:enabled="@bool/leak_canary_watcher_auto_install"
            android:exported="false" />
    </application>
</manifest>

AppWatcher

Watcher 功能的入口位于 AppWatcher.manualInstall(application) 方法中,这个方法的调用时机则是我们上面说到的 AppWatcherInstaller.onCreate() 中。

  @JvmOverloads
  fun manualInstall(
    application: Application,
    retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5),
    watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)
  ) {
    checkMainThread()
    check(!isInstalled) {
      "AppWatcher already installed"
    }
    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()
    }
  }                                                                         

这里主要做了两件事,首先是 Activity 和 Fragment 对象的注册观察:

watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)

注意默认参数传递:

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}

不传递参数时将默认初始化以上四个监听器。以ActivityWatcher为例:

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

调用 registerActivityLifecycleCallbacks() 方法注册 Activity 生命周期回调。在每个 Activity.onDestory 回调中,将每个 Activity 对象加到观察列表中。

/**
* References passed to [watch].
*/private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>()

@Synchronized override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  if (!isEnabled()) {
    return
  }
  removeWeaklyReachableObjects()
  val key = UUID.randomUUID()
    .toString()
  val watchUptimeMillis = clock.uptimeMillis()
  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)
  }
}

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

PS:将弱引用和引用队列 ReferenceQueue 关联起来,如果弱引用referent持有的对象被GC回收,JVM就会把这个弱引用加入到与之关联的引用队列referenceQueue中。

/**
 * A weak reference used by [ObjectWatcher] to determine which objects become weakly reachable
 * and which don't. [ObjectWatcher] uses [key] to keep track of [KeyedWeakReference] instances that
 * haven't made it into the associated [ReferenceQueue] yet.
 *
 * [heapDumpUptimeMillis] should be set with the current time from [Clock.uptimeMillis] right
 * before dumping the heap, so that we can later determine how long an object was retained.
 */class KeyedWeakReference(
  referent: Any,
  val key: String,
  val description: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) 

所以 removeWeaklyReachableObjects() 方法的作用就是将已经被 GC 的对象从 watchedObjects 集合中删除。

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

当我们调用 expectWeaklyReachable() 方法时,先清理已经被 GC 的对象,接着将需要观察的对象,存储为一个 KeyedWeakReference 的弱引用对象,再存放到 watchedObjects 集合中,最后使用 checkRetainedExecutor 安排一次 moveToRetained 任务。

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

先调用一次 removeWeaklyReachableObjects() 删除已经 GC 的对象,那么剩下的对象就可以认为是被保留(没办法 GC)的对象,会调通知事件。

回到流程开始 AppWatcher.manualInstall(application) 方法,会发现在初始化默认的监听器之前还有一个重要的模块需要初始化:

    // Requires AppWatcher.objectWatcher to be set
    LeakCanaryDelegate.loadLeakCanary(application)
@Suppress("UNCHECKED_CAST")
val loadLeakCanary by lazy {
  try {
    val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
    leakCanaryListener.getDeclaredField("INSTANCE")
      .get(null) as (Application) -> Unit
  } catch (ignored: Throwable) {
    NoLeakCanary
  }
}

而这个对象的值是通过反射获取的 InternalLeakCanary.INSTANCE 这是一个单例对象。InternalLeakCanary 位于 leakcanary-android-core 模块,这也是需要反射的原因。

InternalLeakCanary

接着上面末尾讲,当调用 loadLeakCanary() 方法时,实际会调用 InternalLeakCanary.invoke() 方法:

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

  checkRunningInDebuggableBuild()

  AppWatcher.objectWatcher.addOnObjectRetainedListener(this)

  val heapDumper = AndroidHeapDumper(application, createLeakDirectoryProvider(application))

  val gcTrigger = GcTrigger.Default

  val configProvider = { LeakCanary.config }

  val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
  handlerThread.start()
  val backgroundHandler = Handler(handlerThread.looper)

  heapDumpTrigger = HeapDumpTrigger(
    application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger, heapDumper,
    configProvider
  )
  application.registerVisibilityListener { applicationVisible ->
    this.applicationVisible = applicationVisible
    heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
  }
  registerResumedActivityListener(application)
  addDynamicShortcut(application)

  // We post so that the log happens after Application.onCreate()
  mainHandler.post {
    // https://github.com/square/leakcanary/issues/1981
    // We post to a background handler because HeapDumpControl.iCanHasHeap() checks a shared pref
    // which blocks until loaded and that creates a StrictMode violation.
    backgroundHandler.post {
      SharkLog.d {
        when (val iCanHasHeap = HeapDumpControl.iCanHasHeap()) {
          is Yup -> application.getString(R.string.leak_canary_heap_dump_enabled_text)
          is Nope -> application.getString(
            R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
          )
        }
      }
    }
  }
}

这里做的事情比较多,首先是 addOnObjectRetainedListener() 方法,这里会注册一个 OnObjectRetainedListener 事件,也就是我们上面说到的在 moveToRetained() 方法中的回调事件。

AndroidHeapDumper 则是通过调用 Debug.dumpHprofData() 方法从虚拟机中 dump hprof 文件。

override fun dumpHeap(): DumpHeapResult {
  val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump

  val waitingForToast = FutureResult<Toast?>()
  showToast(waitingForToast)

  if (!waitingForToast.wait(5, SECONDS)) {
    SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
    return NoHeapDump
  }

  val notificationManager =
    context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  if (Notifications.canShowNotification) {
    val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
    val builder = Notification.Builder(context)
      .setContentTitle(dumpingHeap)
    val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
    notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
  }

  val toast = waitingForToast.get()

  return try {
    val durationMillis = measureDurationMillis {
      // ---- main
      Debug.dumpHprofData(heapDumpFile.absolutePath)
    }
    if (heapDumpFile.length() == 0L) {
      SharkLog.d { "Dumped heap file is 0 byte length" }
      NoHeapDump
    } else {
      HeapDump(file = heapDumpFile, durationMillis = durationMillis)
    }
  } catch (e: Exception) {
    SharkLog.d(e) { "Could not dump heap" }
    // Abort heap dump
    NoHeapDump
  } finally {
    cancelToast(toast)
    notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
  }
}

GcTrigger 通过调用 Runtime.getRuntime().gc() 方法触发虚拟机进行 GC 操作:

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

HeapDumpTrigger 管理触发 Heap Dump 的逻辑,有两个地方会触发 Heap Dump:

  • 保留对象超过阙值 这个阙值默认为 5(应用可见的情况下),可以通过 Config 配置:
  val retainedVisibleThreshold: Int = 5
  • 当 ObjectWatcher 回调 onObjectRetained() 方法时,HeapDumpTrigger.onObjectRetained() 方法会被调用:
  //InternalLeakCanary.kt
  override fun onObjectRetained() = scheduleRetainedObjectCheck()

  fun scheduleRetainedObjectCheck() {
    if (this::heapDumpTrigger.isInitialized) {
      heapDumpTrigger.scheduleRetainedObjectCheck()
    }
  }
  //HeapDumpTrigger.kt
  fun scheduleRetainedObjectCheck(
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      // 同时只会有一个任务
      return
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects()
    }, delayMillis)
  }
  //HeapDumpTrigger.kt
  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) {
            // 先执行一次 GC
          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
          )
          //此处不会 return 会继续往下走 符合条件则 dumpHeap
        }
      } 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) {
      // 60s 内只会执行一次,重新安排     
      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"
    // ---- main
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    )
  }
  fun scheduleRetainedObjectCheck(
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      return
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      // ---- main
      checkRetainedObjects()
    }, delayMillis)
  }

所以不论是超出阀值还是生命周期对象结束时的回调,最终都是执行dumpHeap

  private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    saveResourceIdNamesToMemory()
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    // ---- main
    when (val heapDumpResult = heapDumper.dumpHeap()) {
      is NoHeapDump -> {
        if (retry) {
          SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" }
          scheduleRetainedObjectCheck(
            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
          )
        )
      }
      is HeapDump -> {
        lastDisplayedRetainedObjectCount = 0
        lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
        objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
        HeapAnalyzerService.runAnalysis(
          context = application,
          heapDumpFile = heapDumpResult.file,
          heapDumpDurationMillis = heapDumpResult.durationMillis,
          heapDumpReason = reason
        )
      }
    }
  }

when 语句块的条件 heapDumper.dumpHeap()是主要代码,会发现调用的是AndroidHeapDumper$dumpHeap(),这点在上面提过,主要是调用系统的 Debug 工具来导出dump hprof 文件:

  //AndroidHeapDumper.kt fun dumpHeap()
  Debug.dumpHprofData(heapDumpFile.absolutePath)

导出完成后根据结果判断弹什么样的通知,成功会将结果转交给HeapAnalyzerService完成结果展示。

HeapAnalyzerService

HeapAnalyzerService 是继承于 IntentService,调用 runAnalysis() 方法最终会调用到 analyzeHeap() 方法:

internal class HeapAnalyzerService : ForegroundService
//parent 
internal abstract class ForegroundService(
  name: String,
  private val notificationContentTitleResId: Int,
  private val notificationId: Int
) : IntentService(name)
fun runAnalysis(
  context: Context,
  heapDumpFile: File,
  heapDumpDurationMillis: Long? = null,
  heapDumpReason: String = "Unknown"
) {
  val intent = Intent(context, HeapAnalyzerService::class.java)
  intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
  intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason)
  heapDumpDurationMillis?.let {
    intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis)
  }
  startForegroundService(context, intent)
}
override fun onHandleIntentInForeground(intent: Intent?) {
  if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) {
    SharkLog.d { "HeapAnalyzerService received a null or empty 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
  val heapDumpReason = intent.getStringExtra(HEAPDUMP_REASON_EXTRA)
  val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, -1)

  val config = LeakCanary.config
  val heapAnalysis = if (heapDumpFile.exists()) {
    // ---- main
    analyzeHeap(heapDumpFile, config)
  } else {
    missingFileFailure(heapDumpFile)
  }
  val fullHeapAnalysis = when (heapAnalysis) {
    is HeapAnalysisSuccess -> heapAnalysis.copy(
      dumpDurationMillis = heapDumpDurationMillis,
      metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
    )
    is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
  }
  onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
  config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
}

看下analyzeHeap方法:

private fun analyzeHeap(
  heapDumpFile: File,
  config: Config
): HeapAnalysis {
  val heapAnalyzer = HeapAnalyzer(this)

  val proguardMappingReader = try {
    ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME))
  } catch (e: IOException) {
    null
  }
  return heapAnalyzer.analyze(
    heapDumpFile = heapDumpFile,
    leakingObjectFinder = config.leakingObjectFinder,
    referenceMatchers = config.referenceMatchers,
    computeRetainedHeapSize = config.computeRetainedHeapSize,
    objectInspectors = config.objectInspectors,
    metadataExtractor = config.metadataExtractor,
    proguardMapping = proguardMappingReader?.readProguardMapping()
  )
}

proguardMappingReader 是用于处理代码混淆的,支持在测试版本打开代码混淆开关,PROGUARD_MAPPING_FILE_NAME 表示 Mapping 文件,这个文件在 leakcanary-deobfuscation-gradle-plugin 模块处理的,具体的可以看 CopyObfuscationMappingFileTask

HeapAnalyzer.analyze() 这个方法的作用是:从 hprof 文件中搜索泄露对象,然后计算它们到 GC Roots 的最短路径。

val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile))
sourceProvider.openHeapGraph(proguardMapping).use { graph ->
  val helpers =
    FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors)
  val result = helpers.analyzeGraph(
    metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime
  )
  //...
}

解析 hprof 文件

先了解下 hprof 文件:hprof 是由 JVM TI Agent HPROF 生成的一种二进制文件,具体文件格式可以看官方文档:链接

根据上面的链接大致翻译下,则 hprof 文件可以分为以下几个部分:

  • header header 中有以下三个部分组成:
    • 文件格式名和版本号
      JDK 1.6 的值为 “JAVA PROFILE 1.0.2″,在此之前还有 “JAVA PROFILE 1.0” 和 “JAVA PROFILE 1.0.1”,如果是 Android 平台的话,这个值为 “JAVA PROFILE 1.0.3″,这也是为什么 MAT 不支持直接解析 Android 平台生成的 hprof 文件了。
    • identifiers 标识符的大小。
      标识符用于表示 UTF8字符串、对象、堆栈跟踪等。它们可以具有与主机指针或 sizeof (void *)相同的大小,但不一定是这样
    • 时间戳
      高位 4 字节 + 低位 4 字节
  • records record 表示文件中记录的信息,每个 record 都由以下 4 个部分组成:
    • tag
      1 字节,表示 record 的类型,支持的值可以看文档。
    • time
      4 字节,表示 record 的时间戳。
    • length
      4 字节,表示 body 的字节长度。
    • body
      表示 record 中存储的数据。

在了解完 hprof 文件的格式后,我们再来看 LeakCanary 的解析 hprof 文件的代码,

sourceProvider.openHeapGraph(proguardMapping)源码:

fun DualSourceProvider.openHeapGraph(
  proguardMapping: ProguardMapping? = null,
  indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
): CloseableHeapGraph {
  val header = openStreamingSource().use { HprofHeader.parseHeaderOf(it) }
  val index = HprofIndex.indexRecordsOf(this, header, proguardMapping, indexedGcRootTypes)
  return index.openHeapGraph()
}

读取 hprof 文件:

HprofHeader.parseHeaderOf(it) 
/**
 * Reads the header of the provided [source] and returns it as a [HprofHeader].
 * This does not close the [source].
 */fun parseHeaderOf(source: BufferedSource): HprofHeader {
  require(!source.exhausted()) {
    throw IllegalArgumentException("Source has no available bytes")
  }
  val endOfVersionString = source.indexOf(0)
  val versionName = source.readUtf8(endOfVersionString)

  val version = supportedVersions[versionName]
  checkNotNull(version) {
    "Unsupported Hprof version [$versionName] not in supported list ${supportedVersions.keys}"
  }
  // Skip the 0 at the end of the version string.
  source.skip(1)
  val identifierByteSize = source.readInt()
  val heapDumpTimestamp = source.readLong()
  return HprofHeader(heapDumpTimestamp, version, identifierByteSize)
}

从上面的代码可以看到先依次读取 version(versionName)、identifierByteSizeheapDumpTimestamp 后,再创建一个 HprofReader 用于读取 records。HprofReader 的工作方式也是类似的,接着openHeapGraph处源码调用路径(层层递进):

-> HprofIndex.indexRecordsOf
-> HprofInMemoryIndex.indexHprof
-> reader.readRecords

最后在 reader.readRecords 方法会发现,实际上是根据 tag 的值去读取不同的 record,这里我们以 “STRING IN UTF8” 为例:

//StreamingHprofReader.kt fun readRecords
STRING_IN_UTF8.tag -> {
  if (STRING_IN_UTF8 in recordTags) {
    listener.onHprofRecord(STRING_IN_UTF8, length, reader)
  } else {
    reader.skip(length)
  }
}

生成 heap graph

HeapGraph 用于表示 heap 中的对象关系图,通过调用 HprofHeapGraph.openHeapGraph 生成:

fun DualSourceProvider.openHeapGraph(
  proguardMapping: ProguardMapping? = null,
  indexedGcRootTypes: Set<HprofRecordTag> = HprofIndex.defaultIndexedGcRootTags()
): CloseableHeapGraph {
  val header = openStreamingSource().use { HprofHeader.parseHeaderOf(it) }
  val index = HprofIndex.indexRecordsOf(this, header, proguardMapping, indexedGcRootTypes)
  return index.openHeapGraph()
}
//index.openHeapGraph()
/**
 * Opens a [CloseableHeapGraph] which you can use to navigate the indexed hprof and then close.
 */fun openHeapGraph(): CloseableHeapGraph {
  val reader = RandomAccessHprofReader.openReaderFor(sourceProvider, header)
  return HprofHeapGraph(header, reader, index)
}

indexedGcRootTypes 表示我们要收集的 GC Roots 节点,通过查看源码得知可以作为 GC Roots 节点的有以下对象:

fun defaultIndexedGcRootTags() = EnumSet.of(
  // native 中的全局变量
  HprofRecordTag.ROOT_JNI_GLOBAL,
  // java 中的局部变量
  HprofRecordTag.ROOT_JAVA_FRAME,
  // native 中的局部变量
  HprofRecordTag.ROOT_JNI_LOCAL,
  // 调用 wait() 或者 notify(),或者 synchronized 锁对象
  HprofRecordTag.ROOT_MONITOR_USED,
  // native 中的入参和出参
  HprofRecordTag.ROOT_NATIVE_STACK,
  // 系统类
  HprofRecordTag.ROOT_STICKY_CLASS,
  // 活动线程引用的代码块
  HprofRecordTag.ROOT_THREAD_BLOCK,
  // ThreadObject points to threads, which we need to find the thread that a JavaLocalPattern
  // belongs to
  // 活动线程引用的对象
  HprofRecordTag.ROOT_THREAD_OBJECT,
  // native 中的锁对象
  HprofRecordTag.ROOT_JNI_MONITOR
)

接着会从 hprof 文件中读取 records,读取原理可以参考 hprof 文件格式。这里有个小细节,LeakCanary 只会读取以下几种类型的 record:

val bytesRead = reader.readRecords(
  EnumSet.of(CLASS_DUMP, INSTANCE_DUMP, OBJECT_ARRAY_DUMP, PRIMITIVE_ARRAY_DUMP),
  //...

val recordTypes = EnumSet.of(
    STRING_IN_UTF8,
    LOAD_CLASS,
    CLASS_DUMP,
    INSTANCE_DUMP,
    OBJECT_ARRAY_DUMP,
    PRIMITIVE_ARRAY_DUMP
) + HprofRecordTag.rootTags.intersect(indexedGcRootTags)
  • STRING_IN_UTF8
    UTF8 格式的字符串
  • LOAD CLASS
    虚拟机中加载的类
  • CLASS_DUMP
    dump 出来内存中的类实例
  • INSTANCE_DUMP
    dump 出来内存中的对象实例
  • OBJECT_ARRAY_DUMP
    dump 出来内存中的对象数组实例
  • PRIMITIVE_ARRAY_DUMP
    dump 出来内存中的原始类型数组实例
  • indexedGcRootTags
    包括了上面定义的所有 GC Roots 对象实例

查询泄露对象

在生成 heap graph 后,我们就可以根据它,来获取泄露对象的 objectIds:

//回到 FindLeakInput.analyzeGraph 方法
//...
val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph)
//...

LeakingObjectFinder 用于查询泄露对象,它的实现有两个:KeyedWeakReferenceFinderFilteringLeakingObjectFinder,默认为 KeyedWeakReferenceFinder,即通过 KeyedWeakReference 引用的对象,关于 KeyedWeakReference 的作用我们在 AppWatcher 那里有说到。

override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> =
  findKeyedWeakReferences(graph)
    .filter { it.hasReferent && it.isRetained }
    .map { it.referent.value }
    .toSet()
internal fun findKeyedWeakReferences(graph: HeapGraph): List<KeyedWeakReferenceMirror> {
  return graph.context.getOrPut(KEYED_WEAK_REFERENCE.name) {
    val keyedWeakReferenceClass = graph.findClassByName("leakcanary.KeyedWeakReference")

    val keyedWeakReferenceClassId = keyedWeakReferenceClass?.objectId ?: 0
    val legacyKeyedWeakReferenceClassId =
      graph.findClassByName("com.squareup.leakcanary.KeyedWeakReference")?.objectId ?: 0

    val heapDumpUptimeMillis = heapDumpUptimeMillis(graph)

    val addedToContext: List<KeyedWeakReferenceMirror> = graph.instances
      .filter { instance ->
        instance.instanceClassId == keyedWeakReferenceClassId || instance.instanceClassId == legacyKeyedWeakReferenceClassId
      }
      .map {
        KeyedWeakReferenceMirror.fromInstance(
          it, heapDumpUptimeMillis
        )
      }
      .toList()
    graph.context[KEYED_WEAK_REFERENCE.name] = addedToContext
    addedToContext
  }
}

KeyedWeakReferenceFinder 通过过滤 heap dump 中的所有 KeyedWeakReference 实例,来获取泄露对象实例。而 FilteringLeakingObjectFinder 则是用于我们自定义的泄露对象判断逻辑:

override fun findLeakingObjectIds(graph: HeapGraph): Set<Long> {
  return graph.objects
    .filter { heapObject ->
      filters.any { filter ->
        filter.isLeakingObject(heapObject)
      }
    }
    .map { it.objectId }
    .toSet()
}

生成泄露对象报告

LeakCanary 定义了两个主要泄露类型:ApplicationLeak 和 LibraryLeak,同时还有一些unreachableObjects:

  • ApplicationLeak
    表示应用本身导致内存泄露
  • LibraryLeak
    表示依赖库导致的内存泄露,例如 Android Framework 等
val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds)
//...
val pathFinder = PathFinder(graph, listener, referenceMatchers)
//查询泄露对象到 GC ROOTS 路径
val pathFindingResults =
  pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)
//...

查询泄露对象到 GC Roots 的路径,一层套一层:

fun findPathsFromGcRoots(
  leakingObjectIds: Set<Long>,
  computeRetainedHeapSize: Boolean
): PathFindingResults {
  listener.onAnalysisProgress(FINDING_PATHS_TO_RETAINED_OBJECTS)

  val objectClass = graph.findClassByName("java.lang.Object")
  val sizeOfObjectInstances = determineSizeOfObjectInstances(objectClass, graph)
  val javaLangObjectId = objectClass?.objectId ?: -1

  // Estimate of how many objects we'll visit. This is a conservative estimate, we should always
  // visit more than that but this limits the number of early array growths.
  val estimatedVisitedObjects = (graph.instanceCount / 2).coerceAtLeast(4)

  val state = State(
    leakingObjectIds = leakingObjectIds.toLongScatterSet(),
    sizeOfObjectInstances = sizeOfObjectInstances,
    computeRetainedHeapSize = computeRetainedHeapSize,
    javaLangObjectId = javaLangObjectId,
    estimatedVisitedObjects = estimatedVisitedObjects
  )

  return state.findPathsFromGcRoots()
}

最后到达的方法:

private fun State.findPathsFromGcRoots(): PathFindingResults {
  enqueueGcRoots()

  val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
  visitingQueue@ while (queuesNotEmpty) {
    val node = poll()
    if (leakingObjectIds.contains(node.objectId)) {
      shortestPathsToLeakingObjects.add(node)
      // Found all refs, stop searching (unless computing retained size)
      if (shortestPathsToLeakingObjects.size == leakingObjectIds.size()) {
        if (computeRetainedHeapSize) {
          listener.onAnalysisProgress(FINDING_DOMINATORS)
        } else {
          break@visitingQueue
        }
      }
    }

    when (val heapObject = graph.findObjectById(node.objectId)) {
      is HeapClass -> visitClassRecord(heapObject, node)
      is HeapInstance -> visitInstance(heapObject, node)
      is HeapObjectArray -> visitObjectArray(heapObject, node)
    }
  }
  return PathFindingResults(
    shortestPathsToLeakingObjects,
    if (visitTracker is Dominated) visitTracker.dominatorTree else null
  )
}

State.findPathsFromGcRoots() 的代码有点长,我们一点点分析。首先是 enqueueGcRoots() 方法,它的作用是将所有 GC Roots 节点放入到队列中:

private fun State.enqueueGcRoots() {
  // 将 GC Roots 进行排序
  val gcRoots = sortedGcRoots()
  // 存储线程名称
  val threadNames = mutableMapOf<HeapInstance, String>()
  // 存储线程的 SerialNumber,可以通过 SerialNumber 访问对应的线程信息
  val threadsBySerialNumber = mutableMapOf<Int, Pair<HeapInstance, ThreadObject>>()
  gcRoots.forEach { (objectRecord, gcRoot) ->
    when (gcRoot) {
      is ThreadObject -> {
        // 活动的 Thread 实例 缓存 threadsBySerialNumber
        threadsBySerialNumber[gcRoot.threadSerialNumber] = objectRecord.asInstance!! to gcRoot
        // 入列 NormalRootNode
        enqueue(NormalRootNode(gcRoot.id, gcRoot))
      }
      is JavaFrame -> {
        // Java 局部变量       
        val threadPair = threadsBySerialNumber[gcRoot.threadSerialNumber]
        if (threadPair == null) {
          // Could not find the thread that this java frame is for.
          enqueue(NormalRootNode(gcRoot.id, gcRoot))
        } else {
          val (threadInstance, threadRoot) = threadPair
          val threadName = threadNames[threadInstance] ?: {
            val name = threadInstance[Thread::class, "name"]?.value?.readAsJavaString() ?: ""
            threadNames[threadInstance] = name
            name
          }()
          // RefreshceMatchers 用于匹配已知的引用节点
          // IgnoredReferenceMatcher 表示忽略这个引用节点
          // LibraryLeakReferenceMatcher 表示这是库内存泄露对象
          val referenceMatcher = threadNameReferenceMatchers[threadName]

          if (referenceMatcher !is IgnoredReferenceMatcher) {
            val rootNode = NormalRootNode(threadRoot.id, gcRoot)

            val refFromParentType = LOCAL
            // Unfortunately Android heap dumps do not include stack trace data, so
            // JavaFrame.frameNumber is always -1 and we cannot know which method is causing the
            // reference to be held.
            val refFromParentName = ""

            val childNode = if (referenceMatcher is LibraryLeakReferenceMatcher) {
              LibraryLeakChildNode(
                objectId = gcRoot.id,
                parent = rootNode,
                refFromParentType = refFromParentType,
                refFromParentName = refFromParentName,
                matcher = referenceMatcher
              )
            } else {
              NormalNode(
                objectId = gcRoot.id,
                parent = rootNode,
                refFromParentType = refFromParentType,
                refFromParentName = refFromParentName
              )
            }
            // 入列 LibraryLeakChildNode 或 NormalNode         
            enqueue(childNode)
          }
        }
      }
      is JniGlobal -> {
        // Native 全局变量
        // 是否匹配已知引用节点
        val referenceMatcher = when (objectRecord) {
          is HeapClass -> jniGlobalReferenceMatchers[objectRecord.name]
          is HeapInstance -> jniGlobalReferenceMatchers[objectRecord.instanceClassName]
          is HeapObjectArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
          is HeapPrimitiveArray -> jniGlobalReferenceMatchers[objectRecord.arrayClassName]
        }
        if (referenceMatcher !is IgnoredReferenceMatcher) {
          if (referenceMatcher is LibraryLeakReferenceMatcher) {
            // 入列 LibraryLeakRootNode          
            enqueue(LibraryLeakRootNode(gcRoot.id, gcRoot, referenceMatcher))
          } else {
           // 入列 NormalRootNode         
            enqueue(NormalRootNode(gcRoot.id, gcRoot))
          }
        }
      }
      // 其他 GC Roots,入列 NormalRootNode         
      else -> enqueue(NormalRootNode(gcRoot.id, gcRoot))
    }
  }
}

在将 GC Roots 节点入列的过程,有两个地方值得注意:

  • ReferenceMatcher
    ReferenceMatcher 用于匹配引用节点,判断是否要忽略它。LeakCanary 支持 4 种类型的匹配:
    • 类实例字段
      缓存在 fieldNameByClassName 里,例如,android.os.Message 中的 obj 字段
    • 类静态字段
      缓存在 staticFieldNameByClassName 里,例如,android.app.ActivityManagermContext 字段
    • 指定线程
      缓存在 threadNames 里,例如,FinalizerWatchdogDaemon 线程
    • Native 全局变量
      缓存在 jniGlobals 里,例如,android.widget.Toast\$TN

内置的引用节点匹配为 AndroidReferenceMatchers.appDefaults

  • VisitQueue
    PathFinder 中有两个队列,一个优先级更高的 toVisitQueue,另外一个是 toVisitLastQueue,同时提供 toVisitSettoVisitLastSet 用于提供常数级查询。 队列中的节点分为两种:
    • RootNode 根节点,它有两个实现类:
      • LibraryLeakRootNode 依赖库的泄露根节点
      • NormalRootNode 普通的根节点
    • ChildNode 子节点,可以通过 parent 字段访问父节点。它有两个实现类:
      • LibraryLeakChildNode 依赖库的泄露子节点
      • NormalNode 普通的子节点

以下 3 种情况会将节点放入到 toVisitLastQueue 中:

  • LibraryLeakNode
  • GC Root 为 ThreadObject
  • 父节点的 GC Root 为 JavaFrame

因为这 3 种导致的内存泄露情况比较少,所以降低它们的访问优先级。

val visitLast =
  visitingLast ||
    node is LibraryLeakNode ||
    // We deprioritize thread objects because on Lollipop the thread local values are stored
    // as a field.
    (node is RootNode && node.gcRoot is ThreadObject) ||
    (node is NormalNode && node.parent is RootNode && node.parent.gcRoot is JavaFrame)

在将所有的 GC Roots 节点入列后,使用广度优先遍历所有的节点,当访问节点是泄露节点,则添加到 shortestPathsToLeakingObjects 中:

val shortestPathsToLeakingObjects = mutableListOf<ReferencePathNode>()
visitingQueue@ while (queuesNotEmpty) {
  val node = poll()
  if (leakingObjectIds.contains(node.objectId)) {
    shortestPathsToLeakingObjects.add(node)
    // Found all refs, stop searching (unless computing retained size)
    if (shortestPathsToLeakingObjects.size == leakingObjectIds.size()) {
      if (computeRetainedHeapSize) {
        listener.onAnalysisProgress(FINDING_DOMINATORS)
      } else {
        break@visitingQueue
      }
    }
  }

  when (val heapObject = graph.findObjectById(node.objectId)) {
    is HeapClass -> visitClassRecord(heapObject, node)
    is HeapInstance -> visitInstance(heapObject, node)
    is HeapObjectArray -> visitObjectArray(heapObject, node)
  }
}

在遍历子节点时,有 3 种情况需要考虑:

  • HeapClass
    当节点表示 HeapClass,我们将它的静态变量入列:
  val node = when (val referenceMatcher = ignoredStaticFields[fieldName]) {
    null -> NormalNode(
      objectId = objectId,
      parent = parent,
      refFromParentType = STATIC_FIELD,
      refFromParentName = fieldName
    )
    is LibraryLeakReferenceMatcher -> LibraryLeakChildNode(
      objectId = objectId,
      parent = parent,
      refFromParentType = STATIC_FIELD,
      refFromParentName = fieldName,
      matcher = referenceMatcher
    )
    // 忽略 IgnoredReferenceMatcher
    is IgnoredReferenceMatcher -> null
  }
  if (node != null) {
    enqueue(node)
  }
  • HeapInstance
    当节点表示 HeapInstance,我们将它的实例变量入列:
val node = when (val referenceMatcher = fieldReferenceMatchers[instanceRefField.fieldName]) {
    null -> NormalNode(
      objectId = instanceRefField.refObjectId,
      parent = parent,
      refFromParentType = INSTANCE_FIELD,
      refFromParentName = instanceRefField.fieldName,
      owningClassId = instanceRefField.declaringClassId
    )
    is LibraryLeakReferenceMatcher ->
      LibraryLeakChildNode(
        objectId = instanceRefField.refObjectId,
        parent = parent,
        refFromParentType = INSTANCE_FIELD,
        refFromParentName = instanceRefField.fieldName,
        matcher = referenceMatcher,
        owningClassId = instanceRefField.declaringClassId
      )
    is IgnoredReferenceMatcher -> null
  }
  if (node != null) {
    enqueue(node)
  }
  • HeapObjectArray
    当节点表示 HeapObjectArray,我们将它的非空元素入列:
    val record = objectArray.readRecord()
    val nonNullElementIds = record.elementIds.filter { objectId ->
      objectId != ValueHolder.NULL_REFERENCE && graph.objectExists(objectId)
    }
    nonNullElementIds.forEachIndexed { index, elementId ->
      val name = index.toString()
      enqueue(
        NormalNode(
          objectId = elementId,
          parent = parent,
          refFromParentType = ARRAY_ENTRY,
          refFromParentName = name
        )
      )
    }
  }

这里不需要考虑 HeapPrimitiveArray 的情况,因为原始类型不能导致内存泄露。至此,通过调用 findPathsFromGcRoots() 方法可以将所有泄露对象的引用节点都查询出来了。

最短路径

返回上层方法 FindLeakInput.findLeaks,会发现在通过 findPathsFromGcRoots() 获取的节点中,一个泄露对象可能会有多个引用路径,所以我们还需要做一次遍历,找到每个泄露对象的最短路径(导致泄露的可能性最大)。

private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): LeaksAndUnreachableObjects {
  val pathFinder = PathFinder(graph, listener, referenceMatchers)
  val pathFindingResults =
    pathFinder.findPathsFromGcRoots(leakingObjectIds, computeRetainedHeapSize)

  val unreachableObjects = findUnreachableObjects(pathFindingResults, leakingObjectIds)

  val shortestPaths =
    deduplicateShortestPaths(pathFindingResults.pathsToLeakingObjects)
//...
}

构建最短路径树:

  private fun deduplicateShortestPaths(
    inputPathResults: List<ReferencePathNode>
  ): List<ShortestPath> {
    val rootTrieNode = ParentNode(0)

    inputPathResults.forEach { pathNode ->
      // Go through the linked list of nodes and build the reverse list of instances from
      // root to leaking.
      val path = mutableListOf<Long>()
      var leakNode: ReferencePathNode = pathNode
      while (leakNode is ChildNode) {
        path.add(0, leakNode.objectId)
        leakNode = leakNode.parent
      }
      path.add(0, leakNode.objectId)
      updateTrie(pathNode, path, 0, rootTrieNode)
    }

    val outputPathResults = mutableListOf<ReferencePathNode>()
    findResultsInTrie(rootTrieNode, outputPathResults)

    if (outputPathResults.size != inputPathResults.size) {
      SharkLog.d {
        "Found ${inputPathResults.size} paths to retained objects," +
          " down to ${outputPathResults.size} after removing duplicated paths"
      }
    } else {
      SharkLog.d { "Found ${outputPathResults.size} paths to retained objects" }
    }

    return outputPathResults.map { retainedObjectNode ->
      val shortestChildPath = mutableListOf<ChildNode>()
      var node = retainedObjectNode
      while (node is ChildNode) {
        shortestChildPath.add(0, node)
        node = node.parent
      }
      val rootNode = node as RootNode
      ShortestPath(rootNode, shortestChildPath)
    }
  }

  private fun updateTrie(
    pathNode: ReferencePathNode,
    path: List<Long>,
    pathIndex: Int,
    parentNode: ParentNode
  ) {
    val objectId = path[pathIndex]
    if (pathIndex == path.lastIndex) {
      parentNode.children[objectId] = LeafNode(objectId, pathNode)
    } else {
      val childNode = parentNode.children[objectId] ?: {
        val newChildNode = ParentNode(objectId)
        parentNode.children[objectId] = newChildNode
        newChildNode
      }()
      if (childNode is ParentNode) {
        updateTrie(pathNode, path, pathIndex + 1, childNode)
      }
    }
  }

通过遍历泄露对象节点的父节点,构建出一棵树,多个相同泄露对象节点的不同路径,最终获取最短路径的树。多条最短路径(不同泄露对象)最终合并成一棵树。(这里已经是 shark 的算法内容,不做多描述,说得越多可能错得越多)

生成 LeakTrace

从上面生成泄露对象路径 ReferencePathNode 到最终的 LeakTrace

private fun FindLeakInput.findLeaks(leakingObjectIds: Set<Long>): LeaksAndUnreachableObjects {
    //...
  val inspectedObjectsByPath = inspectObjects(shortestPaths)
  //...
}

有两个地方需要注意下:

  • inspectObjects
    通过调用FindLeakInput.inspectObjects,可以对每个 ObjectReporter 添加一些说明。
  private fun FindLeakInput.inspectObjects(shortestPaths: List<ShortestPath>): List<List<InspectedObject>> {
  //...
    objectInspectors.forEach { inspector ->
      leakReportersByPath.forEach { leakReporters ->
        leakReporters.forEach { reporter ->
          inspector.inspect(reporter)
        }
      }
    }

    return leakReportersByPath.map { leakReporters ->
        computeLeakStatuses(leakReporters)
    }
  }

例如源码中的判断对象是否为单例:

  /**
   * Inspector that automatically marks instances of the provided class names as not leaking
   * because they're app wide singletons.
   */  class AppSingletonInspector(private vararg val singletonClasses: String) : ObjectInspector {
    override fun inspect(
      reporter: ObjectReporter
    ) {
      if (reporter.heapObject is HeapInstance) {
        reporter.heapObject.instanceClass
          .classHierarchy
          .forEach { heapClass ->
            if (heapClass.name in singletonClasses) {
              reporter.notLeakingReasons += "${heapClass.name} is an app singleton"
            }
          }
      }
    }
  }

可以通过 Config.objectInspectors 添加自定义的 ObjectInspector。

  • LeakingStatus
    inspectObjects方法返回时还调用了computeLeakStatuses,通过调用 HeapAnalyzer.computeLeakStatuses() 可以计算路径上每个节点的泄露状态。

UI 展现

HeapDumpTrigger.dumpHeap完成后会调用HeapAnalyzerService.runAnalysis

fun runAnalysis(
  context: Context,
  heapDumpFile: File,
  heapDumpDurationMillis: Long? = null,
  heapDumpReason: String = "Unknown"
) {
  val intent = Intent(context, HeapAnalyzerService::class.java)
  intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile)
  intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason)
  heapDumpDurationMillis?.let {
    intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis)
  }
  startForegroundService(context, intent)
}
override fun onHandleIntentInForeground(intent: Intent?) {
//...
    val config = LeakCanary.config
    val heapAnalysis = if (heapDumpFile.exists()) {
      analyzeHeap(heapDumpFile, config)
    } else {
      missingFileFailure(heapDumpFile)
    }
    val fullHeapAnalysis = when (heapAnalysis) {
      is HeapAnalysisSuccess -> heapAnalysis.copy(
        dumpDurationMillis = heapDumpDurationMillis,
        metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason)
      )
      is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis)
    }
    onAnalysisProgress(REPORTING_HEAP_ANALYSIS)
    config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis)
//...
}

当前 hprof 文件分析成功后,会回调 onHeapAnalyzed() 方法,在默认实现的 DefaultOnHeapAnalyzedListener 中,可以看到内存泄露导出后的相关通知内容。

override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
  SharkLog.d { "\u200B\n${LeakTraceWrapper.wrap(heapAnalysis.toString(), 120)}" }

  val db = LeaksDbHelper(application).writableDatabase
  val id = HeapAnalysisTable.insert(db, heapAnalysis)
  db.releaseReference()

  val (contentTitle, screenToShow) = when (heapAnalysis) {
    is HeapAnalysisFailure -> application.getString(
      R.string.leak_canary_analysis_failed
    ) to HeapAnalysisFailureScreen(id)
    is HeapAnalysisSuccess -> {
      val retainedObjectCount = heapAnalysis.allLeaks.sumBy { it.leakTraces.size }
      val leakTypeCount = heapAnalysis.applicationLeaks.size + heapAnalysis.libraryLeaks.size
      application.getString(
        R.string.leak_canary_analysis_success_notification, retainedObjectCount, leakTypeCount
      ) to HeapDumpScreen(id)
    }
  }

  if (InternalLeakCanary.formFactor == TV) {
    showToast(heapAnalysis)
    printIntentInfo()
  } else {
    showNotification(screenToShow, contentTitle)
  }
}

当点击通知栏消息后,再跳转到 LeakActivity:

private fun showNotification(
  screenToShow: Screen,
  contentTitle: String
) {
  val pendingIntent = LeakActivity.createPendingIntent(
    application, arrayListOf(HeapDumpsScreen(), screenToShow)
  )

  val contentText = application.getString(R.string.leak_canary_notification_message)

  Notifications.showNotification(
    application, contentTitle, contentText, pendingIntent,
    R.id.leak_canary_notification_analysis_result,
    LEAKCANARY_MAX
  )
}

总结

LeakCanary 通过 ContentProvider 的形式进行自动初始化,初始化时会默认注册ActivityWatcherFragmentAndViewModelWatcherRootViewWatcherServiceWatcher,额外还会初始化一个核心类InternalLeakCanary,用于创建各种回调钩子。以ActivityWatcher为例,当Activity销毁时,将 activity 的弱引用加入到观察列表中,然后触发InternalLeakCanary.onObjectRetained(),从而触发HeapDumpTrigger类的checkRetainedObjects()方法,先手动 gcTrigger.runGc(),再检测泄露对象。在导出堆数据时,使用的是 android.os.Debug 类的 Debug.dumpHprofData(heapDumpFile.absolutePath),导出一个Hprof文件,导出完成后,调用HeapAnalyzerService前台服务,开始堆栈分析,分析使用的是 Shark,涉及到Hprof相关内容以及由 GC root 组建的最短路径树,分析完成后回调DefaultOnHeapAnalyzedListener.onHeapAnalyzed,触发相关通知显示以及点击后的 LeakActivity 数据展示。

参考

主要参考/复制:LeakCanary2 源码分析

源码分析耗时巨大,感谢原作者。

其他:

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

https://square.github.io/leakcanary/shark/


本站由以下主机服务商提供服务支持:

0条评论

发表评论