带你学开源项目:LeakCanary-如何检测 Activity 是否泄漏
JocelynO45
7年前
<p>OOM 是 Android 开发中常见的问题,而内存泄漏往往是罪魁祸首。</p> <p>为了简单方便的检测内存泄漏,Square 开源了 <a href="/misc/goto?guid=4958874766191309432" rel="nofollow,noindex"> LeakCanary </a> ,它可以实时监测 Activity 是否发生了泄漏,一旦发现就会自动弹出提示及相关的泄漏信息供分析。</p> <p>本文的目的是试图通过分析 LeakCanary 源码来探讨它的 Activity 泄漏检测机制。</p> <h2>LeakCanary 使用方式</h2> <p>为了将 LeakCanary 引入到我们的项目里,我们只需要做以下两步:</p> <pre> <code class="language-java">dependencies { debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' } public class ExampleApplication extends Application { @Override public void onCreate() { super.onCreate(); if (LeakCanary.isInAnalyzerProcess(this)) { // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this); } } </code></pre> <p>可以看出,最关键的就是 LeakCanary.install(this); 这么一句话,正式开启了 LeakCanary 的大门,未来它就会自动帮我们检测内存泄漏,并在发生泄漏是弹出通知信息。</p> <h2>从 LeakCanary.install(this); 开始</h2> <p>下面我们来看下它做了些什么?</p> <pre> <code class="language-java">public static RefWatcher install(Application application) { return install(application, DisplayLeakService.class, AndroidExcludedRefs.createAppDefaults().build()); } public static RefWatcher install(Application application, Class<? extends AbstractAnalysisResultService> listenerServiceClass, ExcludedRefs excludedRefs) { if (isInAnalyzerProcess(application)) { return RefWatcher.DISABLED; } enableDisplayLeakActivity(application); HeapDump.Listener heapDumpListener = new ServiceHeapDumpListener(application, listenerServiceClass); RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs); ActivityRefWatcher.installOnIcsPlus(application, refWatcher); return refWatcher; } </code></pre> <p>首先,我们先看最重要的部分,就是:</p> <pre> <code class="language-java">RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs); ActivityRefWatcher.installOnIcsPlus(application, refWatcher); </code></pre> <p>先生成了一个 RefWatcher ,这个东西非常关键,从名字可以看出,它是用来 watch Reference 的,也就是用来一个监控引用的工具。然后再把 refWatcher 和我们自己提供的 application 传入到 ActivityRefWatcher.installOnIcsPlus(application, refWatcher); 这句里面,继续看。</p> <pre> <code class="language-java">public static void installOnIcsPlus(Application application, RefWatcher refWatcher) { ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher); activityRefWatcher.watchActivities(); } </code></pre> <p>创建了一个 ActivityRefWatcher ,大家应该能感受到,这个东西就是用来监控我们的 Activity 泄漏状况的,它调用 watchActivities() 方法,就可以开始进行监控了。下面就是它监控的核心原理:</p> <pre> <code class="language-java">public void watchActivities() { application.registerActivityLifecycleCallbacks(lifecycleCallbacks); } </code></pre> <p>它向 application 里注册了一个 ActivitylifecycleCallbacks 的回调函数,可以用来监听 Application 整个生命周期所有 Activity 的 lifecycle 事件。再看下这个 lifecycleCallbacks 是什么?</p> <pre> <code class="language-java">private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { ActivityRefWatcher.this.onActivityDestroyed(activity); } }; </code></pre> <p>原来它只监听了所有 Activity 的 onActivityDestroyed 事件,当 Activity 被 Destory 时,调用 ActivityRefWatcher.this.onActivityDestroyed(activity); 函数。</p> <p>猜测下,正常情况下,当一个这个函数应该 activity 被 Destory 时,那这个 activity 对象应该变成 null 才是正确的。如果没有变成null,那么就意味着发生了内存泄漏。</p> <p>因此我们向,这个函数 ActivityRefWatcher.this.onActivityDestroyed(activity); 应该是用来监听 activity 对象是否变成了 null。继续看。</p> <pre> <code class="language-java">void onActivityDestroyed(Activity activity) { refWatcher.watch(activity); } RefWatcher refWatcher = androidWatcher(application, heapDumpListener, excludedRefs); </code></pre> <p>可以看出,这个函数把目标 activity 对象传给了 RefWatcher ,让它去监控这个 activity 是否被正常回收了,若未被回收,则意味着发生了内存泄漏。</p> <h2>RefWatcher 如何监控 activity 是否被正常回收呢?</h2> <p>我们先来看看这个 RefWatcher 究竟是个什么东西?</p> <pre> <code class="language-java">public static RefWatcher androidWatcher(Context context, HeapDump.Listener heapDumpListener, ExcludedRefs excludedRefs) { AndroidHeapDumper heapDumper = new AndroidHeapDumper(context, leakDirectoryProvider); heapDumper.cleanup(); int watchDelayMillis = 5000; AndroidWatchExecutor executor = new AndroidWatchExecutor(watchDelayMillis); return new RefWatcher(executor, debuggerControl, GcTrigger.DEFAULT, heapDumper, heapDumpListener, excludedRefs); } </code></pre> <p>这里面涉及到两个新的对象: AndroidHeapDumper 和 AndroidWatchExecutor ,前者用来 dump 堆内存状态的,后者则是用来 watch 一个引用的监听器。具体原理后面再看。总之,这里已经生成好了一个 RefWatcher 对象了。</p> <p>现在再看上面 onActivityDestroyed(Activity activity) 里调用的 refWatcher.watch(activity); ,下面来看下这个最为核心的 watch(activity) 方法,了解它是如何监控 activity 是否被回收的。</p> <pre> <code class="language-java">private final Set<String> retainedKeys; public void watch(Object activity, String referenceName) { String key = UUID.randomUUID().toString(); retainedKeys.add(key); final KeyedWeakReference reference = new KeyedWeakReference(activity, key, referenceName, queue); watchExecutor.execute(new Runnable() { @Override public void run() { ensureGone(reference, watchStartNanoTime); } }); } final class KeyedWeakReference extends WeakReference<Object> { public final String key; public final String name; } </code></pre> <p>可以看到,它首先把我们传入的 activity 包装成了一个 KeyedWeakReference (可以暂时看成一个普通的 WeakReference),然后 watchExecutor 会去执行一个 Runnable,这个 Runnable 会调用 ensureGone(reference, watchStartNanoTime) 函数。</p> <p>看这个函数之前猜测下,我们知道 watch 函数本身就是用来监听 activity 是否被正常回收,这就涉及到两个问题:</p> <ol> <li>何时去检查它是否回收?</li> <li>如何有效地检查它真的被回收?</li> </ol> <p>所以我们觉得 ensureGone 函数本身要做的事正如它的名字,就是确保 reference 被回收掉了,否则就意味着内存泄漏。</p> <p>核心函数:ensureGone(reference) 检测回收</p> <p>下面来看这个函数实现:</p> <pre> <code class="language-java">void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) { removeWeaklyReachableReferences(); if (gone(reference) || debuggerControl.isDebuggerAttached()) { return; } gcTrigger.runGc(); removeWeaklyReachableReferences(); if (!gone(reference)) { File heapDumpFile = heapDumper.dumpHeap(); heapdumpListener.analyze( new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs)); } } private boolean gone(KeyedWeakReference reference) { return !retainedKeys.contains(reference.key); } private void removeWeaklyReachableReferences() { KeyedWeakReference ref; while ((ref = (KeyedWeakReference) queue.poll()) != null) { retainedKeys.remove(ref.key); } } </code></pre> <p>这里先来解释下 WeakReference 和 ReferenceQueue 的工作原理。</p> <ol> <li>弱引用 WeakReference<br> 被强引用的对象就算发生 OOM 也永远不会被垃圾回收机回收;被弱引用的对象,只要被垃圾回收器发现就会立即被回收;被软引用的对象,具备内存敏感性,只有内存不足时才会被回收,常用来做内存敏感缓存器;虚引用则任意时刻都可能被回收,使用较少。</li> <li>引用队列 ReferenceQueue<br> 我们常用一个 WeakReference<Activity> reference = new WeakReference(activity); ,这里我们创建了一个 reference 来弱引用到某个 activity ,当这个 activity 被垃圾回收器回收后,这个 reference 会被放入内部的 ReferenceQueue 中。也就是说,从队列 ReferenceQueue 取出来的所有 reference ,它们指向的真实对象都已经成功被回收了。</li> </ol> <p>然后再回到上面的代码。</p> <p>在一个 activity 传给 RefWatcher 时会创建一个唯一的 key 对应这个 activity,该key存入一个集合 retainedKeys 中。也就是说,所有我们想要观测的 activity 对应的唯一 key 都会被放入 retainedKeys 集合中。</p> <p>基于我们对 ReferenceQueue 的了解,只要把队列中所有的 reference 取出来,并把对应 retainedKeys 里的key移除,剩下的 key 对应的对象都没有被回收。</p> <ol> <li>ensureGone 首先调用 removeWeaklyReachableReferences 把已被回收的对象的 key 从 retainedKeys 移除,剩下的 key 都是未被回收的对象;</li> <li>if (gone(reference)) 用来判断某个 reference 的key是否仍在 retainedKeys 里,若不在,表示已回收,否则继续;</li> <li>gcTrigger.runGc(); 手动出发 GC,立即把所有 WeakReference 引用的对象回收;</li> <li>removeWeaklyReachableReferences(); 再次清理 retainedKeys,如果该 reference 还在 retainedKeys里 (if (!gone(reference))),表示泄漏;</li> <li>利用 heapDumper 把内存情况 dump 成文件,并调用 heapdumpListener 进行内存分析,进一步确认是否发生内存泄漏。</li> <li>如果确认发生内存泄漏,调用 DisplayLeakService 发送通知。</li> </ol> <p>至此,核心的内存泄漏检测机制便看完了。</p> <h2>内存泄漏检测小结</h2> <p>从上面我们大概了解了内存泄漏检测机制,大概是以下几个步骤:</p> <ol> <li>利用 application.registerActivityLifecycleCallbacks(lifecycleCallbacks) 来监听整个生命周期内的 Activity onDestoryed 事件;</li> <li>当某个 Activity 被 destory 后,将它传给 RefWatcher 去做观测,确保其后续会被正常回收;</li> <li>RefWatcher 首先把 Activity 使用 KeyedWeakReference 引用起来,并使用一个 ReferenceQueue 来记录该 KeyedWeakReference 指向的对象是否已被回收;</li> <li>AndroidWatchExecutor 会在 5s 后,开始检查这个弱引用内的 Activity 是否被正常回收。判断条件是:若 Activity 被正常回收,那么引用它的 KeyedWeakReference 会被自动放入 ReferenceQueue 中。</li> <li>判断方式是:先看 Activity 对应的 KeyedWeakReference 是否已经放入 ReferenceQueue 中;如果没有,则手动GC: gcTrigger.runGc(); ;然后再一次判断 ReferenceQueue 是否已经含有对应的 KeyedWeakReference 。若还未被回收,则认为可能发生内存泄漏。</li> <li>利用 HeapAnalyzer 对 dump 的内存情况进行分析并进一步确认,若确定发生泄漏,则利用 DisplayLeakService 发送通知。</li> </ol> <h2>探讨一些关于 LeakCanary 有趣的问题</h2> <p>在学习了 LeakCanary 的源码之后,我想再提几个有趣的问题做些探讨。</p> <h3>LeakCanary 项目目录结构为什么这样分?</h3> <p>下面是整个 LeakCanary 的项目结构:</p> <p><img src="https://simg.open-open.com/show/eabc028d156252a98fbb7fd2425c3f7e.png"></p> <p>对于开发者而言,只需要使用到 LeakCanary.install(this); 这一句即可。那整个项目为什么要分成这么多个 module 呢?</p> <p>实际上,这里面每一个 module 都有自己的角色。</p> <ul> <li> <p>leakcanary-watcher : 这是一个通用的内存检测器,对外提供一个 RefWatcher#watch(Object watchedReference),可以看出,它不仅能够检测 Activity ,还能监测任意常规的 Java Object 的泄漏情况。</p> </li> <li> <p>leakcanary-android : 这个 module 是与 Android 世界的接入点,用来专门监测 Activity 的泄漏情况,内部使用了 application#registerActivityLifecycleCallbacks 方法来监听 onDestory 事件,然后利用 leakcanary-watcher 来进行弱引用+手动 GC 机制进行监控。</p> </li> <li> <p>leakcanary-analyzer : 这个 module 提供了 HeapAnalyzer ,用来对 dump 出来的内存进行分析并返回内存分析结果 AnalysisResult ,内部包含了泄漏发生的路径等信息供开发者寻找定位。</p> </li> <li> <p>leakcanary-android-no-op : 这个 module 是专门给 release 的版本用的,内部只提供了两个完全空白的类 LeakCanary 和 RefWatcher ,这两个类不会做任何内存泄漏相关的分析。为什么?因为 LeakCanary 本身会由于不断 gc 影响到 app 本身的运行,而且主要用于开发阶段的内存泄漏检测。因此对于 release 则可以 disable 所有泄漏分析。</p> </li> <li> <p>leakcanary-sample : 这个很简单,就是提供了一个用法 sample。</p> </li> </ul> <h3>当 Activity 被 destory 后,LeakCanary 多久后会去进行检查其是否泄漏呢?</h3> <p>在源码中可以看到,LeakCanary 并不会在 destory 后立即去检查,而是让一个 AndroidWatchExecutor 去进行检查。它会做什么呢?</p> <pre> <code class="language-java">@Override public void execute(final Runnable command) { if (isOnMainThread()) { executeDelayedAfterIdleUnsafe(command); } else { mainHandler.post(new Runnable() { @Override public void run() { executeDelayedAfterIdleUnsafe(command); } }); } } void executeDelayedAfterIdleUnsafe(final Runnable runnable) { // This needs to be called from the main thread. Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { backgroundHandler.postDelayed(runnable, delayMillis); return false; } }); } </code></pre> <p>可以看到,它首先会向主线程的 MessageQueue 添加一个 IdleHandler 。</p> <p>什么是 IdleHandler ?我们知道 Looper 会不断从 MessageQueue 里取出 Message 并执行。当没有新的 Message 执行时,Looper 进入 Idle 状态时,就会取出 IdleHandler 来执行。</p> <p>换句话说, IdleHandler 就是 优先级别较低的 Message ,只有当 Looper 没有消息要处理时才得到处理。而且,内部的 queueIdle() 方法若返回 true ,表示该任务一直存活,每次 Looper 进入 Idle 时就执行;反正,如果返回 false ,则表示只会执行一次,执行完后丢弃。</p> <p>那么,这件优先级较低的任务是什么呢? backgroundHandler.postDelayed(runnable, delayMillis); ,runnable 就是之前 ensureGone() 。</p> <p>也就是说,当主线程空闲了,没事做了,开始向后台线程发送一个延时消息,告诉后台线程,5s(delayMillis)后开始检查 Activity 是否被回收了。</p> <p>所以,当 Activity 发生 destory 后,首先要等到主线程空闲,然后再延时 5s(delayMillis),才开始执行泄漏检查。</p> <p>知识点:</p> <ol> <li> <p>如何创建一个优先级低的主线程任务,它只会在主线程空闲时才执行,不会影响到app的性能?</p> <pre> <code class="language-java">Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // do task return false; // only once } }); </code></pre> </li> <li> <p>如何快速创建一个主/子线程handler?</p> <pre> <code class="language-java">//主线程handler mainHandler = new Handler(Looper.getMainLooper()); //子线程handler HandlerThread handlerThread = new HandlerThread(“子线程任务”); handlerThread.start(); Handler backgroundHandler = new Handler(handlerThread.getLooper()); </code></pre> </li> <li> <p>如何快速判断当前是否运行在主线程?</p> <pre> <code class="language-java">Looper.getMainLooper().getThread() == Thread.currentThread(); </code></pre> </li> </ol> <h3>System.gc() 可以触发立即 gc 吗?如果不行那怎么才能触发即时 gc 呢?</h3> <p>在 LeakCanary 里,需要立即触发 gc,并在之后立即判断弱引用是否被回收。这意味着该 gc 必须能够立即同步执行。</p> <p>常用的触发 gc 方法是 System.gc() ,那它能达到我们的要求吗?</p> <p>我们来看下其实现方式:</p> <pre> <code class="language-java">/** * Indicates to the VM that it would be a good time to run the * garbage collector. Note that this is a hint only. There is no guarantee * that the garbage collector will actually be run. */ public static void gc() { boolean shouldRunGC; synchronized(lock) { shouldRunGC = justRanFinalization; if (shouldRunGC) { justRanFinalization = false; } else { runGC = true; } } if (shouldRunGC) { Runtime.getRuntime().gc(); } } </code></pre> <p>注释里清楚说了, System.gc() 只是建议垃圾回收器来执行回收,但是 不能保证真的去回收 。从代码也能看出,必须先判断 shouldRunGC 才能决定是否真的要 gc。</p> <p>知识点:</p> <p>那要怎么实现 即时 GC 呢?</p> <p>LeakCanary 参考了一段 <a href="/misc/goto?guid=4959748808121367148" rel="nofollow,noindex">AOSP 的代码</a></p> <pre> <code class="language-java">// System.gc() does not garbage collect every time. Runtime.gc() is // more likely to perfom a gc. Runtime.getRuntime().gc(); enqueueReferences(); System.runFinalization(); public static void enqueueReferences() { /* * Hack. We don't have a programmatic way to wait for the reference queue * daemon to move references to the appropriate queues. */ try { Thread.sleep(100); } catch (InterruptedException e) { throw new AssertionError(); } } </code></pre> <h3>可以怎样来改造 LeakCanary 呢?</h3> <p>忽略某些已知泄漏的类或Activity</p> <p>LeakCanary 提供了 ExcludedRefs 类,可以向里面添加某些主动忽略的类。比如已知 Android 源代码里有某些内存泄漏,不属于我们 App 的泄漏,那么就可以 exclude 掉。</p> <p>另外,如果不想监控某些特殊的 Activity,那么可以在 onActivityDestroyed(Activity activity) 里,过滤掉特殊的 Activity,只对其它 Activity 调用 refWatcher.watch(activity) 监控。</p> <p>把内存泄漏数据上传至服务器</p> <p>在 LeakCanary 提供了 AbstractAnalysisResultService ,它是一个 intentService,接收到的 intent 内包含了 HeapDump 数据和 AnalysisResult 结果,我们只要继承这个类,实现自己的 listenerServiceClass ,就可以将堆数据和分析结果上传到我们自己的服务器上。</p> <h2>小结</h2> <p>本文通过源代码分析了 LeakCanary 的原理,并提出了一些有趣的问题,学习了一些实用的知识点。希望对读者有所启发,欢迎与我讨论。</p> <p>之后会继续挑选优质开源项目进行分析,欢迎提意见。</p> <p> </p> <p>来自:http://wingjay.com/2017/05/14/dig_into_leakcanary/</p> <p></p> <p> </p>