Android 性能監控框架 Matrix(2)內存泄漏監控源碼分析

修復內存泄漏

在開始監測 Activity 內存泄漏以前,Resource Canary 首先會嘗試修復可能的內存泄漏問題,它是經過監聽 ActivityLifeCycleCallbacks 實現的,在 Activity 回調 onDestroy 時,它會嘗試解除 Activity 和 InputMethodManager、View 之間的引用關係:java

public static void activityLeakFixer(Application application) {
    application.registerActivityLifecycleCallbacks(new ActivityLifeCycleCallbacksAdapter() {
        @Override
        public void onActivityDestroyed(Activity activity) {
            ActivityLeakFixer.fixInputMethodManagerLeak(activity);
            ActivityLeakFixer.unbindDrawables(activity);
        }
    });
}
複製代碼

對於 InputMethodManager,它可能引用了 Activity 中的某幾個 View,所以,將它和這幾個 View 解除引用關係便可:算法

public static void fixInputMethodManagerLeak(Context destContext) {
    final InputMethodManager imm = (InputMethodManager) destContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    final String[] viewFieldNames = new String[]{"mCurRootView", "mServedView", "mNextServedView"};
    for (String viewFieldName : viewFieldNames) {
        final Field paramField = imm.getClass().getDeclaredField(viewFieldName);
        ...
        // 若是 IMM 引用的 View 引用了該 Activity,則切斷引用關係
        if (view.getContext() == destContext) {
            paramField.set(imm, null);
        }
    }
}
複製代碼

對於 View,它可能經過監聽器或 Drawable 的形式關聯 Activity,所以,咱們須要把每個可能的引用關係解除掉:json

public static void unbindDrawables(Activity ui) {
    final View viewRoot = ui.getWindow().peekDecorView().getRootView();
    unbindDrawablesAndRecycle(viewRoot);
}

private static void unbindDrawablesAndRecycle(View view) {
    // 解除通用的 View 引用關係
    recycleView(view);

    // 不一樣類型的 View 可能有不一樣的引用關係,一一處理便可
    if (view instanceof ImageView) {
        recycleImageView((ImageView) view);
    }

    if (view instanceof TextView) {
        recycleTextView((TextView) view);
    }

    ...
}

// 將 Listener、Drawable 等可能存在的引用關係切斷
private static void recycleView(View view) {
    view.setOnClickListener(null);
    view.setOnFocusChangeListener(null);
    view.getBackground().setCallback(null);
    view.setBackgroundDrawable(null);
    ...
}
複製代碼

監測內存泄漏

具體的監測工做,ResourcePlugin 交給了 ActivityRefWatcher 來完成。bash

ActivityRefWatcher 主要的三個方法:start、stop、destroy 分別用於啓動監聽線程、中止監聽線程、結束監聽。以 start 爲例:markdown

public class ActivityRefWatcher extends FilePublisher implements Watcher, IAppForeground {

    @Override
    public void start() {
        stopDetect();
        final Application app = mResourcePlugin.getApplication();
        if (app != null) {
            // 監聽 Activity 的 onDestroy 回調,記錄 Activity 信息
            app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
            // 監聽 onForeground 回調,以便根據應用可見狀態修改輪詢間隔時長
            AppActiveMatrixDelegate.INSTANCE.addListener(this);
            // 啓動監聽線程
            scheduleDetectProcedure();
        }
    }
}
複製代碼

記錄 Activity 信息

其中 mRemovedActivityMonitor 用於在 Activity 回調 onDestroy 時記錄 Activity 信息,主要包括 Activity 的類名和一個根據 UUID 生成的 key:app

// 用於記錄 Activity 信息
private final ConcurrentLinkedQueue<DestroyedActivityInfo> mDestroyedActivityInfos;

private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {

    @Override
    public void onActivityDestroyed(Activity activity) {
        pushDestroyedActivityInfo(activity);
    }
};

// 在 Activity 銷燬時,記錄 Activity 信息
private void pushDestroyedActivityInfo(Activity activity) {
    final String activityName = activity.getClass().getName();
    final UUID uuid = UUID.randomUUID();
    final String key = keyBuilder.toString(); // 根據 uuid 生成
    final DestroyedActivityInfo destroyedActivityInfo = new DestroyedActivityInfo(key, activity, activityName);
    mDestroyedActivityInfos.add(destroyedActivityInfo);
}
複製代碼

DestroyedActivityInfo 包含信息以下:dom

public class DestroyedActivityInfo {
    public final String mKey; // 根據 uuid 生成
    public final String mActivityName; // 類名
    public final WeakReference<Activity> mActivityRef; // 弱引用
    public int mDetectedCount = 0; // 重複檢測次數,默認檢測 10 次後,依然能經過弱引用獲取,才認爲發生了內存泄漏
}
複製代碼

啓動監聽線程

線程啓動後,應用可見時,默認每隔 1min(經過 IDynamicConfig 指定) 將輪詢任務發送到默認的後臺線程(MatrixHandlerThread)執行:ide

// 自定義的線程切換機制,用於將指定的任務延時發送到主線程/後臺線程執行
private final RetryableTaskExecutor             mDetectExecutor;

private ActivityRefWatcher(...) {
    HandlerThread handlerThread = MatrixHandlerThread.getDefaultHandlerThread();
    mDetectExecutor = new RetryableTaskExecutor(config.getScanIntervalMillis(), handlerThread);
}

private void scheduleDetectProcedure() {
    // 將任務發送到 MatrixHandlerThread 執行
    mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
複製代碼

下面看輪詢任務 mScanDestroyedActivitiesTask,它是一個內部類,代碼很長,咱們一點一點分析。優化

設置哨兵檢測 GC 是否執行

首先,在上一篇文章關於原理的部分介紹過,ResourceCanary 會設置了一個哨兵元素,檢測是否真的執行了 GC,若是沒有,它不會往下執行:ui

private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {

    @Override
    public Status execute() {
        // 建立指向一個臨時對象的弱引用
        final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
        // 嘗試觸發 GC
        triggerGc();
        // 檢測弱引用指向的對象是否存活來判斷虛擬機是否真的執行了GC
        if (sentinelRef.get() != null) {
            // System ignored our gc request, we will retry later.
            return Status.RETRY;
        }
        ...
        return Status.RETRY; // 返回 retry,這個任務會一直執行
    }
};

private void triggerGc() {
    Runtime.getRuntime().gc();
    Runtime.getRuntime().runFinalization();
}
複製代碼

過濾已上報的 Activity

接着,遍歷全部 DestroyedActivityInfo,並標記該 Activity,避免重複上報:

final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();

while (infoIt.hasNext()) {
    if (!mResourcePlugin.getConfig().getDetectDebugger() 
            && isPublished(destroyedActivityInfo.mActivityName) // 若是已標記,則跳過
            && mDumpHprofMode != ResourceConfig.DumpMode.SILENCE_DUMP) {
        infoIt.remove();
        continue;
    }

    if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP) {
        if (mResourcePlugin != null && !isPublished(destroyedActivityInfo.mActivityName)) { // 若是已標記,則跳過
            ...
        }
        if (null != activityLeakCallback) { // 但還會回調 ActivityLeakCallback
            activityLeakCallback.onLeak(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey);
        }
    } else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP) {
        ...
        markPublished(destroyedActivityInfo.mActivityName); // 標記
    } else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP) {
        ...
        markPublished(destroyedActivityInfo.mActivityName); // 標記
    } else { // NO_DUMP
        ...
        markPublished(destroyedActivityInfo.mActivityName); // 標記
    }
}
複製代碼

屢次檢測,避免誤判

同時,在重複檢測大於等於 mMaxRedetectTimes 次時(由 IDynamicConfig 指定,默認爲 10),若是還能獲取到該 Activity 的引用,纔會認爲出現了內存泄漏問題:

while (infoIt.hasNext()) {
    ...

    // 獲取不到,Activity 已回收
    if (destroyedActivityInfo.mActivityRef.get() == null) {
        continue;
    }

    // Activity 未回收,可能出現了內存泄漏,但爲了不誤判,須要重複檢測屢次,若是都能獲取到 Activity,才認爲出現了內存泄漏
    // 只有在 debug 模式下,纔會上報問題,不然只會打印一個 log
    ++destroyedActivityInfo.mDetectedCount;
    if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes 
            || !mResourcePlugin.getConfig().getDetectDebugger()) {
        MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
                + "exists in %s times, wait for next detection to confirm.",
            destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
        continue;
    }
}
複製代碼

須要注意的是,只有在 debug 模式下,纔會上報問題,不然只會打印一個 log。

上報問題

對於 silence_dump 和 no_dump 模式,它只會記錄 Activity 名,並回調 onDetectIssue:

final JSONObject resultJson = new JSONObject();
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
mResourcePlugin.onDetectIssue(new Issue(resultJson));
複製代碼

對於 manual_dump 模式,它會使用 ResourceConfig 指定的 Intent 生成一個通知:

...
Notification notification = buildNotification(context, builder);
notificationManager.notify(NOTIFICATION_ID, notification);
複製代碼

對於 auto_dump,它會自動生成一個 hprof 文件並對該文件進行分析:

final File hprofFile = mHeapDumper.dumpHeap();
final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
mHeapDumpHandler.process(heapDump);
複製代碼

生成 hprof 文件

dumpHeap 方法作了兩件事:生成一個文件,寫入 Hprof 數據到文件中:

public File dumpHeap() {
    final File hprofFile = mDumpStorageManager.newHprofFile();
    Debug.dumpHprofData(hprofFile.getAbsolutePath());
}
複製代碼

以後 HeapDumpHandler 就會處理該文件:

protected AndroidHeapDumper.HeapDumpHandler createHeapDumpHandler(...) {
    return new AndroidHeapDumper.HeapDumpHandler() {

        @Override
        public void process(HeapDump result) {
            CanaryWorkerService.shrinkHprofAndReport(context, result);
        }
    };
}
複製代碼

處理流程以下:

private void doShrinkHprofAndReport(HeapDump heapDump) {
    // 裁剪 hprof 文件
    new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile);
    // 壓縮裁剪後的 hprof 文件
    zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile)));
    copyFileToStream(shrinkedHProfFile, zos);
    // 刪除舊文件
    shrinkedHProfFile.delete();
    hprofFile.delete();
    // 上報結果
    CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName());
}

private void doReportHprofResult(String resultPath, String activityName) {
    final JSONObject resultJson = new JSONObject();
    resultJson.put(SharePluginInfo.ISSUE_RESULT_PATH, resultPath);
    resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityName);
    Plugin plugin =  Matrix.with().getPluginByClass(ResourcePlugin.class);
    plugin.onDetectIssue(new Issue(resultJson));
}
複製代碼

能夠看到,因爲原始 hprof 文件很大,所以 Matrix 先對它作了一個裁剪優化,接着再壓縮裁剪後的文件,並刪除舊文件,最後回調 onDetectIssue,上報文件位置、Activity 名稱等信息。

分析結果

示例

檢測到內存泄漏問題後,ActivityRefWatcher 會打印日誌以下:

activity with key [MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLeakActivity_...] was suspected to be a leaked instance. mode[AUTO_DUMP]
複製代碼

若是模式爲 AUTO_DUMP,且設置了 mDetectDebugger 爲 true,那麼,還會生成一個 hprof 文件:

hprof: heap dump "/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/dump_*.hprof" starting...
複製代碼

裁剪壓縮後在 /sdcard/data/[package name]/matrix_resource 文件夾下會生成一個 zip 文件,好比:

/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/dump_result_*.zip
複製代碼

zip 文件裏包括一個 dump_*_shinked.hprof 文件和一個 result.info 文件,其中 result.info 包含設備信息和關鍵 Activity 的信息,好比:

# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !!
sdkVersion=23
manufacturer=vivo
hprofEntry=dump_323ff84d95424d35b0f62ef6a3f95838_shrink.hprof
leakedActivityKey=MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLeakActivity_8c5f3e9db8b54a199da6cb2abf68bd12
複製代碼

拿到這個 zip 文件,輸入路徑參數,執行 matrix-resource-canary-analyzer 中的 CLIMain 程序,便可獲得一個 result.json 文件:

{
    "activityLeakResult": {
        "failure": "null",
        "referenceChain": ["static sample.tencent.matrix.resource.TestLeakActivity testLeaks", ..., "sample.tencent.matrix.resource.TestLeakActivity instance"],
        "leakFound": true,
        "className": "sample.tencent.matrix.resource.TestLeakActivity",
        "analysisDurationMs": 185,
        "excludedLeak": false
    },
    "duplicatedBitmapResult": {
        "duplicatedBitmapEntries": [],
        "mFailure": "null",
        "targetFound": false,
        "analyzeDurationMs": 387
    }
}
複製代碼

注意,CLIMain 在分析重複 Bitmap 時,須要反射 Bitmap 中的 "mBuffer" 字段,而這個字段在 API 26 已經被移除了,所以,對於 API 大於等於 26 的設備,CLIMain 只能分析 Activity 內存泄漏,沒法分析重複 Bitmap。

分析過程

下面簡單分析一下 CLIMain 的執行過程,它是基於 Square Haha 開發的,執行過程分爲 5 步:

  1. 根據 result.info 文件拿到 hprof 文件、sdkVersion 等信息
  2. 分析 Activity 泄漏
  3. 分析重複 Bitmap
  4. 生成 result.json 文件並寫入結果
  5. 輸出重複的 Bitmap 圖像到本地
public final class CLIMain {
    public static void main(String[] args) {
        doAnalyze();
    }

    private static void doAnalyze() throws IOException {
        // 從 result.info 文件中拿到 hprof 文件、sdkVersion 等信息,接着開始分析
        analyzeAndStoreResult(tempHprofFile, sdkVersion, manufacturer, leakedActivityKey, extraInfo);
    }

    private static void analyzeAndStoreResult(...) {
        // 分析 Activity 內存泄漏
        ActivityLeakResult activityLeakResult
                = new ActivityLeakAnalyzer(leakedActivityKey, ).analyze(heapSnapshot);

        // 分析重複 Bitmap
        DuplicatedBitmapResult duplicatedBmpResult
                = new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot);

        // 生成 result.json 文件並寫入結果
        final File resultJsonFile = new File(outputDir, resultJsonName);
        resultJsonPW.println(resultJson.toString());

        // 輸出重複的 Bitmap 圖像
        for (int i = 0; i < duplicatedBmpEntryCount; ++i) {
            final BufferedImage img = BitmapDecoder.getBitmap(...);
            ImageIO.write(img, "png", os);
        }
    }
}
複製代碼

Activity 內存泄漏檢測的關鍵是找到最短引用路徑,原理是:

  1. 根據 result.info 中的 leakedActivityKey 字段獲取 Activity 結點
  2. 使用一個集合,存儲與該 Activity 存在強引用的全部結點
  3. 從這些結點出發,使用寬度優先搜索算法,找到最近的一個 GC Root,GC Root 多是靜態變量、棧幀中的本地變量、JNI 變量等

重複 Bitmap 檢測的原理在上一篇文章有介紹,這裏跳過。

總結

Resource Canary 的實現原理

  1. 註冊 ActivityLifeCycleCallbacks,監聽 onActivityDestroyed 方法,經過弱引用判斷是否出現了內存泄漏,使用後臺線程(MatrixHandlerThread)週期性地檢測
  2. 經過一個「哨兵」對象來確認系統是否進行了 GC
  3. 若發現某個 Activity 沒法被回收,再重複判斷 3 次(0.6.5 版本的代碼默認是 10 次),且要求從該 Activity 被記錄起有 2 個以上的 Activity 被建立才認爲是泄漏(沒發現對應的代碼),以防在判斷時該 Activity 被局部變量持有致使誤判
  4. 不會重複報告同一個 Activity

Resource Canary 的限制

  1. 只能在 Android 4.0 以上的設備運行,由於 ActivityLifeCycleCallbacks 是在 API 14 才加入進來的
  2. 沒法分析 Android 8.0 及以上的設備的重複 Bitmap 狀況,由於 Bitmap 的 mBuffer 字段在 API 26 被移除了

可配置的選項

  1. DumpMode。有 no_dump(報告 Activity 類名)、silence_dump(報告 Activity 類名,回調 ActivityLeakCallback)、auto_dump(生成堆轉儲文件)、manual_dump(發送一個通知) 四種
  2. debug 模式,只有在 debug 模式下,DumpMode 纔會起做用,不然會持續打印日誌
  3. ContentIntent,在 DumpMode 模式爲 manual_dump 時,會生成一個通知,ContentIntent 可指定跳轉的目標 Activity
  4. 應用可見/不可見時監測線程的輪詢間隔,默認分別是 1min、20min
  5. MaxRedetectTimes,只有重複檢測大於等於 MaxRedetectTimes 次以後,若是依然能獲取到 Activity,才認爲出現了內存泄漏

修復內存泄漏

在監測的同時,Resource Canary 使用 ActivityLeakFixer 嘗試修復內存泄漏問題,實現原理是切斷 InputMethodManager、View 和 Activity 的引用

hprof 文件處理

  1. 在 debug 狀態下,且 DumpMode 爲 audo_dump 時,Matrix 纔會在監測到內存泄漏問題後,自動生成一個 hprof 文件
  2. 因爲原文件很大,所以 Matrix 會對該文件進行裁剪優化,並將裁剪後的 hprof 文件和一個 result.info 文件壓縮到一個 zip 包中,result.info 包括 hprof 文件名、sdkVersion、設備廠商、出現內存泄漏的 Activity 類名等信息
  3. 拿到這個 zip 文件,輸入路徑參數,執行 matrix-resource-canary-analyzer 中的 CLIMain 程序,便可獲得一個 result.json 文件,從這個文件能獲取 Activity 的關鍵引用路徑、重複 Bitmap 等信息

CLIMain 的解析步驟

  1. 根據 result.info 文件拿到 hprof 文件、Activity 類名等關鍵信息
  2. 分析 Activity 泄漏
  3. 分析重複 Bitmap
  4. 生成 result.json 文件並寫入結果
  5. 輸出重複的 Bitmap 圖像到本地

最短路徑查找

Activity 內存泄漏檢測的關鍵是找到最短引用路徑,原理是:

  1. 根據 result.info 中的 leakedActivityKey 字段獲取 Activity 結點
  2. 使用一個集合,存儲與該 Activity 存在強引用的全部結點
  3. 從這些結點出發,使用寬度優先搜索算法,找到最近的一個 GC Root,GC Root 多是靜態變量、棧幀中的本地變量、JNI 變量等

重複 Bitmap 的分析原理

把全部未被回收的 Bitmap 的數據 buffer 取出來,而後先對比全部長度爲 1 的 buffer,找出相同的,記錄所屬的 Bitmap 對象;再對比全部長度爲 2 的、長度爲 3 的 buffer……直到把全部 buffer 都比對完,這樣就記錄了全部冗餘的 Bitmap 對象。

內存監測程序執行流程
相關文章
相關標籤/搜索