LeakCanary 原理詳解

內存泄漏是性能優化中必須去關注的一個方面,LeakCanary 在發現內存泄漏問題上是一個優秀的工具,今天來分析下它內部的工作原理是怎樣的。

首先來看幾個問題:

  1. 集成 LeakCanary 後,安裝應用到手機上,會發現桌面上多了一個 LeakCanary 的圖標,這個圖標是怎麼來的呢
  2. 內存泄漏的問題是怎麼檢測到的呢

先來看第一個問題吧,這個比較簡單,在調用 LeakCanary.install(this) 時,點進源碼,會看到這樣一行代碼

enableDisplayLeakActivity(application);

這行代碼最終執行的是

public static void setEnabledBlocking(Context appContext, Class<?> componentClass, boolean enabled) {
    ComponentName component = new ComponentName(appContext, componentClass);
    PackageManager packageManager = appContext.getPackageManager();
    int newState = enabled?1:2;
    packageManager.setComponentEnabledSetting(component, newState, 1);
}

這裏的 componentClass 是 DisplayLeakActivity.class, 在 AndroidManifest.xml 文件中聲明這個 Activity 時,設置 android:enabled="false", 然後再在代碼中動態這樣設置,就會在桌面上生成新的圖標,作爲 Activity 的入口。


接下來解答第二個問題,內存泄漏的問題是怎麼檢測到的,也就是說比如如何檢測 Activity finish 後還沒有被回收,這裏 LeakCanary 用的方法是,記錄所有的 Activity,在 Application 中註冊 LeakCanary 時通過 registerActivityLifecycleCallbacks 監聽 Activity 的生命週期,然後在 Activity 執行 onDestroy 時看 Activity 是否被回收,如果沒有沒回收,觸發 gc, 再看有沒有被回收,如果還沒有被回收,那麼就是有內存泄漏了,就收集內存泄漏相關日誌信息。
大概的流程就是這樣子了,具體細節,如何檢測 Activity 是否被回收,如何觸發了 gc, 看下 LeakCanary 的實現方式的主要代碼:

public void watch(Object watchedReference, String referenceName) {
    //...
    if(!this.debuggerControl.isDebuggerAttached()) {
        final long watchStartNanoTime = System.nanoTime();
        String key = UUID.randomUUID().toString();
        this.retainedKeys.add(key);
        // watchedReference 執行過onDrstroy的Activity的引用,key爲隨機數,queue 是一個ReferenceQueue對象,在引用中用於記錄回收的對象
        final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, this.queue);
        this.watchExecutor.execute(new Runnable() {
            public void run() {
                // 方法最後執行到這裏
                RefWatcher.this.ensureGone(reference, watchStartNanoTime);
            }
        });
    }
}
// reference 這是一個弱引用
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    long gcStartNanoTime = System.nanoTime();
    long watchDurationMs = TimeUnit.NANOSECONDS.toMillis(gcStartNanoTime - watchStartNanoTime);
    // 這個方法做的操作是 有被回收的,從集合中移除 reference.key
    // 這個方法裏就利用的 Reference.queue, 從 queue 裏面取出來說明是被回收的
    this.removeWeaklyReachableReferences();
    if(!this.gone(reference) && !this.debuggerControl.isDebuggerAttached()) {
        // 進到 if 裏面說明還沒有被回收
        // 觸發 gc, 這裏觸發 gc 的方式是調用 Runtime.getRuntime().gc();
        this.gcTrigger.runGc();
        // 重新把已回收的 key 從集合中 remove 掉
        this.removeWeaklyReachableReferences();
        if(!this.gone(reference)) {
            // 進到 if 裏,說明還沒有被回收,gc後還沒有被回收,說明這是回收不了的了,也就是發生了內存泄漏了
            long startDumpHeap = System.nanoTime();
            long gcDurationMs = TimeUnit.NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);
            // 收集 hprof 文件
            File heapDumpFile = this.heapDumper.dumpHeap();
            if(heapDumpFile == HeapDumper.NO_DUMP) {
                return;
            }

            long heapDumpDurationMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);
            // 解析泄漏日誌,通知有泄漏,並保存到本地
            this.heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, this.excludedRefs, watchDurationMs, gcDurationMs, heapDumpDurationMs));
        }

    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章