android LeakCanary 工作原理 详解

一、LeakCanary 是什么

LeakCanary 是由 Square 开源的 Android 内存泄漏检测工具,专门用于在开发阶段自动识别 Java 层的内存泄漏问题。它通过监控对象生命周期 + 弱引用检测 + 堆转储分析的组合方案,帮助开发者定位内存泄漏根因。


二、核心工作原理

2.1 检测基石:WeakReference + ReferenceQueue

LeakCanary 的核心检测机制建立在 Java 的弱引用(WeakReference) 和**引用队列(ReferenceQueue)**之上:

  • WeakReference:指向被监控对象,当对象仅被弱引用持有时,GC 会回收该对象

  • ReferenceQueue:当 WeakReference 指向的对象被 GC 回收后,该 WeakReference 会自动进入关联的 ReferenceQueue

检测逻辑:

  1. 为被销毁的 Activity 等对象创建 WeakReference,并绑定一个 ReferenceQueue

  2. 延迟 5 秒后检查该 WeakReference 是否已进入队列

  3. 若未进入,手动触发 GC 后再检查一次

  4. 若仍未进入队列,则判定该对象发生内存泄漏

2.2 完整工作流程

LeakCanary 的工作流程可分为 5 个阶段:

阶段 说明
1. 注册监听 通过生命周期回调或 Hook 技术,感知对象进入"无用"状态的时机
2. 监控泄漏 为无用对象创建弱引用,延迟检查是否被回收;泄漏对象计数达到阈值才触发分析
3. Heap Dump 将 Java 堆转储为 .hprof 文件(会短暂冻结应用)
4. 堆分析 使用 Shark 解析 .hprof,找出阻止 GC 的引用链(leak trace)
5. 泄漏分类 将泄漏分为 Application Leaks 和 Library Leaks 两类展示

三、支持的监控类型与实现方式

LeakCanary 自动监控以下 6 种对象的泄漏:

监控对象 监听方式 关键源码文件
Activity Application.registerActivityLifecycleCallbacks() 监听 onActivityDestroyed ActivityWatcher.kt
Fragment 同上 + FragmentManager.registerFragmentLifecycleCallbacks() FragmentAndViewModelWatcher.kt
Fragment View 在 Fragment 生命周期回调中监听 View 销毁 FragmentAndViewModelWatcher.kt
ViewModel 自定义 ViewModel + 反射获取 ViewModelStore.mMap,在 onCleared() 中遍历检测 ViewModelClearedWatcher.kt
Service Hook ActivityThread.mH.mCallback 监听 STOP_SERVICE + 动态代理 IActivityManager.serviceDoneExecuting() ServiceWatcher.kt
RootView (Dialog等) Hook WindowManagerGlobal.mViews,监听 onViewDetachedFromWindow RootViewWatcher.kt

关键源码示例

Activity 监控:

kotlin

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

ViewModel 监控(通过反射 Hook):

kotlin

复制代码
internal class ViewModelClearedWatcher(
    storeOwner: ViewModelStoreOwner,
    private val reachabilityWatcher: ReachabilityWatcher
) : ViewModel() {

    private val viewModelMap: Map<String, ViewModel>? = try {
        val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
        mMapField.isAccessible = true
        mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
    } catch (ignored: Exception) { null }

    override fun onCleared() {
        viewModelMap?.values?.forEach { viewModel ->
            reachabilityWatcher.expectWeaklyReachable(viewModel, "...")
        }
    }
}

四、核心源码分析

4.1 ObjectWatcher ------ 泄漏判定中心

ObjectWatcher 是 LeakCanary 的核心类,负责判定对象是否泄漏:

kotlin

复制代码
class ObjectWatcher(
    private val clock: Clock,
    private val checkRetainedExecutor: Executor,
    private val isEnabled: () -> Boolean
) {
    private val watchedObjects = mutableMapOf<String, KeyedWeakReference>()
    private val queue = ReferenceQueue<Any>()

    fun watch(watchedObject: Any, description: String) {
        val key = UUID.randomUUID().toString()
        // 创建带 key 的弱引用,绑定引用队列
        val weakReference = KeyedWeakReference(watchedObject, key, description, queue)
        watchedObjects[key] = weakReference
        
        // 延迟 5 秒后检查
        checkRetainedExecutor.execute {
            moveToRetained(key)
        }
    }

    private fun moveToRetained(key: String) {
        removeWeaklyReachableObjects() // 清理已回收的对象
        
        val retainedRef = watchedObjects[key]
        if (retainedRef != null) {
            // 对象未被回收,触发泄漏通知
            onObjectRetainedListeners.forEach { it.onObjectRetained() }
        }
    }

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

判定逻辑:

  1. 为对象创建 KeyedWeakReference(带唯一 key 的弱引用),存入 watchedObjects 映射表

  2. postDelay 5 秒后检查引用队列

  3. 若对象已被 GC,其弱引用会进入 queue,从 watchedObjects 移除

  4. 若对象仍在 watchedObjects 中,说明未被回收,标记为泄漏

4.2 HeapDumpTrigger ------ 触发堆转储的"守门员"

LeakCanary 不会每次发现泄漏都立即 Dump ,而是通过 HeapDumpTrigger 进行多层拦截:

kotlin

复制代码
private fun checkRetainedObjects() {
    var retainedReferenceCount = objectWatcher.retainedObjectCount
    if (retainedReferenceCount > 0) {
        gcTrigger.runGc()  // 主动触发 GC
        retainedReferenceCount = objectWatcher.retainedObjectCount
    }

    // 拦截 1:泄漏对象数未达阈值
    if (retainedKeysCount < retainedVisibleThreshold) {
        if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
            showRetainedCountNotification("App visible, waiting...")
            scheduleRetainedObjectCheck(WAIT_FOR_OBJECT_THRESHOLD_MILLIS)
            return
        }
    }

    // 拦截 2:距离上次 HeapDump 未超过 60s
    val elapsedSinceLastDump = SystemClock.uptimeMillis() - lastHeapDumpUptimeMillis
    if (elapsedSinceLastDump < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
        scheduleRetainedObjectCheck(WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDump)
        return
    }

    // 通过拦截,执行 Heap Dump
    dumpHeap(...)
}

阈值策略:

  • App 前台可见 :阈值为 5 个泄漏对象

  • App 后台不可见 :阈值为 1 个泄漏对象

4.3 GcTrigger ------ 主动触发 GC

kotlin

复制代码
object Default : GcTrigger {
    override fun runGc() {
        Runtime.getRuntime().gc()  // 比 System.gc() 更可能触发 GC
        Thread.sleep(100)        // 等待 GC 完成
        System.runFinalization()   // 执行 Finalizer
    }
}

4.4 Shark ------ 堆分析引擎

Dump 生成的 .hprof 文件由 Shark(LeakCanary 自研的堆分析库)解析,主要工作:

  1. 定位泄漏对象:在堆快照中找到 retained 对象

  2. 计算引用链:使用 ** dominator tree** 算法找到从 GC Root 到泄漏对象的最短引用路径

  3. 生成签名:将引用链上的类名 + 字段名串联哈希,相同签名的泄漏归为一组

  4. 标记怀疑对象 :从第一个 LEAKING 节点到第一个 NOT_LEAKING 节点之间的引用标记为 ~~~ 怀疑对象


五、泄漏报告解读

LeakCanary 的分析结果分为两类:

  • Application Leaks:应用自身代码导致的泄漏,需要开发者修复

  • Library Leaks:第三方库已知的泄漏,LeakCanary 内置了常见库(如 Android Framework、Support Library)的已知泄漏规则

报告示例

复制代码
====================================
HEAP ANALYSIS RESULT
====================================
2 APPLICATION LEAKS

Displaying only 1 leak trace out of 2 with the same signature
Signature: ce9dee3a1feb859fd3b3a9ff51e3ddfd8efbc6
┬───
│ GC Root: Local variable in native code
│
├─ com.example.LeakingSingleton class
│    Leaking: NO (a class is never leaking)
│    ↓ static LeakingSingleton.leakedViews
│                              ~~~~~~~~~~~
├─ java.util.ArrayList instance
│    Leaking: UNKNOWN
│    ↓ ArrayList.elementData
│                ~~~~~~~~~~~
├─ java.lang.Object[] array
│    Leaking: UNKNOWN
│    ↓ Object[].elementData[0]
│               ~~~
├─ android.widget.TextView instance
│    Leaking: YES (View.mContext references a destroyed activity)
...

六、整体架构图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    App 生命周期层                        │
│  ActivityWatcher │ FragmentWatcher │ ServiceWatcher │ ...  │
└────────────────────┬────────────────────────────────────┘
                     │ expectWeaklyReachable(object)
                     ▼
┌─────────────────────────────────────────────────────────────┐
│                  ObjectWatcher                          │
│  ┌─────────────┐    ┌─────────────┐    ┌────────┐ │
│  │KeyedWeakRef │───▶│ ReferenceQueue│◀──│  GC    │ │
│  │  (watched) │    │  (poll检查)  │    │        │ │
│  └─────────────┘    └─────────────┘    └────────┘ │
│         │ 5秒后未回收                              │
│         ▼                                          │
│   onObjectRetained() ────────────────────────────────┘
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────────┐
│                HeapDumpTrigger                          │
│  阈值检查(前台5/后台1)│ 60s间隔检查 │ 主动GC确认    │
└────────────────────┬────────────────────────────────────┘
                     │ 通过检查
                     ▼
┌─────────────────────────────────────────────────────────────┐
│              Heap Dump (.hprof)                       │
│         Debug.dumpHprofData(heapDumpFile)             │
└────────────────────┬────────────────────────────────────┘
                     ▼
┌─────────────────────────────────────────────────────────────┐
│                   Shark 分析引擎                         │
│  解析堆文件 → 找最短引用链 → 生成签名 → 分类展示        │
└─────────────────────────────────────────────────────────────┘

七、总结

要点 说明
检测核心 WeakReference + ReferenceQueue
触发时机 Activity/Fragment/ViewModel 等销毁后延迟 5 秒检查
防误报机制 手动触发 GC + 双重确认 + 阈值拦截
堆分析 使用 Shark 自研库解析 .hprof,计算 dominator tree
性能保护 前台阈值 5、后台阈值 1、60s 内不重复 Dump
扩展性 支持自定义 ObjectWatcher.watch() 检测任意对象

LeakCanary 的设计精髓在于:用弱引用做轻量筛查,用阈值策略控制 Dump 频率,用 Shark 做精准分析。

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work5 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work5 天前
ch21 签名、校验与发版
kotlin
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui