煤矿中的金丝雀 LeakCanary 是一个帮我们在 Android 或 Java 项目开发时检测内存泄漏的库。
本文以 LeakCanary-1.5.1 版本为例,分析 LeakCanary 原理,以及借鉴其中的方法。 关于它的图标有一个故事:
17 世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为 “瓦斯检测指标”,以便在危险状况下紧急撤离。
内存泄漏检测的基本原理 给弱引用关联一个引用队列,当弱引用持有内容被 gc 回收后,该弱引用
会被添加到关联的引用队列中。
试试下面的代码,注释掉user=null;
和不注释掉,看看有什么不同
public class RefTest { public static void main (String[] args) { User user = new User("张三" , 18 ); ReferenceQueue<User> queue = new ReferenceQueue<>(); WeakReference<User> weakReference = new WeakReference<User>(user,queue); user=null ; Runtime.getRuntime().gc(); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.runFinalization(); WeakReference pollRef=null ; while ((pollRef = (WeakReference) queue.poll()) != null ) { System.out.println("pollRef的内存地址:" +pollRef.toString()); System.out.println("pollRef等于weakReference?:" +weakReference.equals(pollRef)); } } }
Java 垃圾回收 (GC)
在 Java 中垃圾判断方法是 可达性分析算法,这个算法的基本思路是通过一系列的”GC Root” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到 GC Root 没有任何引用链相连时,则证明此对象是不可用的。
GC Root 的对象包括以下几种:
1、虚拟机栈中引用的对象。
2、方法区中类静态属性引用的对象。
3、方法区中常量引用的对象。
4、本地方法栈中 JNI 引用的对象。
就算一个对象,通过可达性分析算法分析后,发现其是『不可达』的,也并不是非回收不可的。一般情况下,要宣告一个对象死亡,至少要经过两次标记过程:
1、经过可达性分析后,一个对象并没有与 GC Root 关联的引用链,将会被第一次标记和筛选。筛选条件是此对象有没有必要执行 finalize() 方法。如果对象没有覆盖 finalize() 方法,或者已经执行过了。那就认为他可以回收了。如果有必要执行 finalize() 方法,那么将会把这个对象放置到 F-Queue 的队列中,等待执行。
2、虚拟机会建立一个低优先级的 Finalizer 线程执行 F-Queue 里面的对象的 finalize() 方法。如果对象在 finalize() 方法中可以『拯救』自己,那么将不会被回收,否则,他将被移入一个即将被回收的 ReferenceQueue。
LeakCanary 使用 LeakCanary 在 Application 初始化,代码如下:
public class BaseApplication extends Application { @Override public void onCreate () { super .onCreate(); if (LeakCanary.isInAnalyzerProcess(this )) { return ; } LeakCanary.install(this ); } }
编译成功后,可以在 debug 状态下的 AndroidManifest.xml 文件中,看到 LeakCanary 注册的组件。
<!-- 这个是LeakCanary分析泄露的Service --> <service android:name="com.squareup.leakcanary.internal.HeapAnalyzerService" android:enabled="false" android:process=":leakcanary" /> <!-- 这个是LeakCanary展示泄露的Service --> <service android:name="com.squareup.leakcanary.DisplayLeakService" android:enabled="false" /> <activity android:name="com.squareup.leakcanary.internal.DisplayLeakActivity" android:enabled="false" android:icon="@mipmap/leak_canary_icon" android:label="@string/leak_canary_display_activity_label" android:taskAffinity="com.squareup.leakcanary.com.pengguanming.studydemo" android:theme="@style/leak_canary_LeakCanary.Base" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
其中 DisplayLeakActivity 被设置为 Launcher,并设置了金丝雀的 icon,这也就是为什么使用 LeakCanary 会在桌面上生成 icon 入口的原因。但是,这里要注意 DisplayLeakActivity 的 enable 属性被设置为 false 了,默认在桌面上是不会显示入口的。
监听 Activity 销毁的方法 返回来看 LeakCanary.install(this) 方法
public static RefWatcher install (Application application) { return refWatcher(application).listenerServiceClass(DisplayLeakService.class) .excludedRefs(AndroidExcludedRefs.createAppDefaults().build()) .buildAndInstall(); } public static AndroidRefWatcherBuilder refWatcher (Context context) { return new AndroidRefWatcherBuilder(context); }
install 方法通过 AndroidRefWatcherBuilder,构造了一个 RefWatcher 对象,再看 buildAndInstall() 方法:
public RefWatcher buildAndInstall () { RefWatcher refWatcher = build(); if (refWatcher != DISABLED) { LeakCanary.enableDisplayLeakActivity(context); ActivityRefWatcher.install((Application) context, refWatcher); } return refWatcher; }
ActivityRefWatcher.install() 调用了 watchActivities() 方法。
public static void install (Application application, RefWatcher refWatcher) { new ActivityRefWatcher(application, refWatcher).watchActivities(); }
watchActivities() 通过传过来的 application 对象注册了一个 registerActivityLifecycleCallbacks。
public void watchActivities () { stopWatchingActivities(); application.registerActivityLifecycleCallbacks(lifecycleCallbacks); }
这个监听只监听了 onActivityDestroyed() 方法
private final Application.ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacksAdapter() { @Override public void onActivityDestroyed (Activity activity) { refWatcher.watch(activity); } };
上面就是 LeakCanary 注册监听 Activity onDestroy() 生命周期的过程
检测对象弱引用关联引用队列 前面提到,弱引用关联一个引用队列
,当弱引用保存的引用被释放掉后,弱引用就会进入到引用队列
中,LeakCanary 当然也是用这种方式,不过它更聪明。
进入到 refWatcher.watch() 方法中:
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(); retainedKeys.add(key); final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue); ensureGoneAsync(watchStartNanoTime, reference); }
上面的 retainedKeys 是一个支持并发访问的 CopyOnWriteArraySet 对象。 这个 KeyedWeakReference 里的 key 值就是 LeakCanary 聪明的地方了
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" ); } }
为什么要多保存一个 key 值呢?它在哪里用到了呢?继续往下看:
private void ensureGoneAsync (final long watchStartNanoTime, final KeyedWeakReference reference) { watchExecutor.execute(new Retryable() { @Override public Retryable.Result run () { return ensureGone(reference, watchStartNanoTime); } }); }
@SuppressWarnings ("ReferenceEquality" ) Retryable.Result ensureGone (final KeyedWeakReference reference, final long watchStartNanoTime) { long gcStartNanoTime = System.nanoTime(); long watchDurationMs = NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime); removeWeaklyReachableReferences(); if (debuggerControl.isDebuggerAttached()) { return RETRY; } if (gone(reference)) { return DONE; } gcTrigger.runGc(); removeWeaklyReachableReferences(); 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; } 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); } }
在 removeWeaklyReachableReferences() 方法中,将 queue 队列里所有弱引用弹栈。如果出栈了,就移除掉 retainedKeys 中保存的 key。
在 gone() 方法判断,如果 retainedKeys 依然包含 reference 的 key,说明该 reference 没有进入 queue 队列,也就是没有释放掉对象的引用,所以很可能是发生了内存泄漏。
所以 KeyedWeakReference 中保存的 key 值,就是为了方便判断当前的弱引用是否发生了内存泄漏。
LeakCanary 的线程调度 刚才再看刚才忽略的 watchExecutor.execute() 方法,watchExecutor 是一个接口,它的实现类是 AndroidWatchExecutor。
public final class AndroidWatchExecutor implements WatchExecutor { static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump" ; private final Handler mainHandler; private final Handler backgroundHandler; private final long initialDelayMillis; private final long maxBackoffFactor; public AndroidWatchExecutor (long initialDelayMillis) { mainHandler = new Handler(Looper.getMainLooper()); HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME); handlerThread.start(); backgroundHandler = new Handler(handlerThread.getLooper()); this .initialDelayMillis = initialDelayMillis; maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis; } @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.post(new Runnable() { @Override public void run () { waitForIdle(retryable, failedAttempts); } }); } void waitForIdle (final Retryable retryable, final int failedAttempts) { 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; backgroundHandler.postDelayed(new Runnable() { @Override public void run () { Retryable.Result result = retryable.run(); if (result == RETRY) { postWaitForIdle(retryable, failedAttempts + 1 ); } } }, delayMillis); } }
AndroidWatchExecutor 类中,有两个 handler,一个在 main thread,一个在 HandlerThread 的子线程中。外部调用 execute() 方法,最终都会在主线程调用 waitForIdle() 方法。
waitForIdle() 运行在主线程,会在主线程添加一个 IdleHandler。
IdleHandler 的妙用
IdleHandler,这是一种在只有当消息队列没有消息时 或者是队列中的消息还没有到执行时间时 才会执行的 IdleHandler,它存放在 mPendingIdleHandlers
队列中。
关于 IdleHandler 如何使用,这篇文章提供了一个非常好的方法:你知道 android 的 MessageQueue.IdleHandler 吗?
简单总结下这篇文章:
1. 使用 HandlerThread,利用 HandlerThread 绑定的子线程 Handler,实现单线程队列处理数据 + 异步线程接收数据更新
2. 反射 HandlerThread,得到 MessageQueue 对象,调用 MessageQueue.addIdleHandler()
3. 在 IdleHandler 的 queueIdle() 回调方法里通知数据改变
这样做有两个好处:
1.HanlderThread 按加入顺序操作数据,不需要对数据加锁
2. 对 HandlerThread 的 MessageQueue 调用 addIdleHandler(),不管操作了多少遍数据,只会在所有操作完成后(idle),通知数据的监听者,避免频繁更新 UI
AndroidWatchExecutor 的作用就是,在主线程空闲的时候才会调用,并且是延迟调用子线程的 retryable.run() 方法。这样可以保证不影响主线程的流畅度。
手动触发 GC 上面的 retryable.run() 运行在子线程,说明 ensureGone() 就是在子线程。在看一下 LeakCanary 是如何调用 GC 的:
public interface GcTrigger { GcTrigger DEFAULT = new GcTrigger() { @Override public void runGc () { Runtime.getRuntime().gc(); enqueueReferences(); System.runFinalization(); } private void enqueueReferences () { try { Thread.sleep(100 ); } catch (InterruptedException e) { throw new AssertionError(); } } }; void runGc () ; }
gcTrigger.runGc() 先会调用 Runtime.getRuntime().gc() 以触发系统 gc 操作,然后当前后台子线程 sleep 100 毫秒,最后调用 System.runFinalization() 强制系统回收没有引用的队列,这样确保引用对象是否真的被回收了。
泄露信息分析 上面 heapDumper.dumpHeap() 方法的具体实现是在 AndroidHeapDumper 类中:
@SuppressWarnings ("ReferenceEquality" ) @Override public File dumpHeap () { File heapDumpFile = leakDirectoryProvider.newHeapDumpFile(); if (heapDumpFile == RETRY_LATER) { return RETRY_LATER; } FutureResult<Toast> waitingForToast = new FutureResult<>(); showToast(waitingForToast); if (!waitingForToast.wait(5 , SECONDS)) { CanaryLog.d("Did not dump heap, too much time waiting for Toast." ); return RETRY_LATER; } Toast toast = waitingForToast.get(); try { Debug.dumpHprofData(heapDumpFile.getAbsolutePath()); cancelToast(toast); return heapDumpFile; } catch (Exception e) { CanaryLog.d(e, "Could not dump heap" ); return RETRY_LATER; } } private void showToast (final FutureResult<Toast> waitingForToast) { mainHandler.post(new Runnable() { @Override public void run () { final Toast toast = new Toast(context); toast.setGravity(Gravity.CENTER_VERTICAL, 0 , 0 ); toast.setDuration(Toast.LENGTH_LONG); LayoutInflater inflater = LayoutInflater.from(context); toast.setView(inflater.inflate(R.layout.leak_canary_heap_dump_toast, null )); toast.show(); Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle () { waitingForToast.set(toast); return false ; } }); } }); } private void cancelToast (final Toast toast) { mainHandler.post(new Runnable() { @Override public void run () { toast.cancel(); } }); }
这里的 FutureResult,会阻塞当前后台子线程,并监听主线程是否空闲,若 5 秒内不空闲,则返回 RETRY_LATER。 若主线程空闲则会调用 waitingForToast.set(toast),不在阻塞后台子线程,调用 android.os.Debug.dumpHprofData() 生成. prof 文件。
Debug.dumpHprofData 会导致线程阻塞。因为 dumpheap 的操作是在应用进程的主线程中进行操作,本质上是在该应用进程的虚拟机中进行,dumpheap 时应用进程会 block 住,如过 heap 文件过大很容易导致应用进程操作界面卡住,如果此时再进行点击或滑动等操作极易再抛出 anr 等弹窗,用户体验极差。
这里介绍下 FutureResult 的实现:
public final class FutureResult <T > { private final AtomicReference<T> resultHolder; private final CountDownLatch latch; public FutureResult () { resultHolder = new AtomicReference<>(); latch = new CountDownLatch(1 ); } public boolean wait (long timeout, TimeUnit unit) { try { return latch.await(timeout, unit); } catch (InterruptedException e) { throw new RuntimeException("Did not expect thread to be interrupted" , e); } } public T get () { if (latch.getCount() > 0 ) { throw new IllegalStateException("Call wait() and check its result" ); } return resultHolder.get(); } public void set (T result) { resultHolder.set(result); latch.countDown(); } }
关于 CountDownLatch 的介绍参考:Java 并发学习之 CountDownLatch 实现原理及使用姿势
prof 文件分析 关于 prof 文件的分析,还要回到 RefWatch.ensureGone() 方法中:
heapdumpListener.analyze( new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
最终会调用到
HeapAnalyzerService.runAnalysis(context, heapDump, listenerServiceClass); public static void runAnalysis (Context context, HeapDump heapDump, Class<? extends AbstractAnalysisResultService> listenerServiceClass) { Intent intent = new Intent(context, HeapAnalyzerService.class); intent.putExtra(LISTENER_CLASS_EXTRA, listenerServiceClass.getName()); intent.putExtra(HEAPDUMP_EXTRA, heapDump); context.startService(intent); }
HeapAnalyzerService 继承自 IntentService。在前面的 AndroidManifest 中可以看到,HeapAnalyzerService 是运行在单独的进程中。
在 HeapAnalyzerService 的 onHandleIntent 方法中接收和处理传递过来的 dump 文件。
@Override protected void onHandleIntent (Intent intent) { if (intent == null ) { CanaryLog.d("HeapAnalyzerService received a null intent, ignoring." ); return ; } String listenerClassName = intent.getStringExtra(LISTENER_CLASS_EXTRA); HeapDump heapDump = (HeapDump) intent.getSerializableExtra(HEAPDUMP_EXTRA); HeapAnalyzer heapAnalyzer = new HeapAnalyzer(heapDump.excludedRefs); AnalysisResult result = heapAnalyzer.checkForLeak(heapDump.heapDumpFile, heapDump.referenceKey); AbstractAnalysisResultService.sendResultToListener(this , listenerClassName, heapDump, result); }
再看下在 heapAnalyzer.checkForLeak() 方法:
public AnalysisResult checkForLeak (File heapDumpFile, String referenceKey) { long analysisStartNanoTime = System.nanoTime(); if (!heapDumpFile.exists()) { Exception exception = new IllegalArgumentException("File does not exist: " + heapDumpFile); return failure(exception, since(analysisStartNanoTime)); } try { HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile); HprofParser parser = new HprofParser(buffer); Snapshot snapshot = parser.parse(); deduplicateGcRoots(snapshot); Instance leakingRef = findLeakingReference(referenceKey, snapshot); if (leakingRef == null ) { return noLeak(since(analysisStartNanoTime)); } return findLeakTrace(analysisStartNanoTime, snapshot, leakingRef); } catch (Throwable e) { return failure(e, since(analysisStartNanoTime)); } }
在 heapAnalyzer.checkForLeak() 方法中引入 HAHA 库 (一个 heap prof 堆文件分析库),将 hprof 文件解析成内存快照 Snapshot 对象进行分析。 还使用 jetBrains 的 THashMap (THashMap 的内存占用量比 HashMap 小) 做中转,去掉 snapshot 中 GCRoot 的重复路径,以减少内存压力。
返回分析结果 在 HeapAnalyzerService 的 onHandleIntent 方法中,调用了
AbstractAnalysisResultService.sendResultToListener(this , listenerClassName, heapDump, result); public static void sendResultToListener (Context context, String listenerServiceClassName, HeapDump heapDump, AnalysisResult result) { Class<?> listenerServiceClass; try { listenerServiceClass = Class.forName(listenerServiceClassName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } Intent intent = new Intent(context, listenerServiceClass); intent.putExtra(HEAP_DUMP_EXTRA, heapDump); intent.putExtra(RESULT_EXTRA, result); context.startService(intent); }
启动 DisplayLeakService,并将结果返回。DisplayLeakService 运行在主线程,继承自 AbstractAnalysisResultService,也是一个 IntentService。 在 AbstractAnalysisResultService 的 onHandleIntent() 接收结果,并回调 onHeapAnalyzed() 方法,创建并显示一个通知。
具体的结果分析和处理,可以看这篇文章:LeakCanary 源码分析第三讲-HeapAnalyzerService 详解
内存泄漏信息保存和分析的大概流程是:
通过 Debug.dumpHprofData(),生成一个 xxx_pending.hprof 文件
使用 HAHA 库(github.com/square/haha),将 hprof 文件解析成内存快照 Snapshot 对象进行分析
使用 jetBrains 的 THashMap 做中转,去掉 snapshot 中 GCRoot 的重复路径,以减少内存压力。
找出泄露对象并找出泄露对象的最短路径
得到结果后,重命名 xxx_pending.hprof 文件为 yyyy-MM-dd_HH-mm-ss_SSS.hprof
将 hprof 文件内容和分析结果都保存到 yyyy-MM-dd_HH-mm-ss_SSS.hprof.result 文件中
以上就是整个 LeakCanary 捕获内存泄漏,并展示出来的整体流程。
学习借鉴
使用 FutureResult 同步主线程和子线程
使用 CopyOnWriteArraySet 解决并发读写问题
构建者模式,代码简洁、清新,链式调用创建对象,参考 RefWatcherBuilder 对象
MessageQueue.addIdleHandler(IdleHandler handler),监听线程空闲
手动 GC,参考 GCTrigger.runGc()。
IntentService 跨进程
Ref https://www.jianshu.com/p/49239eac7a76
https://juejin.im/post/5d09fb7e5188252af2012e73
本文链接:http://agehua.github.io/2019/07/30/LeakCanary_source_code/
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!
------------------------------------------------------------------------------------------------------------------------------
Enjoy it ? Donate me ! 欣赏此文?求鼓励,求支持!