LeakCanary 源码解析检测泄露工作机制(一)

引用计数的有个缺陷就是识别不了循环引用, JVM GC 的核心是可达性分析 :从 GC Roots 出发,判断对象是否仍然可达;不可达的对象才会被回收。理解这一点是读懂 LeakCanary 的前提 ------它后续分析 .hprof 文件时(由 Shark 库完成),正是复用了同样的可达性分析算法来构建引用链,从而定位泄漏根因。如果只有引用计数法,Shark 就无法在存在循环引用的复杂对象图中找到真正的泄漏路径。

LeakCanary 的源码解析(本次解析针对2.14版本),这边强烈额建议去 GitHub 上下载源码或在 Android Studio 导入依赖,一边看源码一边读。

我们知道在 Java 中一个对象只要被强引用指向, 就不会被 GC 回收, 比如:

kotlin 复制代码
var activity = Activity()

WeakReference是一种弱引用: 它指向一个对象, 但它不阻止GC回收它.当没有强引用指向对象时, GC就会回收它, 同时把这个WeakReference放入一个ReferenceQueue,所以我们可以像这样

kotlin 复制代码
val queue = ReferenceQueue<Any>()
val ref = WeakReference(mActivity,queue)

当我们调用队列的poll()方法并得到ref对象时, 说明mActivity对象已被GC回收, 而 LeakCanary 正是利用这个机制:延迟一段时间,如果WeakReference仍然没有进入队列,说明对象仍然被强引用指向,从而检测内存泄漏

首先我们看到 KeyedWeakReference.kt , 代码量并不多但是是整个 LeakCanary 的基石,可以看到它继承自 WeakReference ,重写了 clear 方法

kotlin 复制代码
class KeyedWeakReference(
  referent: Any,
  val key: String,
  val description: String,
  val watchUptimeMillis: Long,
  referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
  referent, referenceQueue
) {
  @Volatile
  var retainedUptimeMillis = -1L

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

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

retainedUptimeMillis的意思是"泄漏已持续的时间",初始值为-1标注该弱引用暂时未被判定为泄漏, 接着往下看看到重写的 clear 方法, 如果你深入 super.clear 你最终可以看到最终调用的是Reference类的 clear0() ,它是一个 native 方法,它的实现在 HotSpot VM 的 C++ 源码中, 核心作用就是切断对 referent 的弱引用,而不是等到GC.

再来看到 ObjectWatcher.kt

kotlin 复制代码
class ObjectWatcher constructor(
  private val clock: Clock,
  private val checkRetainedExecutor: Executor,
  private val isEnabled: () -> Boolean = { true }
) : ReachabilityWatcher {

  private val onObjectRetainedListeners = mutableSetOf<OnObjectRetainedListener>()

  private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()

  private val queue = ReferenceQueue<Any>()

  val hasRetainedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.any { it.value.retainedUptimeMillis != -1L }
    }

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

  val hasWatchedObjects: Boolean
    @Synchronized get() {
      removeWeaklyReachableObjects()
      return watchedObjects.isNotEmpty()
    }


  val retainedObjects: List<Any>
    @Synchronized get() {
      removeWeaklyReachableObjects()
      val instances = mutableListOf<Any>()
      for (weakReference in watchedObjects.values) {
        if (weakReference.retainedUptimeMillis != -1L) {
          val instance = weakReference.get()
          if (instance != null) {
            instances.add(instance)
          }
        }
      }
      return instances
    }

  @Synchronized fun addOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.add(listener)
  }

  @Synchronized fun removeOnObjectRetainedListener(listener: OnObjectRetainedListener) {
    onObjectRetainedListeners.remove(listener)
  }

  @Deprecated(
    "Add description parameter explaining why an object is watched to help understand leak traces.",
    replaceWith = ReplaceWith(
      "expectWeaklyReachable(watchedObject, \"Explain why this object should be garbage collected soon\")"
    )
  )
  fun watch(watchedObject: Any) {
    expectWeaklyReachable(watchedObject, "")
  }

  @Deprecated(
    "Method renamed expectWeaklyReachable() to clarify usage.",
    replaceWith = ReplaceWith(
      "expectWeaklyReachable(watchedObject, description)"
    )
  )
  fun watch(
    watchedObject: Any,
    description: String
  ) {
    expectWeaklyReachable(watchedObject, description)
  }


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

  @Synchronized fun clearObjectsWatchedBefore(heapDumpUptimeMillis: Long) {
    val weakRefsToRemove =
      watchedObjects.filter { it.value.watchUptimeMillis <= heapDumpUptimeMillis }
    weakRefsToRemove.values.forEach { it.clear() }
    watchedObjects.keys.removeAll(weakRefsToRemove.keys)
  }

  @Synchronized fun clearWatchedObjects() {
    watchedObjects.values.forEach { it.clear() }
    watchedObjects.clear()
  }

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

  private fun removeWeaklyReachableObjects() {
    var ref: KeyedWeakReference?
    do {
      ref = queue.poll() as KeyedWeakReference?
      if (ref != null) {
        watchedObjects.remove(ref.key)
      }
    } while (ref != null)
  }
}

watchedObjects是一个 Map ,它的 key 其实就是创建 KeyedWeakReference 所需要的参数 key , 它的目的是维护所有正在被观测的弱引用. 在这里我们就可以看到 LeakCanary 用来判断 KeyedWeakReference 所指向对象是否回收的工具

kotlin 复制代码
private val queue = ReferenceQueue<Any>()

首先看到第一个方法, hasRetainedObject , 它会调用 removeWeaklyReachableObject() ,而该方法内部会将队列中的元素取出强转为 KeyedWeakReference 类型再来判断该引用指向的对象是否被回收,如果已被回收则将该引用从 watchedObjects 中移除并不断循环直到队列清空,然后返回 watchedObjects 集合中,是否存在任何一个对象,它的 retainedUptimeMillis 值不等于 -1,也就是被判定为retained的检查结果 retainedObjectCount 方法和 hasWatchedObjects 的逻辑差不多, 在这里不过多阐述. 而 retainedObjects 方法的作用则是返回所有被判定为泄漏的对象的强引用列表.

我们接着看到核心方法: expectWeaklyReachable 他是一个线程安全的方法, 它的作用是:

  • 对象已经被创建了(比如 Activity 调用了 onDestroy()
  • 我们期望它能被 GC 回收
  • 所以创建一个 WeakReference 指向它,放入 watchedObjects 监控
  • 如果过了段时间,这个 WeakReference 还没被 ReferenceQueue 回收,说明对象泄漏了

我们接着看到该方法的最后一段代码

kotlin 复制代码
checkRetainedExecutor.execute {
      moveToRetained(key)
    }

它在LeakCanary初始化时是这样传入的

kotlin 复制代码
AppWatcher.objectWatcher = ObjectWatcher(
    clock = Clock.uptimeMillis,
    checkRetainedExecutor = {
        //retainedDelayMillis值默认5s
        mainHandler.postDelayed(it, AppWatcher.config.retainedDelayMillis)
    },
    isEnabled = { AppWatcher.config.enabled }
)

也就是该执行器会在5s之后在主线程执行 moveToRetained(key) 方法 我们再看到 moveToRetained 方法,它依然会先清理 watchedObjects 中所有被回收的对象, 从而根据该 watchedObjects[key] 是否为空来判断被指向的对象是否泄漏,如果泄漏则

  • retainedUptimeMillis = now (泄漏)
  • 通知所有 listener → 触发 heap dump

所以 ObjectWatcher 的本质其实就是通过 MapReferenceQueue 和五秒延迟(给GC足够的时间回收)来得到那些剩下的在 Map 中的泄漏对象

接下来要看的类是 HeapDumpTrigger ,这边建议去 github 下载2.14版本的压缩包或者是在 AndroidStudio导入依赖打开HeapDumpTrigger.kt 查看源码, 此处完整源码就不贴出来了, 只贴出关键部分, 因为太长了,Heap Dump (堆转储) 是 Java 虚拟机(JVM/ART)堆内存中所有对象、类、引用关系和实例数据的一份完整快照文件,其中包括对象间的引用关系,通过分析快照即可找出泄漏的对象和引用链.

我们来分析第一个方法 checkRetainedObjects()

kotlin 复制代码
private fun checkRetainedObjects() {
    val iCanHasHeap = HeapDumpControl.iCanHasHeap()

    val config = configProvider()

    if (iCanHasHeap is Nope) {
      if (iCanHasHeap is NotifyingNope) {
        
        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"
    )
  }

首先他会判断当前环境是否支持 Dump ,比如 LeakCanary 是否被启用,当前是否是调试状态等,他返回的是一个ICanHazHeap密封类

kotlin 复制代码
sealed class ICanHazHeap {
    object Yup : ICanHazHeap()//允许Dump
    abstract class Nope(val reason: () -> String) : ICanHazHeap()//不允许
    class SilentNope(reason: () -> String) : Nope(reason)//静默拒绝,不通知用户
    class NotifyingNope(reason: () -> String) : Nope(reason)//拒绝但通知用户
  }

可以看到在通知用户 LeakCanary 不能 dump heap 之前, 它会先检查一遍是否仍然有泄漏的对象. 之后它会调用 ObjectWatcher 也就是我们刚刚分析过的类的 retainedObjectCount 方法来获取一次泄漏对象的数量, 如果大于0则主动触发GC再进行一次回收再去获取一次.确保在此期间没有被判定为泄漏的对象被回收. 然后它会调用 checkRetainedCount() 方法,如果返回值为 true ,则直接 return , 这个放着待会再看. 如果 checkRetainedCount 方法返回 false , 则获取当前时间戳并计算距离上次 Heap Dump 过去了多久是否满足两次 Heap Dump 间隔不小于60s的条件, 如果不满足则再通知用户泄漏对象数量并调用 scheduleRetainedObjectCheck 方法传入距离下次允许 Heap Dump 的时间还要多久然后返回,否则直接调用 dumpHeap 方法发送通知并开始分析内存泄漏.

我们接下来依次看到 checkRetainedCount() , scheduleRetainedObjectCheck()dumpHeap() 方法 checkRetainedCount() 方法太长,这里只贴出关键逻辑,删去了提示信息相关代码

kotlin 复制代码
private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int,
    nopeReason: String? = null
  ): Boolean {
    val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
    lastDisplayedRetainedObjectCount = retainedKeysCount
    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 (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
          )
        )
        scheduleRetainedObjectCheck(
          delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
  }

首先它会判断 lastDisplayedRetainedObjectCount (当前展示给用户看到的泄漏数量)是否与 retainedKeysCount (调用该方法者提供的检测到的泄漏数量)一致, 并将之对齐. 然后它会判断 retainedKeysCount 是否小于 retainedVisibleThreshold (该变量值为5),如果小于并且应用可见或者刚进入后台则进入下一层判断,否则 return false ,下一层判断接着判断泄漏数量如果改变则调用 onRetainInstanceListener.onEvent() 方法.无论是否改变都通知用户并且调用 scheduleRetainedObjectCheck 方法, delayMillis = WAIT\_FOR\_OBJECT\_THRESHOLD\_MILLIS 也就是 2\_000L ,最后 return true .

再来看到 scheduleRetainedObjectCheck

kotlin 复制代码
fun scheduleRetainedObjectCheck(
    delayMillis: Long = 0L
  ) {
    val checkCurrentlyScheduledAt = checkScheduledAt
    if (checkCurrentlyScheduledAt > 0) {
      return
    }
    checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
    backgroundHandler.postDelayed({
      checkScheduledAt = 0
      checkRetainedObjects()
    }, delayMillis)
  }

它首先会检查当前是否已有检查任务在排队, 有的话直接 return , 然后记录调度时间然后在后台 HandlerThread 中延迟执行, 我们可以看到该 HandlerThread 的创建逻辑在 InternalLeakCanary.ktinvoke() 方法中

kotlin 复制代码
val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper)
heapDumpTrigger = HeapDumpTrigger(
    application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
    configProvider
)

接着看 heapDump() 方法,为了快速理解核心逻辑, 我删除了大部分代码, 建议可以去对照源码阅读

kotlin 复制代码
private fun dumpHeap() {
    val heapDumpFile = directoryProvider.newHeapDumpFile()
    
    val heapDumpUptimeMillis = SystemClock.uptimeMillis()
    KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
    
    configProvider().heapDumper.dumpHeap(heapDumpFile)

    objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
}

其主要逻辑便是:创建 .hprof 文件->记录当前 Dump 时间戳->执行堆转储->清理本次 Dump 之前的所有弱引用 , dump 以及把堆里的 KeyedWeakReference 实例写入了 hprof 文件, 这些引用已经归档到文件中, 运行时不再需要.

我们最后看到 onDumpHeapReceived 方法,他是用户点击通知或调用 LeakCanary.dumpHeap() 时的入口

kotlin 复制代码
fun onDumpHeapReceived(forceDump: Boolean) {
  backgroundHandler.post {
    gcTrigger.runGc()                            // 先尝试触发 GC
    val count = objectWatcher.retainedObjectCount
    if (!forceDump && count == 0) {
      // showNoMoreRetainedObjectNotification()
      return                                     // 非强制 + 没对象 → 不 dump
    }
    dumpHeap(count, retry = false, reason = "user request")
  }
}

注意区分两种触发路径

  • 自动监控流程checkRetainedCount()retainedCount < 5 且 App 可见时,发出"waiting until 5 retained objects"通知;
  • 用户手动触发 :用户点击上述通知,或直接调用 LeakCanary.dumpHeap(),才会进入 onDumpHeapReceived(forceDump = true)。此时即使只有 2 个泄漏对象,也会强制执行 Heap Dump ,不受阈值限制。

其核心逻辑如下: 先尝试触发 GC(gcTrigger.runGc()) ,尽可能回收可释放对象; 获取当前泄漏对象数量:val count = objectWatcher.retainedObjectCount ; 若 非强制触发 (forceDump = false) 且无泄漏对象 (count == 0),则直接返回,不执行 Heap Dump ; 否则,调用 dumpHeap(count, retry = false, reason = "user request") 强制生成堆转储文件。

HeapDumpTrigger的决策树

最近忙着找实习, 剩下的部分以后有时间再发。

相关推荐
且随疾风前行.20 小时前
Android Binder 驱动 - 内核驱动层源码初探
android·网络·binder
额恩6620 小时前
AI 智能体从零搭建实战教程——扣子
android·rxjava·coze
hunterandroid20 小时前
前台服务适配与线上排查:通知权限、启动限制和任务保活
android·前端
帅次21 小时前
Android 高级工程师面试:Flutter 渲染与性能 近1年高频追问 20 题
android·flutter·面试·渲染·性能
糖果店的幽灵1 天前
【langgraph 从入门到精通graphApi 篇】Command 与动态流程控制
android·java·数据库·人工智能·langgraph
Android-Flutter1 天前
Android的http和https知识点
android·http·https
Kapaseker1 天前
Sequence 一定比 List 快?等等,我们先从基础讲起
android·kotlin
AI刀刀1 天前
deepseek 内容粘贴后符号丢失怎么办?AI 导出鸭实测解决排版乱码问题
android·人工智能·excel·ai导出鸭
东方佑1 天前
Per-Group 混合精度量化:将 14B 视频生成模型压缩至 11 GB
android