LeakCanary原理及分析

1.LeakCanary简单使用

简介

LeakCanary用来检测内存泄漏的工具,从源码看目前支持activity和fragment

导包

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'//debug包,直接在通知栏显示内存泄漏信息

releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'//

自定义application,然后再onCreate()方法里面添加LeakCanary.install()方法

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return
        }

        LeakCanary.install(this) //引用LeakCanary
    }
}

2.LeakCanary流程简单分析

研究LeakCanary之前先明确几个问题:

1.LeakCanary是用来检测内存泄露的,那么什么是内存泄漏。

内存泄漏简单说就是该回收的内存没有被回收。那么导致内存没有被回收的原因,肯定是这块内存还被引用,导致回收不了。

关于对象,内存和引用之间的关系:具体可以看这篇文章

举个最常见的new关键字创建对象,简单说一下:
Person per = new Person();

Android leaks里面的内容怎么看_弱引用


new Person()会去创建一个对象,然后jvm给该对象在堆内存里分配一块地址。


Person per这里的per是一个引用,会在栈里分配一块内存,引用存储的是一个地址,该地址是句柄的地址,而句柄是一种结构,分别存储 实例指针和类型指针 这两种指针,(实例指针是指向堆中的对象实例,而类型指针指向的是在方法区中该对象所属类型)。当要访问对象时,先通过引用访问句柄,再通过句柄访问对象实例以及对象类型信息。句柄是存储在堆中的,如果使用这种方式,那么就会从堆中分出一块内存用作句柄池。

了解过jvm我们都知道,引用分为强、软、弱、虚四种引用。
强软弱虚四种引用的特点是什么呢?我们引用一段《深入理解java虚拟机》中的原话:

强引用是最传统的“ 引用 ” 的定义,是指在程序代码之中普遍存在的引用赋值,即类似 “Object obj=new Object()” 这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
Student s=new Student();

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在 JDK 1.2 版之后提供了 SoftReference 类来实现软引用。
Student s=new Student();
SoftReference soft = new SoftReference(prime) ;
s= null;

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只 被弱引用关联的对象。在 JDK 1.2 版之后提供了 WeakReference 类来实现弱引用。
Student s=new Student();
WeakReference weakCounter = new WeakReference(s);
s= null;

虚引用也称为 “ 幽灵引用 ” 或者 “ 幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚 引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2版之后提供 了 PhantomReference 类来实现虚引用。
Student s=new Student();
PhantomReference phantom = new PhantomReference(s);
s= null;

所以LeakCanary通过弱引用是否被回收来检测是否有内存泄漏,即如果弱引用对象没有被回收,那肯定就是发生了内存泄漏。因为弱引用存活不到垃圾回收之后。

2.思考一个问题,activity被销毁之后就会被立即回收吗,答案是不一定会,因为要等垃圾回收机制启动之后,才有可能被回收,不一定会立即被回收。所以弱引用什么时候用呢。LeakCanary是这么处理的,监听activity的生命周期,在activity的onDestroy之后去创建一个该activity的弱引用。至于怎么监听的生命周期以及其原理什么,我们在单独的文章里讨论,这里不多赘述。

3.现在activity的弱引用有了,那么怎么知道这个弱引用有没有被回收呢。这里先简单介绍一下ReferenceReferenceQueue

Reference类有两个构造方法

Reference(T referent) {
    this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

一个带ReferenceQueue一个不带ReferenceQueue,区别就是带ReferenceQueue的方法,垃圾回收之后会把Reference放到queue里面。

所以LeakCanary是这么处理的,把activity对应的引用object和一个ReferenceQueue组成WeakReference, WeakReference是Reference的子类
垃圾回收之后通过queue的poll()方法取出这个WeakReference。

现在应用有很多activity,应用退出后怎么判断所有的activity都被回收了呢,

LeakCanary这么处理,监听activity的onDestroy()方法时,本来是直接将activity的引用和ReferenceQueue组成一个WeakReference, 优化一下,用一个随机生成的key,和activity的引用object以及ReferenceQueue组成一个叫KeyedWeakReference,KeyedWeakReference是WeakReference的子类。然后将key存到set集合里。

触发检测的时候,先从ReferenceQueue里面拿到KeyedWeakReference以及KeyedWeakReference的key参数。然后从set集合里面移除相应的key。最后判断set集合是否有对应的key,如果是空那就没有内存泄漏。如果不为空是不是就一定有内存泄漏了呢,答案是不一定,因为可能没有触发GC操作。

所以在这个基础上,如果set集合有对应的key,那就再手动触发一次GC操作,再从ReferenceQueue里面拿到KeyedWeakReference以及KeyedWeakReference的key参数。然后从set集合里面移除相应的key。最后判断set集合是否有对应的key,如果这时候还不为空,那说明确实发生了内存泄漏。

4.就是什么时候去触发内存泄漏的检测。LeakCanary是这么处理的,通过android消息机制,即Handler的IdleHandler

IdleHandler 说白了,就是 Handler 机制提供的一种,可以在 Looper 事件循环的过程中,当出现空闲的时候,允许我们执行任务的一种机制。
既然 IdleHandler 主要是在 MessageQueue 出现空闲的时候被执行,那么何时出现空闲?
MessageQueue 是一个基于消息触发时间的优先级队列,所以队列出现空闲存在两种场景。
MessageQueue 为空,没有消息;
MessageQueue 中最近需要处理的消息,是一个延迟消息(when>currentTime),需要滞后执行;

当没有消息要处理时,触发内存泄漏检测

总结一下LeakCanary实现的难点:
1.怎么判断内存泄漏(涉及到activity生命周期的监听,弱引用,ReferenceQueue的特性)
2.什么时候触发内存泄漏检测(涉及到消息机制,IdleHandler的特性)
3.怎么判断所有的activity都被回收(涉及到set集合的特点)

3.LeakCanary相关代码流程分析

3.1 入口

入口在application的onCreate()方法里面创建LeakCanary.install(this)

override fun onCreate() {
        super.onCreate()
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return
        }

        LeakCanary.install(this)
    }

3.2 activity生命周期的监听如何实现

通过ActivityRefWatcher#install方法里

public static void install(@NonNull Context context, @NonNull RefWatcher refWatcher) {
    Application application = (Application) context.getApplicationContext();
    ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);

    //通过application的registerActivityLifecycleCallbacks()方法注册生命周期监听
    application.registerActivityLifecycleCallbacks(activityRefWatcher.lifecycleCallbacks);
  }

callback的实现也很简单,只关注onActivityDestroyed()方法,拿到activity的引用

private final Application.ActivityLifecycleCallbacks lifecycleCallbacks =
      new ActivityLifecycleCallbacksAdapter() {
        @Override public void onActivityDestroyed(Activity activity) {
          refWatcher.watch(activity);
        }
      };

3.3 弱引用以及ReferenceQueue的使用技巧

RefWatcher#watch()方法里面

//watchedReference就是3.2中对应的activity
  public void watch(Object watchedReference, String referenceName) {
    if (this == DISABLED) {
      return;
    }
    checkNotNull(watchedReference, "watchedReference");
    checkNotNull(referenceName, "referenceName");
    final long watchStartNanoTime = System.nanoTime();
    String key = UUID.randomUUID().toString();//创建一个random的key值
    retainedKeys.add(key);//retainedKeys是一个set集合
    //将watchedReference(activity的引用),key,queue组合成KeyedWeakReference,KeyedWeakReference是WeakReference的子类
    //垃圾回收之后,queue里面就会有KeyedWeakReference对象,这是Reference的特性。子类也有这个特性。
    final KeyedWeakReference reference =
        new KeyedWeakReference(watchedReference, key, referenceName, queue);//queue就是ReferenceQueue


    ensureGoneAsync(watchStartNanoTime, reference);//准备完毕了,等待触发内存泄漏检测
  }

3.4 触发内存泄漏检测

也就是idleHandler怎么用的:

3.3的技巧组合完毕之后呢,进入ensureGoneAsync方法,首先通过watchExecutor.execute()方法,去执行IdlerHandler操作。
真正判断内存泄露的是ensureGone()方法

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run() {
        return ensureGone(reference, watchStartNanoTime);
      }
    });
  }

execute方法就是教你怎么实现一个Idlehandler,最终run()方法的回调是在postToBackgroundWithDelay()方法里。

@Override public void execute(@NonNull Retryable retryable) {
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0);
    }
  }

  private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
    mainHandler.post(new Runnable() {
      @Override public void run() {
        waitForIdle(retryable, failedAttempts);
      }
    });
  }

  private void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle() {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false;
      }
    });
  }

  private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run() {
        Retryable.Result result = retryable.run(); //run方法的回调
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1);
        }
      }
    }, delayMillis);
  }

3.5 具体判断有没有内存泄漏

具体思路是先遍历一遍ReferenceQueue,拿到对应的key之后清除set集合对应的key,如果集合不包含key就没有内存泄漏。
如果集合不为空,就手动触发一次GC,然后再去遍历一遍ReferenceQueue,拿到对应的key之后清除set集合对应的key,再去判断集合是否有key,没有可以就没有内存泄漏,有key就是内存泄漏。

Retryable.Result ensureGone(final KeyedWeakReference reference, final long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);

    removeWeaklyReachableReferences();//1.遍历ReferenceQueue

    if (debuggerControl.isDebuggerAttached()) {
      // The debugger can create false leaks.
      return RETRY;
    }
    if (gone(reference)) { //2.gone()方法判断集合是否包含当前reference的key
      return DONE;
    }
    gcTrigger.runGc();//3.手动GC
    removeWeaklyReachableReferences();//4.GC完毕后再检查一遍
    if (!gone(reference)) {//5.如果还是有reference的key,说明出现内存泄露了。
      long startDumpHeap = System.nanoTime();
      long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

      File heapDumpFile = heapDumper.dumpHeap();
      if (heapDumpFile == RETRY_LATER) {
        // Could not dump the heap.
        return RETRY;
      }
      long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

      HeapDump heapDump = heapDumpBuilder.heapDumpFile(heapDumpFile).referenceKey(reference.key)
          .referenceName(reference.name)
          .watchDurationMs(watchDurationMs)
          .gcDurationMs(gcDurationMs)
          .heapDumpDurationMs(heapDumpDurationMs)
          .build();

      heapdumpListener.analyze(heapDump);//6.输出内存泄漏信息。
    }
    return DONE;
  }

  private boolean gone(KeyedWeakReference reference) {
    return !retainedKeys.contains(reference.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) {
      retainedKeys.remove(ref.key); //刷新set集合,将回收的key清理掉。
    }
  }