• leakCanary原理


    一、面试中的问题

    一般的中高级面试,都会问到性能优化,内存优化问题,而说到内存问题就肯定会问到内存泄漏问题,而一般的求职者二话不说,直接就上LeakCanary

    紧接着肯定是问:那你知道LeakCanary的原理是什么吗?
    可能还会问:你知道LeakCanary使用到的Idle机制吗?

    二、分析LeakCanary原理

    LeakCanary的集成非常简单,添加依赖,然后在Application主要是LeakCanary.install(this);
    这一句代码,不明白的看文档
    LeakCanary

    那接下来我们直接看install方法干了什么。

    1. public static RefWatcher install(Application application) {
    2. return refWatcher(application)
    3. .listenerServiceClass(DisplayLeakService.class)
    4. .excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
    5. .buildAndInstall();
    6. }

    直接看最后的 .buildAndInstall(),发现 install 就是构建了一个 RefWatcher 。

    1. public RefWatcher buildAndInstall() {
    2. ...
    3. RefWatcher refWatcher = build();
    4. if (refWatcher != DISABLED) {
    5. if (watchActivities) {
    6. // 检测Activity内存泄漏的方式
    7. ActivityRefWatcher.install(context, refWatcher);
    8. }
    9. if (watchFragments) {
    10. // 检测Fragment内存泄漏的方式
    11. FragmentRefWatcher.Helper.install(context, refWatcher);
    12. }
    13. }
    14. LeakCanaryInternals.installedRefWatcher = refWatcher;
    15. return refWatcher;
    16. }

    我们选择检测Activity内存泄漏的例子,看下对应处理 ActivityRefWatcher.install(context, refWatcher);

    1. public static void install(Context context, RefWatcher refWatcher) {
    2. Application application = (Application) context.getApplicationContext();
    3. // 创建一个 ActivityRefWatcher
    4. ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
    5. // 从application的registerActivityLifecycleCallbacks接口中,获取到Activity的生命周期回调,并传给 refWatcher。
    6. application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
    7. }
    8. private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
    9. new ActivityLifecycleCallbacksAdapter() {
    10. @Override public void onActivityDestroyed(Activity activity) {
    11. refWatcher.watch(activity);
    12. }
    13. };

    application.registerActivityLifecycleCallbacks 这里就是重点了,监听Activity生命周期,然后在 onActivityDestroyed 回调中调用 refWatcher.watch(activity)。

    然后继续跟:

    1. private final Set retainedKeys;
    2. private final ReferenceQueue queue;
    3. ...
    4. public void watch(Object watchedReference) {
    5. watch(watchedReference, "");
    6. }
    7. public void watch(Object watchedReference, String referenceName) {
    8. ...
    9. final long watchStartNanoTime = System.nanoTime();
    10. String key = UUID.randomUUID().toString();
    11. retainedKeys.add(key);
    12. final KeyedWeakReference reference =
    13. new KeyedWeakReference(watchedReference, key, referenceName, queue);
    14. //重点
    15. ensureGoneAsync(watchStartNanoTime, reference);
    16. }
    17. watchedReference 是传过来的 activity
      retainedKeys 是一个Set,用来记录每一个加入检测的对象的key
      queue :ReferenceQueue 引用队列
      KeyedWeakReference 继承 WeakReference,保存key和 name,
      name传的是空字符串符,可以忽略。

      我们来看下 KeyedWeakReference :

      1. final class KeyedWeakReference extends WeakReference {
      2. public final String key;
      3. public final String name;
      4. KeyedWeakReference(Object referent, String key, String name,
      5. ReferenceQueue referenceQueue) {
      6. super(checkNotNull(referent, "referent"), checkNotNull(referenceQueue, "referenceQueue"));
      7. this.key = checkNotNull(key, "key");
      8. this.name = checkNotNull(name, "name");
      9. }
      10. }
      11. 这里有一个重要知识点,很多文章都没有说到:

        弱引用和引用队列搭配使用,如果弱引用持有的对象被回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。也就是说如果KeyedWeakReference 持有的 Activity 对象被回收,该KeyedWeakReference就会加入到引用队列 queue 中。反过来说,就是如果引用队列 queue 中包含该KeyedWeakReference,则表示 KeyedWeakReference 持有的对象已经被回收了。

        LeakCanary 就是利用这个原理。

        然后呢,创建了弱引用之后,就调用了 ensureGoneAsync()方法。

        1. private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
        2. watchExecutor.execute(new Retryable() {
        3. @Override public Retryable.Result run() {
        4. return ensureGone(reference, watchStartNanoTime);
        5. }
        6. });
        7. }

        这个 watchExecutor 的实现类是 AndroidWatchExecutor,看看AndroidWatchExecutor#execute方法:

        1. @Override public void execute(Retryable retryable) {
        2. if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
        3. //主线程中执行 ensureGone() 任务
        4. waitForIdle(retryable, 0);
        5. } else {
        6. //子线程中执行 ensureGone() 任务
        7. postWaitForIdle(retryable, 0);
        8. }
        9. }

        为什么不直接分析 ensureGone 方法,因为这里有个小知识点,看 waitForIdle() 方法:

        1. private void waitForIdle(final Retryable retryable, final int failedAttempts) {
        2. // 下面的代码,需要在主线程中调用
        3. Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        4. @Override public boolean queueIdle() {
        5. //延时任务
        6. postToBackgroundWithDelay(retryable, failedAttempts);
        7. return false;
        8. }
        9. });
        10. }

        Looper.myQueue().addIdleHandler 会将一个任务添加到主线程消息队列的一个mIdleHandlers列表里,handler在消息队列中取不到消息时,也就是Handler空闲的时候,会去mIdleHandlers列表里取出任务执行。

        主线程空闲时候执行这个任务,具体干了什么呢?
        postToBackgroundWithDelay,顾名思义,后台延时处理:

        1. private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
        2. long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
        3. long delayMillis = initialDelayMillis * exponentialBackoffFactor;
        4. backgroundHandler.postDelayed(new Runnable() {
        5. @Override public void run() {
        6. Retryable.Result result = retryable.run();
        7. if (result == RETRY) {
        8. postWaitForIdle(retryable, failedAttempts + 1);
        9. }
        10. }
        11. }, delayMillis);
        12. }

        这里的延时时间delayMillis跟过去是一个常量,5秒
        也就是主线程空闲5秒后在后台线程执行 ensureGone() 方法。

        重点来了,接下来我们来看一下 ensureGone()方法:

        1. Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
        2. long gcStartNanoTime = System.nanoTime();
        3. long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
        4. //注释1, 移除弱引用 并移除 Set中对应的 key
        5. removeWeaklyReachableReferences();
        6. //注释2,为true表示 reference 对应的key已经不存在了,即reference被成功回收了,也即没有发生泄露
        7. if (gone(reference)) {
        8. return DONE;
        9. }
        10. //注释3,否则 gone(reference) == false,表示 reference 对应的key还存在,即reference还没有被回收,可能是垃圾回收器没有及时回收,手动触发Gc
        11. gcTrigger.runGc();
        12. // 继续移除引用
        13. removeWeaklyReachableReferences();
        14. //注释4,如果此时 gone(reference) == false,表示 reference 还没有被回收,那就是内存泄漏了。
        15. if (!gone(reference)) {
        16. long startDumpHeap = System.nanoTime();
        17. long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
        18. File heapDumpFile = heapDumper.dumpHeap();
        19. if (heapDumpFile == RETRY_LATER) {
        20. // Could not dump the heap.
        21. return RETRY;
        22. }
        23. long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
        24. // 注释5,接下来得到一个内存快照,并在 leakCanary 进程中,通过 第三方库 HAHA ,并结合 GC root,对得到的内存快照 .hprof 进行分析,最终得出导致内存泄漏的引用链。
        25. HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
        26. .referenceName(reference.name)
        27. .watchDurationMs(watchDurationMs)
        28. .gcDurationMs(gcDurationMs)
        29. .heapDumpDurationMs(heapDumpDurationMs)
        30. .build();
        31. heapdumpListener.analyze(heapDump);
        32. }
        33. return DONE;
        34. }

        如何判断内存泄漏的几个步骤

        1. removeWeaklyReachableReferences

        1. private void removeWeaklyReachableReferences() {
        2. KeyedWeakReference ref;
        3. // 当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
        4. // 如果引用队列中是空的,没有Activity对象被回收。
        5. // 如果引用队列不为空,则清空引用队列
        6. while ((ref = (KeyedWeakReference) queue.poll()) != null) {
        7. retainedKeys.remove(ref.key);
        8. }
        9. }

        遍历引用队列,如果queue里面不为空,说明activity被回收了,并在retainedKeys中移除对应的key(引用被回收,则移除key);反之,queue为空,则说明没有引用被回收。

        2. gone(reference)

        1. private boolean gone(KeyedWeakReference reference) {
        2. return !retainedKeys.contains(reference.key);
        3. }

        上面说了“引用被回收则移除key”,那么如果retainedKeys中有对应的key,就说明对象没有被回收。

        3. gcTrigger.runGc();
        上面分析过 ensureGone 方法是在Activity退出后,主线程空闲时,再过5秒才执行,所以对象没有被回收,不一定就是内存泄漏,对象此时可能已经没有被引用了,正在等待下一次垃圾回收,所以手动触发GC,然后再重复 1和2 的操作,如果对象仍然没被回收,说明真的内存泄漏了。

        4. 判断内存泄漏后的处理
        if (!gone(reference)) {...}
        dump 出堆中的对象,用到HAHA这个库,采用可达性分析算法啥的,包括分析到哪个对象引起的内存泄漏,弹出通知,这些就不是本篇重点了,大家有兴趣可以自己去看。

        5. 接下来得到一个内存快照,并在 leakCanary 进程中,通过 第三方库 HAHA ,并结合 GC root,对得到的内存快照 .hprof 进行分析,最终得出导致内存泄漏的引用链。

        总结,回答面试中的问题:

        LeakCanary的原理是什么?(针对Activity来说)

        LeakCanary 通过监听Activity生命周期,在Activity onDestroy的时候,创建一个弱引用,key跟当前Activity绑定,将key保存到set里面,并且关联一个引用队列,然后在主线程空闲5秒后,开始检测是否内存泄漏,具体检测步骤:
        1:判断引用队列中是否有该Activity的引用,有则说明Activity被回收了,移除Set里面对应的key。
        2:判断Set里面是否有当前要检测的Activity的key,如果没有,说明Activity对象已经被回收了,没有内存泄漏。如果有,只能说明Activity对象还没有被回收,可能此时已经没有被引用,不一定是内存泄漏。
        3:手动触发GC,然后重复1和2操作,确定一下是不是真的内存泄漏。

        你知道LeakCanary中的Idle机制吗?

        在Activity onDestroy的时候,LeakCanary并没有马上去执行检测任务,而是将任务添加到消息队列的一个idle任务列表里,然后当Handler 在消息队列中获取不到消息,也就是主线程空闲的时候,会去idle任务列表里取任务出来执行。

      12. 相关阅读:
        GitHub Pulse 是什么?它是否能衡量 OpenTiny 开源项目的健康程度?
        JAVASE---认识异常
        数据加载及预处理
        【django】Forbidden (CSRF cookie not set.)
        Simon Knowles:30年做成三家独角兽公司,AI芯片创业的底层逻辑
        springboot 配置kafka批量消费,并发消费
        JAVA中常用序列化与反序列化合集
        [Go版]设计模式——Template模版方法模式
        JPA Buddy指南
        数据结构与算法基础(青岛大学-王卓)(9)
      13. 原文地址:https://blog.csdn.net/m0_49508485/article/details/127777536