您當前的位置:首頁 > 攝影

關於Android效能監控Matrix那些事?你知道那些(上)?

作者:由 初壹十五a 發表于 攝影時間:2022-08-25

前兩天錄製了兩節關於

Android

效能監控

Matrix

的影片。

1。面試中問道線上效能監控怎麼辦,

Android

線上監控種種

2。

Matrix

卡頓監控,函式自動埋點監控方案

但是還沒有完全錄制完全。稍後出~今天先文字分析一下關於

Matrix

的種種

文章完整版如下:私信(Matrix)領取

關於Android效能監控Matrix那些事?你知道那些(上)?

關於Android效能監控Matrix那些事?你知道那些(上)?

騰訊Android筆記指南大全:

(影片+文字)騰訊最全面Android高階學習筆記;快來學一波

1。

Matrix

介紹

Matrix

是騰訊微信終端團隊開發的一套應用效能監控系統(

APM

),

GitHub

地址: Tencent -Matrix。

Matrix-android

當前監控範圍包括:應用安裝包大小、幀率變化、啟動耗時、卡頓、慢方法、

SQLite

操作最佳化、檔案讀寫、記憶體洩漏等。整個庫主要由 5 個元件構成:

APK Checker

。針對

APK

安裝包的分析檢測工具,根據一系列設定好的規則,檢測

APK

是否存在

特定的問題,並輸出較為詳細的檢測結果報告,用於分析排查問題以及版本追蹤

Resource Canary

。基於

WeakReference

的特性和

Square Haha

庫開發的

Activity

洩漏和

Bitmap

重複建立檢測工具

Trace Canary

。監控介面流暢性、啟動耗時、頁面切換耗時、慢函式及卡頓等問題

IO Canary

。檢測檔案 IO 問題,包括檔案

IO

監控和

Closeable Leak

監控

SQLite Lint

。按官方最佳實踐自動化檢測

SQLite

語句的使用質量

使用

使用

Matrix

的使用方式很簡單,在

Application

中初始化後啟動即可:

Matrix。Builder builder = new Matrix。Builder(this);

// 新增需要的外掛

builder。plugin(new TracePlugin(。。。));

builder。plugin(new ResourcePlugin(。。。));

builder。plugin(new IOCanaryPlugin(。。。));

builder。plugin(new SQLiteLintPlugin(。。。));

// 初始化

Matrix matrix = Matrix。init(builder。build());

// 啟動

matrix。startAllPlugins();

Matrix

類相當於整個庫的統一對外介面,資源監控、

IO

監控、卡頓監控等功能實現是由其它具體的

Plugin

完成的。

也可以不在

Application

中啟動全部外掛,而是在某個場景中啟動特定的外掛,比如:

public class TestTraceMainActivity extends Activity {

@Override

protected void onCreate(@Nullable Bundle savedInstanceState) {

// 啟動外掛

Plugin plugin = Matrix。with()。getPluginByClass(TracePlugin。class);

if (!plugin。isPluginStarted()) {

plugin。start();

}

}

@Override

protected void onDestroy() {

super。onDestroy();

// 停止外掛

Plugin plugin = Matrix。with()。getPluginByClass(TracePlugin。class);

if (plugin。isPluginStarted()) {

plugin。stop();

}

}

}

每個具體的

Plugin

都會實現

IPlugin

介面:

public interface IPlugin {

Application getApplication();

void init(Application application, PluginListener pluginListener);

void start();

void stop();

void destroy();

String getTag();

// 在應用可見/不可見時回撥

void onForeground(boolean isForeground);

}

可以透過

PluginListener

監聽

Plugin

的生命週期變化,或在

Plugin

上報問題時回撥:

public interface PluginListener {

void onInit(Plugin plugin);

void onStart(Plugin plugin);

void onStop(Plugin plugin);

void onDestroy(Plugin plugin);

void onReportIssue(Issue issue);

}

上報的問題使用實體類

Issue

包裝,

Issue

包含

tag

type

等通用欄位,詳細資訊可以透過

JSON

物件

content

獲取:

public class Issue {

private int type;

private String tag;

private String key;

private JSONObject content;

private Plugin plugin;

}

原始碼簡析

Matrix

Matrix

是一個單例類,在建構函式執行時,

Matrix

內部的所有

Plugin

都會被初始化:

public class Matrix {

private final HashSet plugins;

private Matrix(Application app, PluginListener listener, HashSet plugins) {

this。plugins = plugins;

AppActiveMatrixDelegate。INSTANCE。init(application); // 下面會分析

// 初始化所有 Plugin,並回調 pluginListener

for (Plugin plugin : plugins) {

plugin。init(application, pluginListener);

pluginListener。onInit(plugin);

}

}

}

Plugin

Plugin

是一個抽象類,每次執行

init / start / stop / destroy

等方法時都會更新狀態,並回調

PluginListener

public abstract class Plugin implements IPlugin,

IssuePublisher。OnIssueDetectListener, IAppForeground {

private int status = PLUGIN_CREATE;

@Override

public void init(Application app, PluginListener listener) {

status = PLUGIN_INITED;

AppActiveMatrixDelegate。INSTANCE。addListener(this); // 下面會分析

}

@Override

public void start() {

status = PLUGIN_STARTED;

pluginListener。onStart(this);

}

。。。

}

如果某個具體

Plugin

上報了一個問題,父類

Plugin

還會對該

Issue

填充

tag

type

process

time

等通用欄位,並回調

PluginListener

onReportIssue

方法:

@Override

public void onDetectIssue(Issue issue) {

issue。setPlugin(this);

JSONObject content = issue。getContent();

// 拼接 tag、type、process、time 等通用欄位

content。put(Issue。ISSUE_REPORT_TAG, issue。getTag());

content。put(Issue。ISSUE_REPORT_TYPE, issue。getType());

content。put(Issue。ISSUE_REPORT_PROCESS,

MatrixUtil。getProcessName(application));

content。put(Issue。ISSUE_REPORT_TIME, System。currentTimeMillis());

// 回撥

pluginListener。onReportIssue(issue);

}

AppActiveMatrixDelegate

Matrix

Plugin

都監聽了

AppActiveMatrixDelegate

,它的主要作用是在應用可見/不可見時通知觀察者:

public enum AppActiveMatrixDelegate {

INSTANCE; // 單例

// 觀察者列表

private final Set listeners = new HashSet();

// 應用可見時通知觀察者

private void onDispatchForeground(String visibleScene) {

handler。post(() -> {

isAppForeground = true;

synchronized (listeners) {

for (IAppForeground listener : listeners) {

listener。onForeground(true);

}

}

}

}

// 應用不可見時通知觀察者,邏輯和上面的一樣

private void onDispatchBackground(String visibleScene) {

。。。

}

}

判斷應用是否可見的邏輯是透過

ActivityLifecycleCallbacks

介面實現的:

private final class Controller implements

Application。ActivityLifecycleCallbacks, ComponentCallbacks2 {

@Override

public void onActivityStarted(Activity activity) {

// 應用可見

updateScene(activity);

onDispatchForeground(getVisibleScene());

}

@Override

public void onActivityStopped(Activity activity) {

// 沒有可見的 Activity 了,相當於進入了後臺

if (getTopActivityName() == null) {

onDispatchBackground(getVisibleScene());

}

}

。。。

@Override

public void onTrimMemory(int level) {

// 應用 UI 不可見

if (level == TRIM_MEMORY_UI_HIDDEN && isAppForeground) { // fallback

onDispatchBackground(visibleScene);

}

}

}

總結

Matrix-android

主要包含 5 個元件:

APK Checker

Resource Canary

Trace Canary

IO Canary

SQLite Lint

。其中

APK Checker

獨立執行;其它 4 個模組需要在

Application

中,透過統一對外介面

Matrix

配置完成後執行。每一個模組相當於一個

Plugin

,在執行初始化、啟動、停止、銷燬、報告問題等操作時,都會回撥

PluginListener

,並更新狀態。

每一個

Issue

都有

tag

type

process

time

等 4 個通用欄位。

可以監聽

AppActiveMatrixDelegate

,在應用可見/不可見時,回撥

onForeground

方法,以執行相應的操作。應用可見指的是存在可見的

Activity

,應用不可見指的是沒有可見的

Activity

,或者記憶體不足了,應用的

UI

不可見

2。記憶體洩漏監控及原理介紹

ResourceCanary

介紹

Matrix

的記憶體洩漏監控是由

ResourceCanary

實現的,準確的說,

ResourceCanary

只能實現

Activity

的記憶體洩漏檢測,但在出現

Activity

記憶體洩漏時,可以選擇

dump

一個堆轉儲檔案,透過該檔案,可以分析應用是否存在重複的

Bitmap

使用

ResourceCanary

是基於

WeakReference

特性和

Square Haha

庫開發的

Activity

洩漏和

Bitmap

重複建立檢測工具,使用之前,需要進行如下配置:

Matrix。Builder builder = new Matrix。Builder(this);

// 用於在使用者點選生成的問題通知時,透過這個 Intent 跳轉到指定的 Activity

Intent intent = new Intent();

intent。setClassName(this。getPackageName(),

“com。tencent。mm。ui。matrix。ManualDumpActivity”);

ResourceConfig resourceConfig = new ResourceConfig。Builder()

。dynamicConfig(new DynamicConfigImplDemo()) // 用於動態獲取一些自定義的選項, 不同 Plugin 有不同的選項

。setAutoDumpHprofMode(ResourceConfig。DumpMode。AUTO_DUMP) // 自動生成 Hprof 檔案 //

。setDetectDebuger(true) //matrix test code

。setNotificationContentIntent(intent) // 問題通知

。build();

builder。plugin(new ResourcePlugin(resourceConfig));

// 這個類可用於修復一些記憶體洩漏問題

ResourcePlugin。activityLeakFixer(this);

如果想要在具體的

Activity

中檢測記憶體洩漏,那麼獲取

Plugin

並執行

start

方法(一般在

onCreate

方法中執行)即可:

Plugin plugin = Matrix。with()。getPluginByClass(ResourcePlugin。class);

if (!plugin。isPluginStarted()) {

plugin。start();

}

捕獲到問題後,會上報資訊如下:

{

“tag”: “memory”,

“type”: 0,

“process”: “sample。tencent。matrix”,

“time”: 1590396618440,

“activity”: “sample。tencent。matrix。resource。TestLeakActivity”,

}

如果

DumpMode

AUTO_DUMP

,還會生成一個壓縮檔案,裡面包含一個堆轉儲檔案和一個

result。info

檔案,可以根據

result。info

檔案發現具體是哪一個

Activity

洩漏了:

{

“tag”:“memory”,

“process”:“com。tencent。mm”, “resultZipPath”:“/storage/emulated/0/Android/data/com。tencent。mm/cache/matrix_r esource/dump_result_17400_20170713183615。zip”,

“activity”:“com。tencent。mm。plugin。setting。ui。setting。SettingsUI”,

}

配置

ResourcePlugin

執行之前,需要透過

ResourceConfig

配置,配置選項有:

public static final class Builder {

private DumpMode mDefaultDumpHprofMode = DEFAULT_DUMP_HPROF_MODE;

private IDynamicConfig dynamicConfig;

private Intent mContentIntent;

private boolean mDetectDebugger = false;

}

其中,

ContentIntent

用於傳送通知。

DumpMode

用於控制檢測到問題後的行為,可選值有:

NO_DUMP

,是一個輕量級的模式,會回撥

Plugin

onDetectIssue

方法,但只報告出現記憶體洩漏問題的

Activity

的名稱

SILENCE_DUMP

,和

NO_DUMP

類似,但會回撥

IActivityLeakCallback

MANUAL_DUMP

,用於生成一個通知,點選後跳轉到對應的

Activity

Activity

ContentIntent

指定

AUTO_DUMP

,用於生成堆轉儲檔案

IDynamicConfig

是一個介面,可用於動態獲取一些自定義的選項值:

public interface IDynamicConfig {

String get(String key, String defStr);

int get(String key, int defInt);

long get(String key, long defLong);

boolean get(String key, boolean defBool);

float get(String key, float defFloat);

}

Resource Canary

相關的選項有:

enum ExptEnum {

//resource

clicfg_matrix_resource_detect_interval_millis, // 後臺執行緒輪詢間隔

clicfg_matrix_resource_detect_interval_millis_bg, // 應用不可見時的輪詢間隔

clicfg_matrix_resource_max_detect_times, // 重複檢測多次後才認為出現了記憶體洩漏,避 免誤判

clicfg_matrix_resource_dump_hprof_enable, // 沒見程式碼有用到

}

實現該介面對應的方法,即可透過

ResourceConfig

獲取上述選項的值:

public final class ResourceConfig {

// 後臺執行緒輪詢間隔預設為 1min

private static final long DEFAULT_DETECT_INTERVAL_MILLIS = TimeUnit。MINUTES。toMillis(1);

// 應用不可見時,後臺執行緒輪詢間隔預設為 1min

private static final long DEFAULT_DETECT_INTERVAL_MILLIS_BG = TimeUnit。MINUTES。toMillis(20);

// 預設重複檢測 10 次後,如果依然能獲取到 Activity ,才認為出現了記憶體洩漏

private static final int DEFAULT_MAX_REDETECT_TIMES = 10;

public long getScanIntervalMillis() { 。。。 }

public long getBgScanIntervalMillis() { 。。。 }

public int getMaxRedetectTimes() { 。。。 }

}

可以看到,預設情況下,

Resource Canary

在應用可見(

onForeground

)時每隔 1 分鐘檢測一次,在應用不可見時每隔 20 分鐘檢測一次。對於同一個

Activity

,在重複檢測 10 次後,如果依然能透過弱引用獲取,那麼就認為出現了記憶體洩漏。

原理介紹

這部分內容摘抄自官方文件。

監測階段

在監測階段,對於 4。0 之前的版本,由於沒有

ActivityLifecycleCallbacks

,而使用反射有效能問題,使用

BaseActivity

又存在侵入性的問題,因此,

ResourceCanary

放棄了對 Android 4。0 之前的版本的支援,直接使用

ActivityLifecycleCallbacks

和弱引用來檢測

Activity

的記憶體洩漏。

分析階段在分析階段,由於對

Activity

的強引用鏈很可能不止一條,因此問題的關鍵在於找到最短的引用鏈。比如有如下引用關係:

關於Android效能監控Matrix那些事?你知道那些(上)?

那麼,將

GC Root

Object 1

的引用關係解除即可。對於多條

GC Root

引用鏈的情況,多次檢測即可,這樣至少保證了每次執行

ResourceCanary

模組的耗時穩定在一個可預計的範圍內,不至於在極端情況下耽誤其他流程。

LeakCanary

已實現了上述演算法,但

Matrix

改進了其中的一些問題:

增加一個一定能被回收的“哨兵”物件,用來確認系統確實進行了GC

直接透過

WeakReference。get()

來判斷物件是否已被回收,避免因延遲導致誤判

若發現某個

Activity

無法被回收,再重複判斷 3 次(0。6。5 版本的程式碼預設是 10 次),以防在判斷時該

Activity

被區域性變數持有導致誤判

對已判斷為洩漏的

Activity

,記錄其類名,避免重複提示該

Activity

已洩漏

Hprof

檔案獲取所有冗餘的

Bitmap

物件

對於這個問題,

Android Moniter

已經有完整的實現,原理簡單粗暴:把所有未被回收的

Bitmap

的資料

buffer

取出來,然後先對比所有長度為 1 的

buffer

,找出相同的,記錄所屬的

Bitmap

物件;再對比所有長度為 2 的、長度為 3 的

buffer

……直到把所有

buffer

都比對完,這樣就記錄了所有冗餘的

Bitmap

物件,接著再套用

LeakCanary

獲取引用鏈的邏輯把這些

Bitmap

物件到

GC Root

的最短強引用鏈找出來即可。

效能開銷

在監測階段,

Resource Canary

的週期性輪詢是在後臺執行緒執行的,預設輪詢間隔為 1min,以微信通訊錄、朋友圈介面的幀率作為參考,接入後應用的平均幀率下降了 10 幀左右,開銷並不明顯。但

Dump Hprof

的開銷較大,整個 App 會卡死約 5~15s。分析部分放到了伺服器環境中執行。實際使用時分析一個 200M 左右的

Hprof

平均需要 15s 左右的時間。此部分主要消耗在引用鏈分析上,因為需要廣度優先遍歷完

Hprof

中記錄的全部物件。

3。記憶體洩漏監控原始碼分析

修復記憶體洩漏

在開始監測

Activity

記憶體洩漏之前,

Resource Canary

首先會嘗試修復可能的記憶體洩漏問題,它是透過監聽

ActivityLifeCycleCallbacks

實現的,在

Activity

回撥

onDestroy

時,它會嘗試解除

Activity

InputMethodManager

View

之間的引用關係:

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

,因此,我們需要把每一個可能的引用關係解除掉:

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

來完成。

ActivityRefWatcher

主要的三個方法:

start

stop

destroy

分別用於啟動監聽執行緒、停止監聽執行緒、結束監聽。以

start

為例:

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

// 用於記錄 Activity 資訊

private final ConcurrentLinkedQueue

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

包含資訊如下:

public class DestroyedActivityInfo {

public final String mKey; // 根據 uuid 生成

public final String mActivityName; // 類名

public final WeakReference mActivityRef; // 弱引用

public int mDetectedCount = 0; // 重複檢測次數,預設檢測 10 次後,依然能透過弱引用獲 取,才認為發生了記憶體洩漏

}

啟動監聽執行緒

執行緒啟動後,應用可見時,預設每隔 1min(透過

IDynamicConfig

指定) 將輪詢任務傳送到預設的後臺執行緒(

MatrixHandlerThread

)執行:

// 自定義的執行緒切換機制,用於將指定的任務延時傳送到主執行緒/後臺執行緒執行

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

,如果沒有,它不會往下執行:

private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {

@Override

public Status execute() {

// 建立指向一個臨時物件的弱引用

final WeakReference 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 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/du

mp_*。hprof” starting。。。

裁剪壓縮後在

/sdcard/data/[package name]/matrix_resource

資料夾下會生成一個

zip

檔案,比如:

/storage/emulated/0/Android/data/sample。tencent。matrix/cache/matrix_resource/dum p_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。TestLea

kActivity_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 步:

根據

result。info

檔案拿到

hprof

檔案、

sdkVersion

等資訊

分析

Activity

洩漏

分析重複

Bitmap

生成

result。json

檔案並寫入結果

輸出重複的

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

記憶體洩漏檢測的關鍵是找到最短引用路徑,原理是:

根據

result。info

中的

leakedActivityKey

欄位獲取

Activity

結點

使用一個集合,儲存與該

Activity

存在強引用的所有結點

從這些結點出發,使用寬度優先搜尋演算法,找到最近的一個

GC Root

GC Root

可能是靜態變數、棧幀中的本地變數、JNI 變數等重複

Bitmap

檢測的原理在上一篇文章有介紹,這裡跳過。

總結

Resource Canary

的實現原理

註冊

ActivityLifeCycleCallbacks

,監聽

onActivityDestroyed

方法,透過弱引用判斷是否出現了記憶體洩漏,使用後臺執行緒(

MatrixHandlerThread

)週期性地檢測

透過一個“哨兵”物件來確認系統是否進行了

GC

若發現某個

Activity

無法被回收,再重複判斷 3 次(0。6。5 版本的程式碼預設是 10 次),且要求從該

Activity

被記錄起有 2 個以上的

Activity

被建立才認為是洩漏(沒發現對應的程式碼),以防在判斷時該

Activity

被區域性變數持有導致誤判

不會重複報告同一個

Activity

Resource Canary

的限制

只能在 Android 4。0 以上的裝置執行,因為

ActivityLifeCycleCallbacks

是在 API 14 才加入進來的

無法分析 Android 8。0 及以上的裝置的重複

Bitmap

情況,因為

Bitmap

mBuffer

欄位在 API26 被移除了。

可配置的選項

DumpMode

。有

no_dump

(報告

Activity

類名)、

silence_dump

(報告

Activity

類名,回撥

ActivityLeakCallback

)、

auto_dump

(生成堆轉儲檔案)、

manual_dump

(傳送一個通知) 四 種

debug

模式,只有在

debug

模式下,

DumpMode

才會起作用,否則會持續列印日誌

ContentIntent

,在

DumpMode

模式為

manual_dump

時,會生成一個通知,

ContentIntent

可指定跳轉的目標

Activity

應用可見/不可見時監測執行緒的輪詢間隔,預設分別是 1min、20min

MaxRedetectTimes

,只有重複檢測大於等於

MaxRedetectTimes

次之後,如果依然能獲取到

Activity

,才認為出現了記憶體洩漏

修復記憶體洩漏

在監測的同時,

Resource Canary

使用

ActivityLeakFixer

嘗試修復記憶體洩漏問題,實現原理是切斷

InputMethodManager

View

Activity

的引用

hprof

檔案處理

debug

狀態下,且

DumpMode

audo_dump

時,

Matrix

才會在監測到記憶體洩漏問題後,自動生成一個

hprof

檔案

由於原檔案很大,因此

Matrix

會對該檔案進行裁剪最佳化,並將裁剪後的

hprof

檔案和一個

result。info

檔案壓縮到一個

zip

包中,

result。info

包括

hprof

檔名、

sdkVersion

、裝置廠商、出現記憶體洩漏的

Activity

類名等資訊

拿到這個

zip

檔案,輸入路徑引數,執行

matrix-resource-canary-analyzer

中的

CLIMain

程式,

即可得到一個

result。json

檔案,從這個檔案能獲取

Activity

的關鍵引用路徑、重複

Bitmap

等資訊

CLIMain

的解析步驟

根據

result。info

檔案拿到

hprof

檔案、

Activity

類名等關鍵資訊

分析

Activity

洩漏

分析重複

Bitmap

生成

result。json

檔案並寫入結果

輸出重複的

Bitmap

影象到本地

最短路徑查詢

Activity

記憶體洩漏檢測的關鍵是找到最短引用路徑,原理是:

根據

result。info

中的

leakedActivityKey

欄位獲取

Activity

結點

使用一個集合,儲存與該

Activity

存在強引用的所有結點

從這些結點出發,使用寬度優先搜尋演算法,找到最近的一個

GC Root

GC Root

可能是靜態變數、棧幀中的本地變數、

JNI

變數等

重複

Bitmap

的分析原理

把所有未被回收的

Bitmap

的資料

buffer

取出來,然後先對比所有長度為 1 的

buffer

,找出相同的,記錄所屬的

Bitmap

物件;再對比所有長度為 2 的、長度為 3 的

buffer

……直到把所有

buffer

都比對完,這樣就記錄了所有冗餘的

Bitmap

物件。

關於Android效能監控Matrix那些事?你知道那些(上)?

文字太多,下篇分析:

4。

Hprof

檔案分析

5。卡頓監控

6。卡頓監控原始碼解析

7。插樁

8。資源最佳化

9。I/O監控及原理解析

文章完整版如下:私信(Matrix)領取

關於Android效能監控Matrix那些事?你知道那些(上)?

關於Android效能監控Matrix那些事?你知道那些(上)?

騰訊Android筆記指南大全:

(影片+文字)騰訊最全面Android高階學習筆記;快來學一波

標簽: activity  void  FINAL  NEW  Matrix