LeakCanary原理解析

317 阅读30分钟

1. 内存泄露的定义

  • 传统定义:

    • 申请的内存忘记释放了。
  • Android(或 JVM)的内存泄露:

    • 短生命周期的对象被长生命周期的对象持有,导致短生命周期的对象不能被垃圾回收器释放。

2. 垃圾回收机制

Java 和其他语言采用不同的垃圾回收策略,主要包括两种:

2.1 引用计数法

  • 应用语言:

    • Python、Objective-C、Swift 等
  • 原理:

    • 用一个计数器记录对象被引用的次数,当引用计数降到 0 时,该对象被认为是垃圾对象。
  • 缺点:

    • 存在循环引用问题,可能导致垃圾对象无法被回收。

2.2 可达性分析法

  • 应用语言:

    • Java(JVM)
  • 原理:

    • JVM 从一组被称为 GC Roots 的对象出发,向下搜索所有可达对象。
    • 如果一个对象能被 GC Roots 引用到,则认为它不是垃圾;否则,即使对象间互相引用,也会被视为垃圾。
  • GC Roots: GC Root(垃圾回收根)指的是在垃圾回收过程中,作为起点的那些对象。垃圾回收器从这些对象出发,沿着它们的引用链遍历所有可达对象,最终将无法从任何 GC Root 访问到的对象视为垃圾对象,从而进行回收。它包括:

    • 线程栈中的局部变量:
      当前正在执行的方法的参数和局部变量位于线程的栈上,这些对象始终是可达的,因此是 GC 根。

    • 存活的线程对象:
      正在运行或处于等待状态的线程实例由系统维护着强引用,即使线程处于空闲状态也不会被回收。

    • JNI 的引用:
      当原生代码通过 JNI 持有 Java 对象时,这些引用被视为强引用,确保对象不会被 GC 回收,直到 JNI 释放这些引用。

    • Class 对象:
      Java 类在被加载后,Class 对象会被相应的 ClassLoader 保持引用,而 ClassLoader 通常不会被卸载,所以 Class 对象始终是可达的。

    • 静态变量中的引用:
      静态变量属于类级别的引用,只要类被加载(通常整个应用运行期间保持加载状态),这些引用就会一直存在,保证对象不会被回收。


3. 内存泄露问题的影响

  • 内存泄露不会立刻导致程序崩溃,但随着应用使用,被长生命周期对象持有的短生命周期对象无法回收,导致可用内存逐渐减少。
  • 当内存耗尽时,可能在应用的任何位置抛出 OutOfMemoryError,而每次错误堆栈可能都不同,增加问题排查难度。

4. LeakCanary 监听内存泄漏的整体流程和核心原理概要

步骤作用关键点
1️⃣ 监控目标对象销毁时,LeakCanary 创建 KeyedWeakReference(弱引用)指向它,并把生成的 key 记录到 retainedKeys 集合,同时把弱引用绑定到一个 ReferenceQueue只有弱引用,没有任何强引用,对象才会被 GC。
2️⃣ 首次检查过一小段时间(1.x 利用 IdleHandler,2.x 延迟任务),轮询 ReferenceQueue: ▪ 若该弱引用已入队 → 对象被回收,正常; ▪ 否则继续第 3 步。入队动作发生在 GC 真正回收对象之前
3️⃣ 主动 GC触发 GcTrigger.runGc():调用 System.gc() 并短暂 sleep,给系统时间做垃圾回收;随后再次轮询 ReferenceQueue避免因为 GC 还没运行而误判。
4️⃣ 堆转储 & 分析如果还没入队 ⇒ 对象疑似泄漏: ▪ Dump hprof → 在独立线程/进程用 shark/haha 解析; ▪ 计算 GC-Roots 到该对象的引用链; ▪ 生成结果并通过通知、Activity 或回调暴露给开发者。分析阶段才真正定位是谁持有了它。

一句话概括
“队列里能捡到弱引用 ⇒ 对象已回收;捡不到 ⇒ 先强制 GC 再捡;还捡不到 ⇒ 导出堆文件找泄漏。”

在1.6.3版本上,LeakCanary的粗略流程就如下图所示:

image.png

1. 初始化和注册

  • Application 注册
    在 Application 的 onCreate() 方法中,先判断当前进程是否为 LeakCanary 分析进程,避免在该进程中初始化应用逻辑,然后调用 LeakCanary.install(this)

    public class MyApp extends Application {
       @Override
       public void onCreate() {
           super.onCreate();
           if (LeakCanary.isInAnalyzerProcess(this)) {
               return;
           }
           LeakCanary.install(this);
       }
    }
    
  • 构建 RefWatcher
    LeakCanary.install() 内部会调用 refWatcher() 方法创建一个 AndroidRefWatcherBuilder,并设置用于接收内存泄漏分析结果的 listener(例如 DisplayLeakService),以及排除 Android SDK 或手机厂商修改引入的已知泄漏对象(excludedRefs)。

  • 注册生命周期回调
    buildAndInstall() 中,根据配置:

    • 注册 Activity 生命周期回调(通过 ActivityRefWatcher),在 Activity 的 onDestroyed() 时调用 refWatcher.watch(activity) 进行监控。
    • 注册 Fragment 生命周期回调(通过 FragmentRefWatcher.Helper),在 Fragment 销毁时同样监控相关对象。

2. 监控对象及弱引用机制

  • 监控目标
    LeakCanary 关注的对象通常是那些生命周期结束后应被回收的对象,如 Activity、Fragment、Service 或其他大对象。

  • 弱引用实现

    • 当调用 RefWatcher.watch(watchedReference, referenceName) 时,LeakCanary 为被观察对象创建一个自定义的弱引用 KeyedWeakReference(继承自 WeakReference),同时生成一个唯一的 key,并将这个 key 添加到内部的“怀疑名单”(retainedKeys)。
    • 此弱引用关联了一个 ReferenceQueue。当垃圾回收器回收目标对象后,该弱引用会被加入队列,从而可以通过轮询队列确认该对象是否真的被回收。
    • 实际上,当一个对象只被弱引用所引用,也就是说该对象变得不可达时,垃圾回收器会在适当的时候将这个对象回收,并在回收前将关联的弱引用加入到相应的引用队列中。这并不意味着弱引用本身被回收后才入队,而是因为它所指向的对象不可达而触发了入队动作,从而可以让程序有机会对该对象做一些清理或后续处理。
  • 检测回收情况

    • 在异步任务中(由 WatchExecutor 执行),首先调用 removeWeaklyReachableReferences() 从队列中移除已经回收对象对应的 key。
    • 如果 retainedKeys 中仍包含目标对象的 key,说明对象没有被回收,即可能存在内存泄漏。

3. 触发垃圾回收与堆转储

  • 主动触发 GC
    如果检测到被观察对象依然存在(即弱引用未被清除),LeakCanary 会主动调用 gcTrigger.runGc() 触发一次垃圾回收,再次清理弱引用。

  • 堆转储(Heap Dump)

    • 如果在再次检测后目标对象依然存在,则认为该对象未能正常释放,LeakCanary 开始执行堆转储,通过调用 heapDumper.dumpHeap() 获取 hprof 文件。
    • 为避免系统干扰,还会显示一个通知,并通过 Toast 等机制确认堆转储时机。

4. 堆文件分析与泄漏确认

  • 堆分析服务
    堆转储完成后,LeakCanary 会将转储文件、目标对象的 key 以及其他监控信息封装成一个 HeapDump 对象,并将其传递给监听器服务(如 DisplayLeakService)。

  • 独立进程分析
    在独立进程中,HeapAnalyzer 会加载 hprof 文件,对整个内存快照进行解析:

    • 查找 KeyedWeakReference 实例,验证目标对象是否真的存在。
    • 分析从 GC Roots 到该对象的引用路径,生成泄漏链(Leak Trace)。
    • 结合分析耗时等信息构建最终的 AnalysisResult
  • 结果展示
    分析结果最终通过 AbstractAnalysisResultService.sendResultToListener() 发送到指定的监听服务,开发者可以在 LeakCanary 提供的 UI 中查看详细的泄漏信息,也可以将结果上报服务器。

5. 总结

LeakCanary 的内存泄漏检测原理可以归纳为以下几点:

  • 注册与监控
    通过在 Application 中初始化,注册 Activity 和 Fragment 生命周期回调,在对象销毁时调用 watch() 监控待回收对象。
  • 弱引用与引用队列
    使用自定义的弱引用(KeyedWeakReference)与 ReferenceQueue 来检测对象是否已被垃圾回收,通过 retainedKeys 记录未回收的对象标识。
  • 主动 GC 与堆转储
    当检测到对象未被回收时,主动触发垃圾回收、转储堆内存数据,并启动独立进程进行详细分析。
  • 泄漏路径分析
    通过解析堆转储文件,生成对象的引用路径图,帮助定位导致内存泄漏的原因。

5. LeakCanary 原理详细解析

5.1 引用类型

在开始之前,我们需要先做一些前置知识的准备,了解四种引用类型。

  • 强引用:

    • 对象只要被强引用,就不会被垃圾回收。
  • 弱引用:

    • 可以通过 get() 方法获得引用的对象,若对象被垃圾回收,则返回 null
    • 在对象被回收前,弱引用会被放入关联的队列中,可用来判断对象是否被回收。
  • 软引用:

    • 类似弱引用,但在内存不足时才会被回收。
  • 虚引用:

    • 不能通过 get() 获得对象,主要用于跟踪对象回收的状态。

image.png

5.2 监控原理

LeakCanary 是一个用于检测内存泄露的工具,由于2.0以上的版本对其内容做了改动,为了方便我们理解原理,于是选取了1.6.3的版本,其内部的基础是:

  • LeakCanary 通过注册 Application 和 Activity 的生命周期回调,在 Activity 和 Fragment 销毁时开始观察其内存状态。 image.png image.png
  • 1.x 版本的 LeakCanary 主要监控 support 包和 API 26 以上的 Fragment。

源码流程分析:

1. 完成LeakCanary的注册:

public class MyApp extends Application {
   @Override
   public void onCreate() {
       super.onCreate();
       // 判断是否在主进程中
       if (LeakCanary.isInAnalyzerProcess(this)) {
           // This process is dedicated to LeakCanary for heap analysis.
           // You should not init your app in this process.
           return;
       }
       // 使用LeakCanary
       LeakCanary.install(this);
   }
}

2. 进入LeakCanary#install(Application application)

install方法内部完成了整个LeakCanary的初始化,RefWatcher 字面意思就是引用观察者,

  • listenerServiceClass() 方法:允许指定一个自定义的 Service 类(继承自 AbstractAnalysisResultService),用于接收内存泄漏分析结果并发送通知。
  • excludedRefs:无法处理的内存泄漏,例如三方SDK的泄漏,AndroidExcludedRefs里边包含了一些AndroidSDK自带的内存泄漏,还有手机厂商修改带来的额外的内存泄漏
  • buildAndInstall() : 创建监察者。
/**
 * Creates a {@link RefWatcher} that works out of the box, and starts watching activity
 * references (on ICS+).
 */
public static @NonNull RefWatcher install(@NonNull Application application) {
  return refWatcher(application).listenerServiceClass(DisplayLeakService.class) // DisplayLeakService 可以自定义,可以把分析结果上送服务器。
      // 无法处理的内存泄漏,例如三方SDK的泄漏,AndroidExcludedRefs里边包含了一些AndroidSDK自带的内存泄漏,还有手机产商修改带来的额外的内存泄漏
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) 
      .buildAndInstall();
}

// 创建一个用于观察引用的对象
public static @NonNull AndroidRefWatcherBuilder refWatcher(@NonNull Context context) {
  return new AndroidRefWatcherBuilder(context);
}

// 手机厂商修改导致的内存泄漏
import static com.squareup.leakcanary.internal.LeakCanaryInternals.HUAWEI;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.LENOVO;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.LG;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.MEIZU;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.MOTOROLA;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.NVIDIA;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.SAMSUNG;
import static com.squareup.leakcanary.internal.LeakCanaryInternals.VIVO;

3. 深入AndroidRefWatcherBuilder#buildAndInstall()

  • 单例限制:首先检查是否已有安装的 RefWatcher,确保只在当前进程中调用一次。

  • 构建 RefWatcher:调用内部 build() 方法构建 RefWatcher 实例。

  • 监控配置

    • 如果不是 DISABLED(即未在 release 模式下禁用检测),则根据配置决定是否启用显示泄漏的 Activity(DisplayLeakActivity)。
    • 根据 watchActivities 和 watchFragments 配置,分别安装 Activity 和 Fragment 的生命周期监控器。
  • 保存实例:将构建好的 RefWatcher 保存到全局变量中,确保后续统一使用。

  • 返回实例:返回构建好的 RefWatcher,后续可以用于定制其他监控需求。

/**
 * 创建一个 RefWatcher 实例,并将该实例保存在 LeakCanary 内部,
 * 以便后续通过 LeakCanary.installedRefWatcher() 访问到该实例。
 * 同时,如果配置了监控 Activity 和 Fragment,则会自动注册生命周期回调,
 * 在 Activity 或 Fragment 销毁时开始监控它们是否会被正确回收。
 *
 * @throws UnsupportedOperationException 如果在同一进程中多次调用此方法(只允许初始化一次)
 */
public @NonNull RefWatcher buildAndInstall() {
  // 检查是否已经初始化过 RefWatcher,如果已经安装则抛出异常,防止重复初始化
  if (LeakCanaryInternals.installedRefWatcher != null) {
    throw new UnsupportedOperationException("buildAndInstall() should only be called once.");
  }
  
  // 调用内部 build() 方法构造 RefWatcher 实例
  RefWatcher refWatcher = build();
  
  // 如果构造的 RefWatcher 不是 DISABLED(即当前不是 release 版的空实现)
  if (refWatcher != DISABLED) {
    // 如果启用了显示泄漏分析 Activity 的功能(默认开启),则异步设置使得
    // 分析结果对应的 Activity 图标可以在应用列表中显示出来
    if (enableDisplayLeakActivity) {
      LeakCanaryInternals.setEnabledAsync(context, DisplayLeakActivity.class, true);
    }
  
    // 如果配置为监控 Activity,则安装 ActivityRefWatcher,
    // 通过注册 Application 的生命周期回调在 Activity 销毁时开始监控
    if (watchActivities) {
      ActivityRefWatcher.install(context, refWatcher);
    }
    // 如果配置为监控 Fragment,则安装 FragmentRefWatcher,
    // 通过注册 Activity 生命周期回调,在 Activity 创建时安装 Fragment 监控器
    if (watchFragments) {
      FragmentRefWatcher.Helper.install(context, refWatcher);
    }
  }
  
  // 将构造好的 RefWatcher 保存在全局变量中,保证在当前进程中唯一
  LeakCanaryInternals.installedRefWatcher = refWatcher;
  
  // 返回 RefWatcher 对象,这个对象后续还可以用于定制监控其他对象(例如大对象、Service等)
  return refWatcher;
}

/**
 * 设置一个自定义的 {@link AbstractAnalysisResultService} 用于接收内存泄漏分析结果。
 * 该方法将覆盖之前对 {@link #heapDumpListener(HeapDump.Listener)} 的设置。
 *
 * @param listenerServiceClass 用于监听堆转储分析结果并发送通知的 Service 类,
 *                             该类必须继承自 AbstractAnalysisResultService。
 * @return 当前 AndroidRefWatcherBuilder 对象,支持链式调用以进一步定制 LeakCanary 配置。
 */
public @NonNull AndroidRefWatcherBuilder listenerServiceClass(
    @NonNull Class<? extends AbstractAnalysisResultService> listenerServiceClass) {
  // 检查传入的 listenerServiceClass 是否为 DisplayLeakService 或其子类,
  // 如果是,则启用显示泄漏分析 Activity(即使分析结果的图标能够在 Launcher 中显示)
  enableDisplayLeakActivity = DisplayLeakService.class.isAssignableFrom(listenerServiceClass);
  
  // 创建一个新的 ServiceHeapDumpListener 实例,该 listener 用于在堆转储分析完成后接收结果,
  // 并调用 heapDumpListener() 将其设置到当前构建器中
  return heapDumpListener(new ServiceHeapDumpListener(context, listenerServiceClass));
}
  • ActivityRefWatcher: 在 Activity 被销毁后(调用 onDestroyed 后),自动将该 Activity 对象交给 RefWatcher 进行内存泄漏检测。具体作用如下:
  1. 注册生命周期回调

    • 通过调用 install() 方法,将一个包含自定义生命周期回调(这里继承自 ActivityLifecycleCallbacksAdapter)的对象注册到 Application 上。
    • 在这个回调中,重点关注 onActivityDestroyed(Activity activity) 方法,当某个 Activity 销毁时,自动调用 refWatcher.watch(activity)
  2. 自动检测内存泄漏

    • 当 Activity 被销毁后,RefWatcher 会对该 Activity 进行监控,判断其是否能被正常回收,避免出现内存泄漏情况。
  3. 便捷的安装和卸载

    • 提供 watchActivities()stopWatchingActivities() 方法,方便在运行时动态开启或停止对 Activity 生命周期的监控,确保不会重复注册回调。
public final class ActivityRefWatcher {

  public static void installOnIcsPlus(@NonNull Application application,
      @NonNull RefWatcher refWatcher) {
    install(application, refWatcher);
  }
  
  // 下边的代码想做的事情就是对已经调用了onDestroyed的Activity进行监视
  public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
	  // application通过传入Activity的lifecycleCallbacks对象,就能够实现对所有Activity生命周期方法调用的监控,
    // 并且方法中还能够获取到Activity的对象,但是没有给Service添加这样的方法。
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

  private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
	      // 当某一个Activity的onDestroyed被调用的时候,就会触发这里。
        @Override public void onActivityDestroyed(Activity activity) {
	        // 对已经调用了onDestroyed进行监视。
          refWatcher.watch(activity);
        }
      };

  private final Application application;
  private final RefWatcher refWatcher;

  private ActivityRefWatcher(Application application, RefWatcher refWatcher) {
    this.application = application;
    this.refWatcher = refWatcher;
  }

  public void watchActivities() {
    // Make sure you don't get installed twice.
    stopWatchingActivities();
    application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
  }

  public void stopWatchingActivities() {
    application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
  }
}
  • FragmentRefWatcher :LeakCanary 利用这部分代码自动注册 Fragment 生命周期监听器。通过在 Activity 创建时,安装针对 Fragment 的监控器,可以在 Fragment 销毁时(或其 View 销毁时)调用 refWatcher.watch(),从而检测这些对象是否在合适时机被垃圾回收。
  1. 针对不同类型的 Fragment:

    • 对于 Android O 及以上的原生 Fragment,使用 AndroidOFragmentRefWatcher
    • 对于 Support 包中的 Fragment,尝试通过反射加载并使用 SupportFragmentRefWatcher(前提是项目中包含了对应依赖,不过现在的项目应该很少存在Support包了)。
public interface FragmentRefWatcher {

  /**
   * 注册当前 Activity 中 Fragment 的生命周期回调,
   * 以便在 Fragment 的关键生命周期事件发生时进行内存泄漏检测。
   *
   * @param activity 当前要监控的 Activity
   */
  void watchFragments(Activity activity);

  final class Helper {

    // SupportFragmentRefWatcher 类名,用于检测是否引入了 LeakCanary 的支持包
    private static final String SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME =
        "com.squareup.leakcanary.internal.SupportFragmentRefWatcher";

    /**
     * 安装 FragmentRefWatcher 的辅助类,
     * 根据当前环境和依赖情况,动态构造适用于不同 Fragment 类型的监控器,
     * 并注册 Activity 生命周期回调,在 Activity 创建时为其内部的 Fragment 注册监控。
     *
     * @param context     应用上下文
     * @param refWatcher  用于监控对象是否被回收的 RefWatcher 实例
     */
    public static void install(Context context, RefWatcher refWatcher) {
      // 用于存储构造出的 Fragment 监控器
      List<FragmentRefWatcher> fragmentRefWatchers = new ArrayList<>();

      // (1)原生 Fragment:Android 26(Oreo)及以上的系统支持对原生 Fragment 进行监控
      //     注意:在 Android 26 之前,自带的 Fragment 不支持内存泄漏检测,
      //           同时 AndroidX 包下的 Fragment 同样不支持 LeakCanary 的自动监控。
      if (SDK_INT >= O) {
        fragmentRefWatchers.add(new AndroidOFragmentRefWatcher(refWatcher));
      }

      try {
        // (2)Support Fragment:尝试通过反射加载 SupportFragmentRefWatcher 类,
        //     前提是项目引入了 com.squareup.leakcanary:leakcanary-support-fragment 依赖。
        Class<?> fragmentRefWatcherClass = Class.forName(SUPPORT_FRAGMENT_REF_WATCHER_CLASS_NAME);
        Constructor<?> constructor =
            fragmentRefWatcherClass.getDeclaredConstructor(RefWatcher.class);
        // 创建针对 Support 包下 Fragment 的监控器实例
        FragmentRefWatcher supportFragmentRefWatcher =
            (FragmentRefWatcher) constructor.newInstance(refWatcher);
        fragmentRefWatchers.add(supportFragmentRefWatcher);
      } catch (Exception ignored) {
        // 如果反射失败(例如依赖不存在),则忽略异常
      }

      // 如果没有任何 Fragment 监控器可用,则直接返回,不做安装
      if (fragmentRefWatchers.size() == 0) {
        return;
      }

      // 构造 Helper 实例,将创建好的 FragmentRefWatcher 列表传入
      Helper helper = new Helper(fragmentRefWatchers);

      // 获取全局 Application 实例,用于注册 Activity 生命周期回调
      Application application = (Application) context.getApplicationContext();
      // 注册一个 Activity 生命周期回调监听器,
      // 在 Activity 创建时,为该 Activity 内所有 Fragment 注册监控
      application.registerActivityLifecycleCallbacks(helper.activityLifecycleCallbacks);
    }

    // 定义 Activity 生命周期回调,用于在 Activity 创建时为其中的 Fragment 安装监控器
    private final Application.ActivityLifecycleCallbacks activityLifecycleCallbacks =
        new ActivityLifecycleCallbacksAdapter() {
          @Override 
          public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            // 当 Activity 创建时,遍历所有已构造的 Fragment 监控器,
            // 并调用它们的 watchFragments 方法为当前 Activity 注册 Fragment 生命周期监听器
            for (FragmentRefWatcher watcher : fragmentRefWatchers) {
              watcher.watchFragments(activity);
            }
          }
        };

    // 存储所有构造的 FragmentRefWatcher 实例
    private final List<FragmentRefWatcher> fragmentRefWatchers;

    private Helper(List<FragmentRefWatcher> fragmentRefWatchers) {
      this.fragmentRefWatchers = fragmentRefWatchers;
    }
  }
}


/**
 * 针对 Android O 及以上版本原生 Fragment 的内存泄漏监控实现。
 * 监控内容包括:Fragment 自身和它的 View 在销毁时是否被正确回收。
 */
class AndroidOFragmentRefWatcher implements FragmentRefWatcher {

  private final RefWatcher refWatcher;

  AndroidOFragmentRefWatcher(RefWatcher refWatcher) {
    this.refWatcher = refWatcher;
  }

  // 定义 Fragment 生命周期回调,关注两个关键事件:
  // 1. Fragment 的 View 被销毁(onFragmentViewDestroyed)
  // 2. Fragment 实例被销毁(onFragmentDestroyed)
  private final FragmentManager.FragmentLifecycleCallbacks fragmentLifecycleCallbacks =
      new FragmentManager.FragmentLifecycleCallbacks() {
        @Override 
        public void onFragmentViewDestroyed(FragmentManager fm, Fragment fragment) {
          // 获取当前 Fragment 的 View 对象
          View view = fragment.getView();
          if (view != null) {
            // 当 Fragment 的 View 销毁时,调用 RefWatcher 监控该 View,
            // 以便检测该 View 是否最终能被垃圾回收,防止内存泄漏
            refWatcher.watch(view);
          }
        }
		
        @Override
        public void onFragmentDestroyed(FragmentManager fm, Fragment fragment) {
          // 当 Fragment 本身被销毁时,同样调用 RefWatcher 监控该 Fragment 对象
          refWatcher.watch(fragment);
        }
      };

  /**
   * 注册当前 Activity 中原生 Fragment 的生命周期回调,
   * 这样当 Activity 中的 Fragment 的关键生命周期事件发生时(例如销毁),
   * 就会自动调用相应回调进行内存泄漏检测。
   *
   * @param activity 当前需要监控 Fragment 的 Activity
   */
  @Override 
  public void watchFragments(Activity activity) {
    // 获取当前 Activity 的 FragmentManager
    FragmentManager fragmentManager = activity.getFragmentManager();
    // 注册 FragmentLifecycleCallbacks 回调,第二个参数 true 表示递归注册(监控嵌套的 Fragment)
    fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true);
  }
}

4. RefWatcher内存泄漏检测核心类的分析

(1)内存泄漏的监察与捕捉:

关键点说明:

  • ReferenceQueue 的作用
    当被监控的对象(通过 KeyedWeakReference 包装)仅由弱引用持有时,一旦对象被垃圾回收,该弱引用会自动入队,从而通过 removeWeaklyReachableReferences() 方法将其 key 从怀疑名单中移除。

  • retainedKeys 集合
    用于保存所有待检测的对象的唯一标识(key)。如果某个 key 长时间未被移除,则表示对应对象没有被回收,可能存在内存泄漏。

  • 日志的分析与导出都在这个服务中处理,该服务运行在独立进程中:

<service
    android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
    android:enabled="false"
    android:process=":leakcanary" />
  • ensureGone 方法
    核心检测逻辑:在一定时间内检查对象是否已被回收;如果未被回收,主动触发 GC,最终导出堆转储文件以进一步分析泄漏原因。
public final class RefWatcher {

  // 当 RefWatcher 被禁用时,使用一个空实现
  public static final RefWatcher DISABLED = new RefWatcherBuilder<>().build();
  
  // 任务执行器,用于异步执行检测任务
  private final WatchExecutor watchExecutor;
  // 用于检测调试器是否已连接,调试器可能会影响 GC 行为
  private final DebuggerControl debuggerControl;
  // 触发垃圾回收的工具
  private final GcTrigger gcTrigger;
  // 用于导出堆信息(hprof文件)的工具
  private final HeapDumper heapDumper;
  // 用于接收堆转储后分析结果的监听器
  private final HeapDump.Listener heapdumpListener;
  // 用于构建 HeapDump 对象(封装堆转储相关信息)的构造器
  private final HeapDump.Builder heapDumpBuilder;
  // 怀疑对象集合,用来存储待检测(可能泄漏)的对象的唯一标识(key)
  private final Set<String> retainedKeys;
  // 弱引用关联的队列,当被观察对象被回收后,其对应的弱引用会自动加入此队列
  private final ReferenceQueue<Object> queue;

  // 构造函数,初始化所有必需的组件和内部集合
  RefWatcher(WatchExecutor watchExecutor, DebuggerControl debuggerControl, GcTrigger gcTrigger,
      HeapDumper heapDumper, HeapDump.Listener heapdumpListener, HeapDump.Builder heapDumpBuilder) {
    this.watchExecutor = checkNotNull(watchExecutor, "watchExecutor");
    this.debuggerControl = checkNotNull(debuggerControl, "debuggerControl");
    this.gcTrigger = checkNotNull(gcTrigger, "gcTrigger");
    this.heapDumper = checkNotNull(heapDumper, "heapDumper");
    this.heapdumpListener = checkNotNull(heapdumpListener, "heapdumpListener");
    this.heapDumpBuilder = heapDumpBuilder;
    retainedKeys = new CopyOnWriteArraySet<>();
    // 创建一个 ReferenceQueue,用于跟踪被观察对象的弱引用是否被加入队列(即对象是否被回收)
    queue = new ReferenceQueue<>();
  }

  /**
   * 简化接口:使用空字符串作为引用名称
   */
  public void watch(Object watchedReference) {
    watch(watchedReference, "");
  }

  /**
   * 监控指定对象,观察它是否能够被 GC 回收。该方法会在Activity的OnDestroy 或者是Fragment的OnDestroyView 或者是Fragment的OnDestroy 执行后开始执行,
   * 该方法为非阻塞,实际检查任务由 watchExecutor 异步执行。
   *
   * @param referenceName 用于标识被观察对象的逻辑名称
   */
  public void watch(Object watchedReference, String referenceName) {
    // 如果当前 RefWatcher 已经被禁用,则直接返回,release包就会返回。
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    // 记录监控开始时间(单位:纳秒),用于计算观察时长
    final long watchStartNanoTime = System.nanoTime();
    // 为被观察对象生成一个唯一标识 key
    String key = UUID.randomUUID().toString();
    // 将该 key 加入待观察的集合中,表示该对象应该在未来被回收
    retainedKeys.add(key);
    // 创建一个 KeyedWeakReference,将被观察对象包装为弱引用,并关联到 ReferenceQueue
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);

    // 异步执行检测任务,确保对象能够被垃圾回收
    ensureGoneAsync(watchStartNanoTime, reference);
  }

  /**
   * 清除所有已监控的对象,停止监控
   */
  public void clearWatchedReferences() {
    retainedKeys.clear();
  }

  // 检查所有已监控对象是否已被回收后,判断待观察集合是否为空
  boolean isEmpty() {
    removeWeaklyReachableReferences();
    return retainedKeys.isEmpty();
  }

  HeapDump.Builder getHeapDumpBuilder() {
    return heapDumpBuilder;
  }

  Set<String> getRetainedKeys() {
    return new HashSet<>(retainedKeys);
  }

  // 异步检测:通过 watchExecutor 异步执行确保对象已被回收的检测任务
  private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        // 在子线程中检测对象是否已被回收
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }
  
  // watchExecutor的具体实现:

  // 以下是 watchExecutor 的执行逻辑,根据当前线程是否为主线程选择不同的等待方式
  @Override public void execute(@NonNull Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      // 如果在主线程,则等待主线程空闲后执行
      waitForIdle(retryable, 0);
    } else {
      // 否则直接在后台线程执行
      postWaitForIdle(retryable, 0);
    }
  }

  /**
   * 核心检测逻辑:确保被观察对象已经被垃圾回收
   * 1. 记录 GC 开始时间,并计算观察对象的存活时长
   * 2. 轮询 ReferenceQueue 移除已回收的引用(更新 retainedKeys)
   * 3. 如果对象仍未被回收,则主动触发 GC,再次检测
   * 4. 如果对象仍未被回收,则导出堆信息,并启动堆分析
   */
  @SuppressWarnings("ReferenceEquality") // 显式地检查引用相等性
  Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    // 记录触发垃圾回收检测的时间点
    long gcStartNanoTime = System.nanoTime();
    // 计算从开始监控到触发 GC 的时长(毫秒)
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    // 移除已被回收的对象对应的 key(从 ReferenceQueue 中轮询)
    removeWeaklyReachableReferences();

    // 如果调试器附加在进程上,可能会导致虚假泄漏,直接返回重试状态
    if (debuggerControl.isDebuggerAttached()) {
      return RETRY;
    }
    // 如果该对象已被回收(对应 key 已被移除),则返回 DONE 表示检测结束
    if (gone(reference)) {
      return DONE;
    }
    // 主动触发一次垃圾回收
    gcTrigger.runGc();
    // 再次清除 ReferenceQueue 中的已回收引用
    removeWeaklyReachableReferences();
    // 如果对象依然存在(未被回收),说明可能存在内存泄漏
    if (!gone(reference)) {
      long startDumpHeap = System.nanoTime();
      // 计算 GC 触发后至开始堆转储的耗时(毫秒)
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
      // 导出当前堆的快照到文件
      File heapDumpFile = heapDumper.dumpHeap();
      // 如果无法导出堆文件(例如权限问题),返回 RETRY 状态
      if (heapDumpFile == RETRY_LATER) {
        return RETRY;
      }
      // 计算堆转储耗时(毫秒)
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      // 构建一个 HeapDump 对象,包含堆文件、观察对象的 key、名称以及各个阶段的耗时信息
      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();
      // 通过监听器启动堆分析服务,进一步分析内存泄漏
      heapdumpListener.analyze(heapDump);

      // 分析服务内部的处理(例如:HeapAnalyzerService.runAnalysis)
      @Override public void analyze(@NonNull HeapDump heapDump) {
        checkNotNull(heapDump, "heapDump");
        HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass);
      }
    }
    // 对象已经被回收或者完成检测,返回 DONE
    return DONE;
  }

  // 判断目标对象是否已被回收:如果 retainedKeys 中不包含该对象的 key,则认为已被回收
  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.key);
  }
  
	// 不断的从队列中取出对象,引用的观察对象的弱引用,回收之前,queue就是作为弱引用的构造传入的队列,在垃圾回收的时候,
	// 如果发现某个对象只被弱引用引用,那么这个对象就会被回收,并且在回收之前,会把这个弱引用添加到它构造方法中弱引用关联的队列中。
	// 在watch方法中,为观察的对象创建了一个弱引用,如果这个观察的目标被垃圾回收了,那么这里的弱引用就会被放入到队列当中。
	// 如果能够在这个里边取出弱引用,那么就说明观察的对象就被回收了,所以就从怀疑名单中给移除掉。如果经过了这一系列步骤。retainedKeys还存在着key,说明还没有垃圾回收掉。
  private void removeWeaklyReachableReferences() {
    // 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.
    KeyedWeakReference ref;

    while ((ref = (KeyedWeakReference) queue.poll()) != null) {
	  // 把观察对象的ID从retainedKeys移除掉
      retainedKeys.remove(ref.key);
    }
  }
}
(2)发现内存泄漏,就会打印内存堆栈:
public File dumpHeap() {
  // 1. 创建一个用于存放堆转储文件的文件对象
  File heapDumpFile = leakDirectoryProvider.newHeapDumpFile();
  // 如果创建文件失败(返回 RETRY_LATER,代表无法生成文件),则直接返回
  if (heapDumpFile == RETRY_LATER) {
    return RETRY_LATER;
  }

  // 2. 创建一个 FutureResult 对象,用于等待 Toast 显示完成
  FutureResult<Toast> waitingForToast = new FutureResult<>();
  // 显示一个 Toast(提示用户正在进行堆转储),并将结果通过 waitingForToast 传递回来
  showToast(waitingForToast);
  // 等待最多 5 秒钟,等待 Toast 显示完成
  if (!waitingForToast.wait(5, SECONDS)) {
    CanaryLog.d("Did not dump heap, too much time waiting for Toast.");
    // 如果等待超时,则返回 RETRY_LATER,放弃本次堆转储
    return RETRY_LATER;
  }

  // 3. 构建一个通知,用于提示用户正在进行堆转储操作
  Notification.Builder builder = new Notification.Builder(context)
      .setContentTitle(context.getString(R.string.leak_canary_notification_dumping));
  // 通过内部工具方法构建通知对象
  Notification notification = LeakCanaryInternals.buildNotification(context, builder);
  // 获取系统的通知管理器
  NotificationManager notificationManager =
      (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
  // 生成一个基于系统时间的通知 ID
  int notificationId = (int) SystemClock.uptimeMillis();
  // 发布通知,提示用户堆转储正在进行
  notificationManager.notify(notificationId, notification);

  // 4. 从 waitingForToast 中获取之前显示的 Toast 对象
  Toast toast = waitingForToast.get();
  try {
    // 5. 调用 Debug.dumpHprofData() 方法进行堆转储,
    //    参数为堆转储文件的绝对路径,Framework 提供该能力,将堆数据写入文件
    Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
    // 6. 堆转储成功后,取消之前显示的 Toast
    cancelToast(toast);
    // 7. 同时取消通知
    notificationManager.cancel(notificationId);
    // 返回生成的堆转储文件
    return heapDumpFile;
  } catch (Exception e) {
    // 如果在堆转储过程中出现异常,则记录日志
    CanaryLog.d(e, "Could not dump heap");
    // 并返回 RETRY_LATER,表示本次堆转储失败,需要重试
    return RETRY_LATER;
  }
}
(3)HeapAnalyzerService中的一个子线程用来处理分析结果
@Override 
protected void onHandleIntentInForeground(@Nullable Intent intent) {
  // 如果 intent 为 null,则记录日志并忽略此次调用
  if (intent == null) {
    CanaryLog.d("HeapAnalyzerService received a null intent, ignoring.");
    return;
  }
  // 从 intent 中获取用于接收分析结果的 listener 的类名
  String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA);
  // 从 intent 中获取 HeapDump 对象,HeapDump 包含堆转储文件、排除引用配置、计算保留内存大小的标志等
  HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA);

  // 创建一个 HeapAnalyzer 实例,用于分析堆转储文件
  // 参数包括:排除的引用列表、当前上下文(this),以及堆转储时使用的 ReachabilityInspector 类数组
  HeapAnalyzer heapAnalyzer =
      new HeapAnalyzer(heapDump.excludedRefs, this, heapDump.reachabilityInspectorClasses);
  // 开始堆转储分析,检查泄漏情况,返回一个 AnalysisResult 对象,
  // 其中包含泄漏路径、泄漏原因等信息
  AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey,
      heapDump.computeRetainedHeapSize);
  // 将分析结果发送给指定的 listener 服务,最终展示给用户或者上报服务器,这个listener就是最开始我们注册的时候:refWatcher(application).listenerServiceClass(DisplayLeakService.class)

  AbstractAnalysisResultService.sendResultToListener(this, listenerClassName, heapDump, result);
}

  // 必须使用弱引用而不能使用虚引用,因为虚引用无法获取到引用的对象。但是下边的流程中我们是需要这个对象的。
  // 该方法用于在堆转储快照中查找与指定 key 对应的泄漏对象
// 这里查找的是通过 KeyedWeakReference 包装的对象的弱引用
private Instance findLeakingReference(String key, Snapshot snapshot) {
  // 在快照中查找 KeyedWeakReference 类(该类用于包装被监控对象的弱引用)
  ClassObj refClass = snapshot.findClass(KeyedWeakReference.class.getName());
  if (refClass == null) {
    // 如果堆快照中不存在该类,说明无法获取相关引用信息,抛出异常
    throw new IllegalStateException(
        "Could not find the " + KeyedWeakReference.class.getName() + " class in the heap dump.");
  }
  // 用于存储遍历过程中找到的所有 key 值,便于出错时打印调试信息
  List<String> keysFound = new ArrayList<>();
  // 遍历堆快照中所有 KeyedWeakReference 的实例
  for (Instance instance : refClass.getInstancesList()) {
    // 获取当前实例所有字段的值列表
    List<ClassInstance.FieldValue> values = classInstanceValues(instance);
    // 从字段列表中取出名称为 "key" 的字段的值
    Object keyFieldValue = fieldValue(values, "key");
    if (keyFieldValue == null) {
      // 如果当前实例的 key 字段为空,则记录 null 并继续下一个实例
      keysFound.add(null);
      continue;
    }
    // 将 key 字段值转换为字符串
    String keyCandidate = asString(keyFieldValue);
    // 如果转换后的 key 与传入的目标 key 相等,则说明找到了对应的弱引用
    // 返回该实例中保存被监控对象的字段 "referent" 的值,即真正可能泄漏的对象
    if (keyCandidate.equals(key)) {
      return fieldValue(values, "referent");
    }
    // 将遍历到的 keyCandidate 添加到 keysFound 列表中,便于调试输出
    keysFound.add(keyCandidate);
  }
  // 如果遍历完所有实例都没有找到匹配的 key,则抛出异常,并输出所有遍历到的 key 信息
  throw new IllegalStateException(
      "Could not find weak reference with key " + key + " in " + keysFound);
}

// 分析堆转储文件,检查是否存在内存泄漏,并返回一个 AnalysisResult 对象
// 参数:heapDumpFile - 堆转储文件;referenceKey - 被监控对象的唯一标识;computeRetainedSize - 是否计算保留内存大小
public @NonNull AnalysisResult checkForLeak(@NonNull File heapDumpFile,
      @NonNull String referenceKey,
      boolean computeRetainedSize) {
  // 记录分析开始的纳秒级时间戳
  long analysisStartNanoTime = System.nanoTime();

  // 如果堆转储文件不存在,则构造异常返回失败结果
  if (!heapDumpFile.exists()) {
    Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile);
    return failure(exception, since(analysisStartNanoTime));
  }

  try {
    // 更新分析进度:正在读取堆转储文件
    listener.onProgressUpdate(READING_HEAP_DUMP_FILE);
    // 将堆转储文件映射为内存缓冲区,以便后续解析(采用内存映射文件方式)
    HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
    // 创建 HprofParser 用于解析堆转储数据
    HprofParser parser = new HprofParser(buffer);
    // 更新进度:正在解析堆转储文件
    listener.onProgressUpdate(PARSING_HEAP_DUMP);
    // 解析堆转储数据,生成 Snapshot 对象,代表整个堆的快照
    Snapshot snapshot = parser.parse();
    // 更新进度:正在去重 GC Roots(垃圾回收根节点)
    listener.onProgressUpdate(DEDUPLICATING_GC_ROOTS);
    // 对 GC Roots 进行去重,简化后续分析过程
    deduplicateGcRoots(snapshot);
    // 更新进度:正在查找泄漏的引用
    listener.onProgressUpdate(FINDING_LEAKING_REF);
    // 在堆快照中查找与 referenceKey 对应的泄漏对象
    Instance leakingRef = findLeakingReference(referenceKey, snapshot);

    // 如果返回的泄漏对象为 null,说明在关键时间段内目标对象已经被回收
    // 这里视为误报(false alarm),返回 noLeak 结果,附带泄漏对象的类名和分析耗时
    if (leakingRef == null) {
      String className = leakingRef.getClassObj().getClassName();
      return noLeak(className, since(analysisStartNanoTime));
    }
    // 如果找到了泄漏对象,则进一步查找泄漏引用路径(Leak Trace),
    // 并返回包含详细泄漏信息的 AnalysisResult 对象
    return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, computeRetainedSize);
  } catch (Throwable e) {
    // 如果在整个过程中出现异常,则返回失败的分析结果,并附上耗时信息
    return failure(e, since(analysisStartNanoTime));
  }
}


6. 一些细节的补充与2.0后版本的调整

6.1 正确触发 GC

不重写finalize方法,怎么知道对象是否被回收?三种弱引用都有一个同样的功能,当GC开始时候,如果垃圾回收处理器发现,这个弱引用所引用的对象没有被其他强引用引用到,那么在垃圾回收之前,就会把这个弱引用本身添加到队列当中。注意,这里是引用的对象,而不是弱引用引用的对象。这样就能判断这个对象能否被GC了,finalize方法并不可靠,它在GC调用的时候不一定会被调用。官方JDK的实现,也是通过这种方式做的。 image.png

  • 可以通过调用 dumpHprofData 获取 hprof 文件来进行堆内存分析。

image.png

6.2 LeakCanary2.0的初始化流程简单说明:

image.png

image.png

6.3 LeakCanary2.0与1.0的一些差别:

  • 2.0支持对View以及Service的内存泄漏的监听了,RootViewWatcher 通过监听根视图的添加和其 attach/detach 状态,这里还涉及到另一个库 Curtains,这个库的作用就是可以监听 window 的生命周期。在视图从窗口移除时启动延迟检查,检测它是否能被回收,从而实现对根视图内存泄漏的监控,而Service则是通过反射获取 ActivityThread 中的 Handler(mH)和它的 mCallback 字段,并用自定义的 Callback 替换来实现的:
private fun swapActivityThreadHandlerCallback(swap: (Handler.Callback?) -> Handler.Callback?) {
  val mHField =
    activityThreadClass.getDeclaredField("mH").apply { isAccessible = true }
  val mH = mHField[activityThreadInstance] as Handler

  val mCallbackField =
    Handler::class.java.getDeclaredField("mCallback").apply { isAccessible = true }
  val mCallback = mCallbackField[mH] as Handler.Callback?
  mCallbackField[mH] = swap(mCallback)
}

fun appDefaultWatchers(
  application: Application,
  reachabilityWatcher: ReachabilityWatcher = objectWatcher
): List<InstallableWatcher> {
  return listOf(
    ActivityWatcher(application, reachabilityWatcher),
    FragmentAndViewModelWatcher(application, reachabilityWatcher),
    RootViewWatcher(reachabilityWatcher),
    ServiceWatcher(reachabilityWatcher)
  )
}
  • 导入包发生变化,2.0有两种导入方式:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' // 主进程分析
debugImplementation 'com.squareup.leakcanary:leakcanary-android-process:2.14' // 独立进程分析内存
泄漏

不同的配置方式会让LeakCanary运行在不同的进程中,下图就是独立进程: image.png

  • 初始化时机: : Android 中 ContentProvider#onCreate() 会在 Application#onCreate() 之前执行, LeakCanary 2.0 利用了这一特性,实现了自动初始化,无需手动调用初始化代码。 image.png

  • 堆文件分析方式变化:

    • 1.x 版本:: 内部使用第三方库 haha 进行堆文件分析;
    • 2.x 版本::改用第三方库 shark,内存占用大幅减少,分析速度大幅提升。
  • 分析流程内部逻辑的变化:

    • 延迟检查(与 1.x IdleHandler 的区别): 1.x 版本使用主线程空闲(IdleHandler)来触发检查;2.x 版本改为在主线程中延迟执行 5 秒(或其他指定时间),给系统足够的时间进行垃圾回收。
    • AppWatcher / ObjectWatcher 的引入1.x 版本: 主要通过 RefWatcher 对象来监控 Activity、Fragment 等,手动在 Activity/Fragment 销毁时调用 watch()2.x 版本: 引入了 AppWatcherObjectWatcher,更集中地管理所有被观察对象;对外只需调用 expectWeaklyReachable()watch(),内部会自动安排延迟检测并在确认对象未被回收后触发后续分析。
@Synchronized
override fun expectWeaklyReachable(
  watchedObject: Any,
  description: String
) {
  // 如果监控功能未启用,则直接返回
  if (!isEnabled()) {
    return
  }
  // 先尝试移除已经被回收的对象,确保 watchedObjects 中只保留仍然存活的对象引用
  removeWeaklyReachableObjects()

  // 为要监控的对象生成一个唯一的 key
  val key = UUID.randomUUID().toString()
  // 记录当前时间(毫秒),用于后续判断对象存活时长
  val watchUptimeMillis = clock.uptimeMillis()

  // 创建一个 KeyedWeakReference,用弱引用的方式关联被监控对象
  // queue: ReferenceQueue,用于在对象被回收时将弱引用加入队列
  val reference = KeyedWeakReference(
    referent = watchedObject,
    key = key,
    description = description,
    watchUptimeMillis = watchUptimeMillis,
    queue = queue
  )

  // 打印调试日志,说明正在监控哪个对象(包括对象类型、描述信息以及生成的 key)
  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 用于标识该对象
  watchedObjects[key] = reference

  // 与 1.x 不同:1.x 使用主线程的 IdleHandler 来等待系统空闲后检查;
  // 2.x 改用一个延迟任务(通常延迟 5 秒),给对象足够时间变为不可达并被 GC
  checkRetainedExecutor.execute {
    moveToRetained(key)
  }
}

@Synchronized
private fun moveToRetained(key: String) {
  // 再次尝试移除已经被 GC 回收的弱引用对象
  removeWeaklyReachableObjects()

  // 从 watchedObjects 中获取对应 key 的引用
  val retainedRef = watchedObjects[key]
  if (retainedRef != null) {
    // 如果还能找到该引用,说明对象仍然存活(尚未被 GC 回收)
    // 记录对象被确认为“保留”时的时间
    retainedRef.retainedUptimeMillis = clock.uptimeMillis()

    // 通知所有 OnObjectRetainedListener 监听器,表示有对象真正被保留了
    // 这通常意味着可能存在内存泄漏,需要后续进行进一步分析
    onObjectRetainedListeners.forEach { it.onObjectRetained() }
  }
}

// 在初始化(例如 AppWatcher.objectWatcher.addOnObjectRetainedListener(this))时
// 会将当前对象注册到 onObjectRetainedListeners 中,
// 当 moveToRetained 确认对象尚未被回收时,就会回调相应的监听器。

学后测验

一、单选题(10 题)

  1. JVM 的 GC Roots 不包含下列哪一项?
    A. 线程栈上的局部变量 
    B. “GC Roots” 这一名称本身 
    C. JNI 持有的全局引用 
    D. 存活的线程对象
    【答案】B
    【解析】B 只是概念称呼,不是实际对象,其余三项都是真正的根。
  2. LeakCanary 1.x 解析 hprof 文件采用的第三方库是?
    A. shark B. haha C. MAT D. perfetto
    【答案】B
    【解析】1.x 依赖 haha;2.x 改用 shark。
  3. 下列引用类型中,对象最容易立即被回收的是哪一种?
    A. 强引用 B. 软引用 C. 弱引用 D. 虚引用
    【答案】D
    【解析】只剩虚引用时对象已无法通过 get() 访问,JVM 会尽快回收。
  4. RefWatcher 使用 ReferenceQueue 的主要目的是什么?
    A. 主动触发 GC 
    B. 判断弱引用是否已入队 
    C. 存储堆转储文件 
    D. 生成泄漏路径图
    【答案】B
    【解析】弱引用所指对象被回收前,弱引用会加入队列,RefWatcher 轮询即可识别。
  5. LeakCanary 2.x 比 1.x 新增的默认监控能力是?
    A. Activity B. Fragment C. View & Service D. BroadcastReceiver
    【答案】C
    【解析】2.x 通过 RootViewWatcherServiceWatcher 支持 View、Service 泄漏检测。
  6. 当调用 leakcanary-android-process 依赖时,LeakCanary 的堆分析运行在哪个进程?
    A. 主进程 B. 系统 UI 进程 C. 独立的 “:leakcanary” 进程 D. 任意后台线程
    【答案】C
    【解析】Manifest 中 <service … android:process=":leakcanary" />
  7. KeyedWeakReference 中 “key” 字段的作用是?
    A. 存储对象的 hashCode 
    B. 唯一标识被监控对象 
    C. 保存对象大小 
    D. 记录对象 Class 名
    【答案】B
    【解析】它用 UUID 唯一地把弱引用和待检测对象对应起来。
  8. 在 Android O 及以上监控原生 Fragment,LeakCanary 通过注册哪种回调?
    A. ActivityLifecycleCallbacks
    B. ComponentCallbacks2
    C. ApplicationExitInfo
    D. FragmentManager.FragmentLifecycleCallbacks
    【答案】D
    【解析】见 AndroidOFragmentRefWatcher 实现。
  9. 下面关于 GcTrigger 的描述正确的是?
    A. 直接调用 System.gc() 后同步等待 GC 完成 
    B. 通过多次分配大对象强迫 GC 
    C. 触发一次 GC 并 Thread.sleep(100) 给系统时间 
    D. 与调试器连线时依然强制执行 GC
    【答案】C
    【解析】LeakCanary 默认实现先 System.gc(),再休眠一小段时间。
  10. LeakCanary 2.x 中,如果要停止所有对象监控,应当:
    A. 调用 RefWatcher.clearWatchedReferences()
    B. 设置 AppWatcher.config.enabled = false
    C. 在 ProGuard 中删除 LeakCanary 代码 
    D. 取消注册 Activity 回调
    【答案】B
    【解析】2.x 提供 AppWatcher.Config.enabled 总开关。

二、多选题(5 题)

  1. 下列哪些对象一定属于 GC Roots?
    A. 已加载类的 Class 对象 
    B. 正在运行的线程实例 
    C. 静态字段引用 
    D. SoftReference 指向的对象 
    E. JNI 全局引用
    【答案】A B C E
    【解析】SoftReference 本身可被 GC;其 referent 不一定是根。
  2. LeakCanary 在一次完整检测流程中包含的阶段有(按时序):
    A. 创建 KeyedWeakReference
    B. 手动 dump hprof 
    C. 主动触发 GC 
    D. 堆文件分析 
    E. 发送通知展示结果
    【答案】A C D E
    【解析】hprof 由 LeakCanary 自动 dump,而非“手动”。
  3. 关于 LeakCanary 1.x 与 2.x 的差异,以下说法正确的是:
    A. 2.x 自动通过 ContentProvider 初始化
    B. 2.x 用 shark 替代 haha
    C. 2.x 仍依赖 IdleHandler 触发检查
    D. 2.x 新增 Service 泄漏监控
    E. 2.x 不再需要 install() 手动调用
    【答案】A B D E
    【解析】C 错,2.x 用延迟任务而非 IdleHandler。
  4. 弱引用入队后,RefWatcher 会执行哪些操作?
    A. 从 retainedKeys 移除 key 
    B. 立即 dump hprof 
    C. 取消相关 Toast 
    D. 判断是否仍需分析 
    E. 触发 OnObjectRetainedListener
    【答案】A D
    【解析】真正 dump 只在对象未回收时;Toast 在 dump 成功后才取消;Listener 在对象未回收的情况下触发。
  5. 下列哪些方式可以减少 LeakCanary 误报?
    A. 添加 excludedRefs 排除已知系统泄漏 
    B. 在调试器连接状态下禁用分析 
    C. 在 watch() 前手动 System.gc()
    D. 为 Adapter 复用 ViewHolder 减少持有引用 
    E. 配置 AppWatcher.config.enabled = false
    【答案】A B
    【解析】C 多余;D 属于业务内存优化而非误报处理;E 直接关闭检测。

三、判断题(5 题)

题号断言答案
1软引用对象只有在系统内存不足时才一定被回收。
2LeakCanary 2.x 必须手动调用 LeakCanary.install() 才能工作。×
3ReferenceQueue 只有在弱引用 本身 被回收时才会入队。×
4调用 notifyDataSetChanged() 会使所有 ViewHolder 进入 RecycledViewPool。
5在设置 hasStableIds=true 后,notifyItemChanged() 不会溢出回收池容量。

【解析】

  1. 软引用逻辑正确。
  2. 2.x 利用 ContentProvider 自动初始化。
  3. 入队是因为 referent 被回收。
  4. 文章中已说明。
  5. 设置稳定 ID 后可避免进入池而转入 Scrap。

四、简答题(3 题)

1. 简述 LeakCanary 监控对象是否被回收的核心机制。

【答案要点】

  • 创建 KeyedWeakReference 将目标对象包装成弱引用,并关联 ReferenceQueue
  • 把 key 存入 retainedKeys
  • 轮询 ReferenceQueue:若弱引用入队则移除 key,表示已回收;
  • 若超时仍存在 key,则主动 GC → dump hprof → 堆分析。
    【解析】利用弱引用+ReferenceQueue 判定对象可达性,再辅以主动 GC 与堆分析确定泄漏。

2. 为什么 LeakCanary 在调试器连接时默认跳过分析?

【答案要点】

  • JDWP 会保持大量对象与线程引用,影响可达性判断;
  • 调试器常关闭即时编译,GC 行为不同,易误报;
  • 避免分析阻塞调试体验。

3. 说明 LeakCanary 2.x 新增对 View 泄漏监控的实现思路。

【答案要点】

  • 通过 RootViewWatcher 监听 Window 根 View attach/detach;
  • 借助 Curtains 库 Hook WindowManagerGlobal,在 View detached 时延迟 5 s 调用 ObjectWatcher.expectWeaklyReachable;
  • 如果根 View 未被回收则认定泄漏,继续常规堆分析流程。

五、编程题(1 题)

要求:自定义一个 OnObjectRetainedListener,当检测到应用中保留对象总数 ≥ 3 时,写入日志并在主线程弹出 Toast 提示“可能存在严重内存泄漏”。给出核心代码。

【参考答案】

class ToastOnLeakListener(private val context: Application) : OnObjectRetainedListener {

    override fun onObjectRetained() {
        // 已保留对象数量
        val retained = AppWatcher.objectWatcher.retainedObjectCount
        if (retained >= 3) {
            // 记录日志
            Log.w("LeakCanaryDemo", "Retained objects = $retained, possible leak!")
            // 主线程弹 Toast
            Handler(Looper.getMainLooper()).post {
                Toast.makeText(context,
                    "可能存在严重内存泄漏($retained)", Toast.LENGTH_LONG).show()
            }
        }
    }
}

/*** 在 Application.onCreate() 中注册 ***/
override fun onCreate() {
    super.onCreate()
    AppWatcher.objectWatcher.addOnObjectRetainedListener(
        ToastOnLeakListener(this)
    )
}

【解析】实现接口 OnObjectRetainedListener,在回调里读取 retainedObjectCount。达到阈值后通过 Handler 切换到主线程 Toast。示例符合 LeakCanary 2.x API。