LeakCanary源码简单分析
Posted 涂程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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@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不熟悉的看这里工作中遇到的Android内存优化问题-leakcanary源码解析
工作中遇到的Android内存优化问题-leakcanary源码解析
全新 LeakCanary 2 ! 完全基于 Kotlin 重构升级 !