LeakCanary source code analysis

Java Four References

  • Strong references : never recycle
  • Soft reference : reclaimed when memory is insufficient
  • Weak references : recycle when encountered
  • Virtual reference : It is equivalent to no reference, but is used to identify whether the object pointed to is reclaimed.
Use of weak references

We can specify a reference queue for the weak reference. When the object pointed to by the weak reference is recycled, the weak reference will be added to the queue. We can judge the weak reference by judging whether the weak reference exists in the queue. Whether the pointed-to object has been reclaimed.

// 创建一个引用队列
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();
}
复制代码

The print result is as follows:

这个弱引用是:java.lang.ref.WeakReference@6e0be858
before
after: java.lang.ref.WeakReference@6e0be858
复制代码

Through the above code, we can see that when it objis nullnot done, it is carried out gc, and it is found that queuethere is nothing in it; then after it is objset null, it is carried out again gc, and it is found that queuethere is this weak reference in it, which means that it objhas been recycled, and you can do it yourself. ideaThe Run/Debug Configurationoption Add Vm Optionsto print gclog verification, no more nonsense here.

Using this feature, we can detect Activitythe memory leak of . As we all know, Activityit onDestroy()is destroyed later. If we use weak reference to point to Activityit, and specify a reference queue for it, and then onDestroy()check whether there is a Activitycorresponding reference in the reference queue. Weak reference, you can determine Activitywhether it should be recycled.

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

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

那么,什么才是必要的时候呢?其实Leakcanary已经给我们写好了,我们直接看它的代码就行。

LeakCanary的工作原理

此文针对的是1.5.4版本的

我们先将LeanCanary集成到我们的项目中,步骤如下:

1 在gradle中添加依赖

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.4'
复制代码

2 在MainaApplication中进行初始化

LeakCanary.install(this);
复制代码

经过上述两步,我们就在项目中集成了LeakCanary,我们来看它的工作原理。

我们跟着主线代码install():

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

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

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

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

我们直接跟随主线代码buildAndInstall():

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中 :

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:

// 参数是被销毁的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就是一个弱引用:

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:

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的功劳),所以我们尝试去清除一次activitykey队列,然后检测被destroyactivity是否已经被回收,如果没有被回收,也不一定发生了泄漏,因为可能还没有进行过gc,所以我们手动进行了一次gc,然后再次检测该activity 对应的key是否还在key队列,如果还在,那么就说明发生了泄漏,就直接dump堆空间以及相关信息,并提示给开发者。

还记得我们前面为Activity生成的key吗,当这个Activity被回收后,指向它的弱引用就会被放入引用队列queue中,所以当我们检测到queue中有这个引用时,就说明该Activity已经被回收了,就从retainedKeys队列移除这个key。所以,当一个Activitydestroy之后,就先把它对应的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_IDLEMessage,然后调用

Looper.myQueue().addIdleHandler(mGcIdler)
复制代码

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

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

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

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

AndroidWatchExecutor就是做这件事的。

AndroidWatchExecutor

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

// 主线逻辑的入口代码。
// 检测并切换到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
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 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了。

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

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

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

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

对java引用和回收不熟悉的看这里JVM垃圾回收流程

对Handler不熟悉的看这里Handler源码分析

Guess you like

Origin juejin.im/post/7078571651251732494