關於Android效能監控Matrix那些事?你知道那些(上)?
前兩天錄製了兩節關於
Android
效能監控
Matrix
的影片。
1。面試中問道線上效能監控怎麼辦,
Android
線上監控種種
2。
Matrix
卡頓監控,函式自動埋點監控方案
但是還沒有完全錄制完全。稍後出~今天先文字分析一下關於
Matrix
的種種
文章完整版如下:私信(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
private Matrix(Application app, PluginListener listener, HashSet
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
// 應用可見時通知觀察者
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
的強引用鏈很可能不止一條,因此問題的關鍵在於找到最短的引用鏈。比如有如下引用關係:
那麼,將
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
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
// 嘗試觸發 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
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
物件。
文字太多,下篇分析:
4。
Hprof
檔案分析
5。卡頓監控
6。卡頓監控原始碼解析
7。插樁
8。資源最佳化
9。I/O監控及原理解析
文章完整版如下:私信(Matrix)領取
騰訊Android筆記指南大全:
(影片+文字)騰訊最全面Android高階學習筆記;快來學一波
上一篇:相機鏡頭用什麼清理?