彻底理解LeakCanary的工作原理

15 篇文章 0 订阅
10 篇文章 0 订阅

参考:一文让你彻底理解LeakCanary的工作原理
leakCanary检测内存泄漏的原理

一、Java四大引用

强引用:绝不回收
软引用:内存不足才回收
弱引用:碰到就回收
虚引用:等价于没有引用,只是用来标识下指向的对象是否被回收。

二、弱引用的使用

  我们可以为弱引用指定一个引用队列,当弱引用指向的对象被回收时,此弱引用就会被添加到这个队列中,我们可以通过判断这个队列中有没有这个弱引用,来判断该弱引用指向的对象是否被回收了。

// 创建一个引用队列
ReferenceQueue<Object> queue = new ReferenceQueue<>();

private void test() {
    // 创建一个对象
    Object obj = new Object();
    // 创建一个弱引用,并指向这个对象,并且将引用队列传递给弱引用
    WeakReference<Object> reference = new WeakReference(obj, queue);
    // 打印出这个弱引用,为了跟gc之后queue里面的对比证明是同一个
    System.out.println("这个弱引用是:" + reference);
    // gc一次看看(什么都没)
    System.gc();
    // 打印队列(应该是空)
    printlnQueue("before");

    // 先设置obj为null,obj可以被回收了
    obj = null;
    // 再进行gc,此时obj应该被回收了,那么queue里面应该有这个弱引用了
    System.gc();
    // 再打印队列
    printlnQueue("after");
}

private void printlnQueue(String tag) {
    System.out.print(tag);
    Object obj;
    // 循环打印引用队列
    while ((obj = queue.poll()) != null) {
        System.out.println(": " + obj);
    }
    System.out.println();
}

打印结果如下所示:

这个弱引用:java.lang.ref.WeakReference@2a139a55
before
after: java.lang.ref.WeakReference@2a139a55

  通过上述代码,我们看到,当obj不为null时,进行gc,发现queue里面什么都没有;然后将obj置为null之后,再次进行gc,发现queue里面有这个弱引用了,这就说明obj已经被回收了,大家可以自己在idea的Run/Debug Configuration选择Add Vm Options来打印gc日志验证,这里不再废话。

  利用这个特性,我们就可以检测Activity 的内存泄漏,众所周知,Activity在onDestroy()之后被销毁,那么我们如果利用弱引用来指向Activity,并为它指定一个引用队列,然后在onDestroy()之后,去查看引用队列里是否有该Activity对应的弱引用,就能确定该Activity是否被回收了。

  那么,怎么在onDestroy()之后呢,用Application的registerActivityLifecycleCallbacks()这个api,就可以检测所有Activity 的生命周期,然后在onActivityDestroyed(activity)这个方法里去检测此activity对应的弱引用是否被放入引用队列,如果被放入,说明此activity已经被回收了,否则说明此activity发生了泄漏,此时就可以将相关信息打印出来。

  但是,这里有一点要注意,activity 的onDestroy()被调用了,只是说明该activity被销毁了,并不是说已经发生了gc,所以,必要的时候,我们需要手动调用下gc,来保证我们的内存泄漏检测逻辑一定是执行在gc之后,这样才能防止误报。

三、 LeakCanary的工作原理

此文针对的是1.5.4版本的。

我们先将LeanCanary集成到我们的项目中,步骤如下:
1. 在gradle中添加依赖

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'

2. 在MainaApplication中进行初始化

LeakCanary.install(this);

  经过上述两步,我们就在项目中集成了LeakCanary,我们来看它的工作原理。我们跟着主线代码install():
LeakCanary.java

public static RefWatcher install(Application application) {
  return refWatcher(application) // 创建对象
        .listenerServiceClass(DisplayLeakService.class) // 用来分析并展示泄漏数据的
      .excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) // 排除不需要分析的引用
      .buildAndInstall(); // 主线逻辑
}

refWatcher(application)只是创建了一个对象,然后保存了参数application,如下:
LeakCanary.java

public static AndroidRefWatcherBuilder refWatcher(Context context) {
  return new AndroidRefWatcherBuilder(context);
}

AndroidRefWatcherBuilder(Context context) {
  // 这里保存了context
  this.context = context.getApplicationContext();
}

我们直接跟随主线代码buildAndInstall()。
AndroidRefWatcherBuilder.java

public RefWatcher buildAndInstall() {
  RefWatcher refWatcher = build(); // 支线代码: 创建对象,并且创建了日志分析器、gc触发器、堆转储器等。
  if (refWatcher != DISABLED) {
    LeakCanary.enableDisplayLeakActivity(context);
    // 主线代码: 把context取出来转换为Application
    ActivityRefWatcher.install((Application) context, refWatcher);
  }
  return refWatcher;
}

跟随主线代码ActivityRefWatcher.install(),以下代码位于ActivityRefWatcher中 。
ActivityRefWatcher.java

public static void install(Application application, RefWatcher refWatcher) {
  new ActivityRefWatcher(application, refWatcher).watchActivities();
}

// 只是保存了变量
public ActivityRefWatcher(Application application, RefWatcher refWatcher) {
  this.application = checkNotNull(application, "application");
  this.refWatcher = checkNotNull(refWatcher, "refWatcher");
}

// 观测所有的Activity
public void watchActivities() {
  // 先停止上次的观测,防止重复观测
  stopWatchingActivities();
  // 直接观测所有的Activity
  application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
}

// 移除对Activity的观测
public void stopWatchingActivities() {
  application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks);
}

// Activity的生命周期观测器
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
    new Application.ActivityLifecycleCallbacks() {
      //...省略无用代码

      @Override public void onActivityDestroyed(Activity activity) {
        // 当Activity被销毁,就检测是否被回收
        ActivityRefWatcher.this.onActivityDestroyed(activity);
      }
    };

// 检测activity是否被回收
void onActivityDestroyed(Activity activity) {
      refWatcher.watch(activity);
}

现在又回到了RefWatcher。
RefWatcher.java

// 参数是被销毁的Activity
public void watch(Object watchedReference) {
  watch(watchedReference, "");
}

public void watch(Object watchedReference, String referenceName) {
  if (this == DISABLED) {
    return;
  }
  checkNotNull(watchedReference, "watchedReference");
  checkNotNull(referenceName, "referenceName");
  // 记录当前时间
  final long watchStartNanoTime = System.nanoTime();
  // 为Activity生成一个对应的key
  String key = UUID.randomUUID().toString();
  // 将这个Activity对应的key添加到集合retainedKeys中
  retainedKeys.add(key);
  // 核心代码,创建一个弱引用,指向这个Activity并且指定一个引用队列
  final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);

  // 主线代码
  ensureGoneAsync(watchStartNanoTime, reference);
}

KeyedWeakReference就是一个弱引用。
KeyedWeakReference.java

final class KeyedWeakReference extends WeakReference<Object> {
  public final String key;
  public final String name;

  KeyedWeakReference(Object referent, String key, String name, ReferenceQueue<Object> referenceQueue) {
    super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
    this.key = checkNotNull(key, "key");
    this.name = checkNotNull(name, "name");
  }
}

紧跟主线代码ensureGoneAsync。
RefWatcher.java

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
  // 支线代码: watchExecutor的实现是AndroidWatchExecutor,后面有分析
  watchExecutor.execute(new Retryable() {
    @Override public Retryable.Result run() {
      // 检测Activity是否被回收
      return ensureGone(reference, watchStartNanoTime);
    }
  });
}

// 核心代码
Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
  // 计算时间差提示给开发
  long gcStartNanoTime = System.nanoTime();
  long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

  // 尝试移除已经被回收的Activity对应的key(因为代码跑到这里可能已经gc过了)
  removeWeaklyReachableReferences();

  // 检测Activity是否已经被回收(key被移除了就是被回收了)
  if (gone(reference)) {
    return DONE;
  }

  // 如果没有被回收,尝试进行一次gc(这就是我们上面说的必要的时候,后面有细讲)
  gcTrigger.runGc();

  // gc之后再进行一次移除
  removeWeaklyReachableReferences();

  // 如果Activity还没有被回收,说明发生了泄漏
  if (!gone(reference)) {
    long startDumpHeap = System.nanoTime();
    long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

    // 抓取堆信息并生成文件
    File heapDumpFile = heapDumper.dumpHeap();
    if (heapDumpFile == RETRY_LATER) {
      return RETRY;
    }
    long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
    // 对泄漏结果进行分析并通知给相应的服务,然后就会弹出一个通知告诉我们发生了泄漏
    heapdumpListener.analyze(
        new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs,
            gcDurationMs, heapDumpDurationMs));
  }
  return DONE;
}

// 检测Activity是否已经被回收,只要Activity对应的key不在了,就说明已经回收了
private boolean gone(KeyedWeakReference reference) {
  return !retainedKeys.contains(reference.key);
}

// 移除所有已经被回收的对象,被回收了就移除activity对应的key
private void removeWeaklyReachableReferences() {
  KeyedWeakReference ref;
  // 遍历引用队列,同时移除该弱引用指向的Activity的key
  while ((ref = (KeyedWeakReference) queue.poll()) != null) {
    retainedKeys.remove(ref.key);
  }
}

  可以看到,首先,我们将检测的代码逻辑丢到watchExecutor来执行(watchExecutor其实是个AndroidWatchExecutor,用来切换线程),当我们的检测逻辑运行时,大概率已经发生过gc了(这是watchExecutor的功劳),所以我们尝试去清除一次activity的key队列,然后检测被destroy的activity是否已经被回收,如果没有被回收,也不一定发生了泄漏,因为可能还没有进行过gc,所以我们手动进行了一次gc,然后再次检测该activity 对应的key是否还在key队列,如果还在,那么就说明发生了泄漏,就直接dump堆空间以及相关信息,并提示给开发者

  还记得我们前面为Activity生成的key吗,当这个Activity被回收后,指向它的弱引用就会被放入引用队列queue中,所以当我们检测到queue中有这个引用时,就说明该Activity已经被回收了,就从retainedKeys队列移除这个key。所以,当一个Activity被destroy之后,就先把它对应的key添加到retainedKeys队列中,等到gc之后,再检测retainedKeys这个队列,如果对应的key还在,就说明发生了内存泄漏

  这里有个问题,为什么gc可能发生,也可能没发生,能精确的判断是否发生过gc吗?

答:不能!

  很简单, 我们知道,Android的Gc是通过GcIdler实现的,它是一个IdleHandler

final class GcIdler implements MessageQueue.IdleHandler {
    @Override
    public final boolean queueIdle() {
        doGcIfNeeded();
        purgePendingResources();
        return false;
    }
}

  系统在空闲的时候先向ActivityThread投递一个标记为GC_WHEN_IDLE的Message,然后调用:

Looper.myQueue().addIdleHandler(mGcIdler)

来触发Gc,说白了就是: Android的Gc过程是通过空闲消息实现的,优先级是很低。

那么,系统什么时候空闲呢?

  当MainLooper中没有消息执行时,就是空闲的,此时就会执行mIdleHandlers里面的内容,gc才会得到执行。

  根据前面分析,我们的检测逻辑要放在gc之后,才能保证正确性,那就需要在mIdleHandlers执行之后了,但是,系统并没有提供比mIdleHandlers优先级更低的工具,所以,我们也只能将我们的检测逻辑也放到mIdleHandlers中去碰碰运气了,万一跑在了gc之后就省事了,万一没跑到gc之后呢?后面再说。

AndroidWatchExecutor就是做这件事的。

AndroidWatchExecutor
  前面分析主线代码的时候,我们将检测逻辑放在了watchExecutor.execute()中来执行,这里就来跟一下这个支线逻辑:
AndroidWatchExecutor.java

// 主线逻辑的入口代码。
// 检测并切换到Main线程去执行,为什么必须在Main线程?
@Override
public void execute(Retryable retryable) {
  if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
    waitForIdle(retryable, 0);
  } else {
    postWaitForIdle(retryable, 0);
  }
}

void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
  // mainHandler是Main线程的
  mainHandler.post(new Runnable() {
    @Override public void run() {
      waitForIdle(retryable, failedAttempts);
    }
  });
}

// 这里直接通过addIdleHandler来投递一个空闲消息
void waitForIdle(final Retryable retryable, final int failedAttempts) {
  // 因为这里需要在Main线程中
  Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
    @Override public boolean queueIdle() {
      // 投递到工作线程中去检测是否发生了泄漏
      postToBackgroundWithDelay(retryable, failedAttempts);
      return false;
    }
  });
}

// 投递到工作线程中去检测
void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
  long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
  long delayMillis = initialDelayMillis * exponentialBackoffFactor;
  // 这个handler是通过HandlerThread创建的
  backgroundHandler.postDelayed(new Runnable() {
    @Override public void run() {
      // 这里触发了回调
      Retryable.Result result = retryable.run();
      // 重试逻辑,可忽略
      if (result == RETRY) {
        postWaitForIdle(retryable, failedAttempts + 1);
      }
    }
  }, delayMillis);
}

  上面的逻辑很简单,第一就是切换到Main线程,因为系统空闲指的是Main线程的Looper没有消息要处理,所以我们要放在Main线程中;第二就是将我们的代码通过IdleHandler来执行,从而来碰碰运气,看能不能跑在gc之后。

接上面的问题:万一没跑到gc之后呢?

  那就要走兜底逻辑了:手动再进行一次gc!就像上面代码中的gcTrigger.runGc();一样。这里有人说了,这么麻烦,你直接手动gc一下不就行了,干嘛这么费劲。

  这是不对的,因为每次gc都会停止所有线程,这样会造成app卡顿。而且,如果刚刚发生过gc,我们又手动调用了一次gc,这样两次gc的时间堆叠起来,卡顿会更明显,这是不友好的。所以,我们在祈祷检测逻辑发生在系统gc之后外,再加上手动gc的兜底逻辑,才是正确的解决方案。

手动gc的逻辑也很简单,是借助于GcTrigger实现的。

GcTrigger.java

public interface GcTrigger {
  // 提供了一个默认实现,如果不手动指定,默认使用的就是这个
    GcTrigger DEFAULT = new GcTrigger() {
          // 主线逻辑的入口代码
        public void runGc() {
              // 先进行gc 
            Runtime.getRuntime().gc();
              // 等待弱引用入队(activity回收后就会入队)
            this.enqueueReferences();
              // 触发Object的finalize()方法
            System.runFinalization();
        }

          // 这里直接休眠100ms等待gc完成和弱引用入队(简单粗暴)
        private void enqueueReferences() {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException var2) {
                throw new AssertionError();
            }
        }
    };

    void runGc();
}

那么,我们为什么不用软引用呢,软引用也可以做到相同的事情啊。

因为软引用是内存不足才回收,内存足够就不回收,而我们要检测的是内存是否泄露,而不是内存是否足够。

假如现在发生了泄漏,但是内存还足够,软引用就检测不出来了,所以我们要用弱引用,碰到就回收。

四、检测时机

(1)Activity检测时机
  通过注册 ActivityLifecycleCallbacks 回调,在 onActivityDestroyed 中将该 activity 加入 watchedObjects 里面等待检测

ActivityWatcher.kt

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

(2) Fragment(AndroidX) 检测时机
  通过注册 FragmentLifecycleCallbacks 回调,在 onFragmentViewDestroyed 和 onFragmentDestroyed 分别将 View 和 Fragment 加入 watchedObjects 里面等待检测。
AndroidXFragmentDestroyWatcher.kt

override fun onFragmentViewDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      val view = fragment.view
      if (view != null) {
        reachabilityWatcher.expectWeaklyReachable(
          view, "${fragment::class.java.name} received Fragment#onDestroyView() callback " +
          "(references to its views should be cleared to prevent leaks)"
        )
      }
    }
 
    override fun onFragmentDestroyed(
      fm: FragmentManager,
      fragment: Fragment
    ) {
      reachabilityWatcher.expectWeaklyReachable(
        fragment, "${fragment::class.java.name} received Fragment#onDestroy() callback"
      )
    }
  }

(3)ViewModel 检测时机
  ViewModel 的检测比较巧妙,Leakcanary 在 Fragment onCreate 时为当前的 Fragment 添加一个 ViewModel,而这个 ViewModel 利用跟随宿主生命周期的原理,在被执行 onClear 时,通过 hook 当前宿主的所有 viewmodel 后,遍历将这些 viewmodel 加入到 watchedObjects 里面

AndroidXFragmentDestroyWatcher.kt

override fun onFragmentCreated(
      fm: FragmentManager,
      fragment: Fragment,
      savedInstanceState: Bundle?
    ) {
      ViewModelClearedWatcher.install(fragment, reachabilityWatcher)
    }
 
 
 
 ViewModelClearedWatcher.kt
 private val viewModelMap: Map<String, ViewModel>? = try {
    val mMapField = ViewModelStore::class.java.getDeclaredField("mMap")
    mMapField.isAccessible = true
    @Suppress("UNCHECKED_CAST")
    mMapField[storeOwner.viewModelStore] as Map<String, ViewModel>
  } catch (ignored: Exception) {
    null
  }
 
  override fun onCleared() {
    viewModelMap?.values?.forEach { viewModel ->
      reachabilityWatcher.expectWeaklyReachable(
        viewModel, "${viewModel::class.java.name} received ViewModel#onCleared() callback"
      )
    }
  }
 
  companion object {
    fun install(
      storeOwner: ViewModelStoreOwner,
      reachabilityWatcher: ReachabilityWatcher
    ) {
      val provider = ViewModelProvider(storeOwner, object : Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel?> create(modelClass: Class<T>): T =
          ViewModelClearedWatcher(storeOwner, reachabilityWatcher) as T
      })
      provider.get(ViewModelClearedWatcher::class.java)
    }
  }

(4)Service检测时机
   Service 类似于 Activity,也是在 onDestroy 时将 Service 对象加入到 watchedObjects 里面,但是由于 Service 没有开放声明周期的回调,所以也是通过 hook 的放获取 Service 的声明周期

再看 onServiceDestroyed 方法

  private fun onServiceDestroyed(token: IBinder) {
    // 通过 token 匹配到预处理时获取到的 service 对象
    servicesToBeDestroyed.remove(token)?.also { serviceWeakReference ->
      serviceWeakReference.get()?.let { service ->
      // 将 service 对象加入到 watchedObjects 里面
         reachabilityWatcher.expectWeaklyReachable(
          service, "${service::class.java.name} received Service#onDestroy() callback"
         )
     }
    }

  上面就是所有默认检测的对象的时机,在这之后,该对象将会被封装成一个弱引用,并关联一个回收队列。原理就是: 弱引用包含的对象在无其他引用的情况下,执行 GC,该对象将会被加入到回收队列。

总结

精简流程如下所示:

  1. LeakCanary.install(application);此时使用application进行registerActivityLifecycleCallbacks,从而来监听Activity的何时被destroy。

  2. 在onActivityDestroyed(Activity activity)的回调中,去检测Activity是否被回收,检测方式如以下步骤。

  3. 使用一个弱引用WeakReference指向这个activity,并且给这个弱引用指定一个引用队列queue,同时创建一个key来标识该activity。

  4. 然后将检测的方法ensureGone()投递到空闲消息队列。

  5. 当空闲消息执行的时候,去检测queue里面是否存在刚刚的弱引用,如果存在,则说明此activity已经被回收,就移除对应的key,没有内存泄漏发生。

  6. 如果queue里不存在刚刚的弱引用,则手动进行一次gc。

  7. gc之后再次检测queue里面是否存在刚刚的弱引用,如果不存在,则说明此activity还没有被回收,此时已经发生了内存泄漏,直接dump堆栈信息并打印日志,否则没有发生内存泄漏,流程结束。

关键问题

  1. 为什么要放入空闲消息里面去执行?

因为gc就是发生在系统空闲的时候的,所以当空闲消息被执行的时候,大概率已经执行过一次gc了。

  1. 为什么在空闲消息可以直接检测activity是否被回收?

跟问题1一样,空闲消息被执行的时候,大概率已经发生过gc,所以可以检测下gc后activity是否被回收。

  1. 如果没有被回收,应该是已经泄漏了啊,为什么再次执行了一次gc,然后再去检测?

根据问题2,空闲消息被执行的时候,大概率已经发生过gc,但是也可能还没发生gc,那么此时activity没有被回收是正常的,所以我们手动再gc一下,确保发生了gc,再去检测activity是否被回收,从而100%的确定是否发生了内存泄漏。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值