LeakCanary 原理分析

LeakCanary 原理分析

源码参考:square/leakcanary (GitHub)

一、整体结构

php 复制代码
LeakCanary
├── 监听入口:ActivityLifecycleCallbacks、Fragment 等
├── RefWatcher      - 核心,WeakReference + ReferenceQueue 检测
├── KeyedWeakReference - 带 key 的弱引用
├── HeapDumper     - 生成 .hprof 内存快照
└── HeapAnalyzer   - 解析 hprof,构建泄漏路径

二、核心原理:WeakReference + ReferenceQueue

2.1 Java 引用与 ReferenceQueue

当对象只被 WeakReference 引用时,GC 会回收该对象。回收后,对应的 WeakReference 会被放入关联的 ReferenceQueue。

php 复制代码
正常回收:
  对象 ──(仅被 WeakReference 引用)──→ GC 回收
                                          │
  WeakReference ──────────────────────────┘
          │
          └──→ 自动入队 ReferenceQueue

内存泄漏:
  对象 ──(被其他强引用持有)──→ GC 不回收
          │
  WeakReference ──→ 不入队,retainedKeys 仍包含其 key

2.2 KeyedWeakReference

java 复制代码
// KeyedWeakReference.java
final class KeyedWeakReference extends WeakReference<Object> {
    public final String key;   // 随机 UUID,用于标识
    public final String name;  // 逻辑名,如 "Activity"

    KeyedWeakReference(Object referent, String key, String name,
                       ReferenceQueue<Object> referenceQueue) {
        super(referent, referenceQueue);  // 关联引用队列
        this.key = key;
        this.name = name;
    }
}
  • referent:被监控对象(如 Activity)
  • key :UUID,存入 retainedKeys,用于判断是否已回收
  • referenceQueue:对象被 GC 后,该 Reference 会入队

三、RefWatcher:核心检测逻辑

3.1 watch():开始监控

java 复制代码
// RefWatcher.java
public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) return;

    String key = UUID.randomUUID().toString();
    retainedKeys.add(key);  // 1. 将 key 加入强引用 Set

    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);
    // 2. 创建弱引用,关联 queue

    ensureGoneAsync(watchStartNanoTime, reference);
    // 3. 异步执行检测
}

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
        @Override public Retryable.Result run() {
            return ensureGone(reference, watchStartNanoTime);
        }
    });
}
步骤 逻辑 说明
1 retainedKeys.add(key) 用强引用 Set 持有 key,与 WeakReference 分离
2 new KeyedWeakReference(..., queue) 弱引用指向对象,并关联 ReferenceQueue
3 ensureGoneAsync() 延迟后(如 5s)在子线程执行 ensureGone()

3.2 removeWeaklyReachableReferences():清理已回收的 key

java 复制代码
// 当对象被 GC 回收时,其 WeakReference 会入队
private void removeWeaklyReachableReferences() {
    KeyedWeakReference ref;
    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        retainedKeys.remove(ref.key);  // 从 Set 中移除,表示已回收
    }
}

3.3 gone():判断是否已回收

java 复制代码
private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
}
  • 对象被 GC → WeakReference 入队 → removeWeaklyReachableReferences() 移除 key → gone() 为 true
  • 对象未被 GC(泄漏)→ WeakReference 不入队 → key 仍在 retainedKeys → gone() 为 false

3.4 ensureGone():完整检测流程

java 复制代码
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    removeWeaklyReachableReferences();
    // 1. 先清理已入队的引用

    if (debuggerControl.isDebuggerAttached()) {
        return RETRY;  // 调试器可能造成误报
    }
    if (gone(reference)) {
        return DONE;   // 2. 已回收,结束
    }

    gcTrigger.runGc();
    // 3. 主动触发 GC
    removeWeaklyReachableReferences();

    if (!gone(reference)) {
        // 4. 仍未回收,判定泄漏
        File heapDumpFile = heapDumper.dumpHeap();
        heapdumpListener.analyze(
            new HeapDump(heapDumpFile, reference.key, reference.name, ...));
    }
    return DONE;
}
步骤 逻辑 说明
1 removeWeaklyReachableReferences() 处理已入队的 Reference,移除对应 key
2 gone(reference) 若 key 已移除,说明已回收,直接返回
3 gcTrigger.runGc() 主动触发 GC
4 再次 removeWeaklyReachableReferences() + gone() 若仍存在,判定泄漏,执行 heap dump

四、监听入口:Activity 销毁时 watch

java 复制代码
// ActivityRefWatcher(或类似实现)
// 通过 Application.registerActivityLifecycleCallbacks 注册
@Override
public void onActivityDestroyed(Activity activity) {
    refWatcher.watch(activity, activity.getClass().getName());
}
  • onActivityDestroyed() 在 Activity 销毁时调用
  • 此时将 Activity 传给 RefWatcher.watch(),开始监控
  • Fragment、Service、ViewModel 等同理,在对应销毁时机调用 watch()

五、检测流程总览

scss 复制代码
Activity.onDestroy()
    │
    └── refWatcher.watch(activity)
            │
            ├── retainedKeys.add(key)
            ├── new KeyedWeakReference(activity, key, name, queue)
            └── ensureGoneAsync()
                    │
                    └── 延迟 5s 后,子线程执行 ensureGone()
                            │
                            ├── removeWeaklyReachableReferences()
                            │       └── queue.poll(),已回收的 Reference 入队,移除对应 key
                            │
                            ├── gone(reference) ?
                            │       ├── true  ──→ 结束
                            │       └── false ──→ 继续
                            │
                            ├── gcTrigger.runGc()
                            ├── removeWeaklyReachableReferences()
                            ├── gone(reference) ?
                            │       ├── true  ──→ 结束
                            │       └── false ──→ 判定泄漏
                            │
                            └── heapDumper.dumpHeap()
                                    └── heapdumpListener.analyze()
                                            └── 解析 hprof,构建 GC Root → 泄漏对象路径

六、Heap Dump 与泄漏路径分析

阶段 职责
HeapDumper 调用 Debug.dumpHprofData() 生成 .hprof 文件
HeapAnalyzer 解析 hprof,找到 KeyedWeakReference 的 referent(泄漏对象)
路径构建 从泄漏对象反向追溯到 GC Root,得到引用链
展示 通知栏展示泄漏路径,便于定位

七、关键点小结

机制 作用
WeakReference 不阻止 GC,对象可被回收
ReferenceQueue 对象被回收后,Reference 入队,用于判断
retainedKeys 强引用 Set 存 key,与 Reference 分离,避免误判
延迟检测 给 GC 时间,避免误报
主动 GC 确保已触发回收再判断
Heap Dump 泄漏确认后,生成快照分析引用链
相关推荐
花开盛夏^.^10 小时前
电脑打开多个浏览器Web窗口占用较大的运行内存的原因
前端·浏览器
馒头不想说话11 小时前
实体店老板的24小时AI运营员工,写、剪、发全自动,省下一整个团队工资
前端
椒哥13 小时前
一天被问几十次"允许",我统计了 517 次,然后写了个插件
前端·javascript·github
风骏时光牛马14 小时前
生成一个AI稳定性治理及疑难问题分析标题,只需要一个标题,不需要其他的任何文本。
前端
莫西干式秃头14 小时前
用 AI + threejs 写了个浏览器里的 3D 动作游戏《武来》:砍怪、放技能、开 SU7
前端·人工智能·three.js
kyriewen14 小时前
我测了 6 种防请求竞态的写法:只有 1 种真能打
前端·javascript·程序员
孟健14 小时前
用 WorkBuddy 做个人名片网页,挂上开源设计技能后效果完全不同了
前端·人工智能·ai编程
IT_陈寒14 小时前
SpringBoot自动配置的坑,这次终于爬出来了
前端·人工智能·后端
LuHang15 小时前
从天地图切换到高德地图:一次真实的底图迁移实战
前端·vue.js·cesium
用户3600555790015 小时前
Kestrel_VLLM - 27 · 寄存器常驻与融合发射:两次「少发一次」
前端