在開始監測 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(); } } } 複製代碼
其中 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,它是一個內部類,代碼很長,咱們一點一點分析。優化
首先,在上一篇文章關於原理的部分介紹過,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(); } 複製代碼
接着,遍歷全部 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); 複製代碼
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 步:
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 內存泄漏檢測的關鍵是找到最短引用路徑,原理是:
重複 Bitmap 檢測的原理在上一篇文章有介紹,這裏跳過。
在監測的同時,Resource Canary 使用 ActivityLeakFixer 嘗試修復內存泄漏問題,實現原理是切斷 InputMethodManager、View 和 Activity 的引用
Activity 內存泄漏檢測的關鍵是找到最短引用路徑,原理是:
把全部未被回收的 Bitmap 的數據 buffer 取出來,而後先對比全部長度爲 1 的 buffer,找出相同的,記錄所屬的 Bitmap 對象;再對比全部長度爲 2 的、長度爲 3 的 buffer……直到把全部 buffer 都比對完,這樣就記錄了全部冗餘的 Bitmap 對象。