作者:奔波儿灞取经


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@6e0be858
before
after: java.lang.ref.WeakReference@6e0be858

通过上述代码,我们看到,当​​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​​已经给我们写好了,我们直接看它的代码就行。

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​​​的功劳),所以我们尝试去清除一次​​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()​​中来执行,这里就来跟一下这个支线逻辑:

// 主线逻辑的入口代码。
// 检测并切换到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​​​,所以可以检测下​​gc​​​后​​activity​​是否被回收。

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

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

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

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