APM 全稱 Application Performance Management & Monitoring (應用性能管理/監控)html
性能問題是致使 App 用戶流失的罪魁禍首之一,若是用戶在使用咱們 App 的時候遇到諸如頁面卡頓、響應速度慢、發熱嚴重、流量電量消耗大等問題的時候,極可能就會卸載掉咱們的 App。這也是咱們在目前工做中面臨的巨大挑戰之一,尤爲是低端機型。java
商業化的APM平臺:著名的 NewRelic,還有國內的聽雲、OneAPM 、阿里百川-碼力APM的SDK、百度的APM收費產品等等。linux
報名連接android
若是對APM感興趣,想要進一步跟着我一塊兒學下去,歡迎你們點擊APM訓練營免費參與到學習當中來,突破本身的技術瓶頸,一塊兒加油!c++
那麼移動端須要作的事情就是:git
那咱們到底應該怎麼作?必定要學會看開源的東西。讓咱們先看看大廠的開源怎麼作的?咱們在本身造輪子,完成本身的APM採集框架。github
你會發現自定義Gradle插件技術、ASM技術、打包流程Hook、Android打包流程等。那思考一下,爲何你們作的主要的流程都是同樣的,不同的是具體的實現細節,好比如何插樁採集到頁面幀率、流量、耗電量、GC log等等。面試
ArgusAPM性能監控平臺介紹&SDK開源-卜雲濤.pdf編程
咱們先簡單來看下在matrix中,如何利用Java Hook和 Native Hook完成IO 磁盤性能的監控?json
Java Hook的hook點是系統類CloseGuard
,hook的方式是使用動態代理。
private boolean tryHook() {
try {
Class<?> closeGuardCls = Class.forName("dalvik.system.CloseGuard");
Class<?> closeGuardReporterCls = Class.forName("dalvik.system.CloseGuard$Reporter");
Method methodGetReporter = closeGuardCls.getDeclaredMethod("getReporter");
Method methodSetReporter = closeGuardCls.getDeclaredMethod("setReporter", closeGuardReporterCls);
Method methodSetEnabled = closeGuardCls.getDeclaredMethod("setEnabled", boolean.class);
sOriginalReporter = methodGetReporter.invoke(null);
methodSetEnabled.invoke(null, true);
// open matrix close guard also
MatrixCloseGuard.setEnabled(true);
ClassLoader classLoader = closeGuardReporterCls.getClassLoader();
if (classLoader == null) {
return false;
}
methodSetReporter.invoke(null, Proxy.newProxyInstance(classLoader,
new Class<?>[]{closeGuardReporterCls},
new IOCloseLeakDetector(issueListener, sOriginalReporter)));
return true;
} catch (Throwable e) {
MatrixLog.e(TAG, "tryHook exp=%s", e);
}
return false;
}
複製代碼
這裏的CloseGuard有啥用?爲何騰訊的人要hook這個。這個在後續的分線中咱們在來詳細的說。若是要解決這個疑問,作好的辦法就是看源碼。(==系統埋點方式,監控系統資源的異常回收==)
Native Hook是採用PLT(GOT) Hook的方式hook了系統so中的IO相關的open
、read
、write
、close
方法。在代理了這些系統方法後,Matrix作了一些邏輯上的細分,從而檢測出不一樣的IO Issue。
JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
__android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
const char* so_name = TARGET_MODULES[i];
__android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
//打開so文件,並在內存中映射成ELF文件格式
loaded_soinfo* soinfo = elfhook_open(so_name);
if (!soinfo) {
__android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
continue;
}
//替換open函數
elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
if (is_libjavacore) {
if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
//http://refspecs.linux-foundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/libc---read-chk-1.html 相似於read()
if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
elfhook_close(soinfo);
return false;
}
}
if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
elfhook_close(soinfo);
return false;
}
}
}
elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
elfhook_close(soinfo);
}
return true;
}
複製代碼
關於transform api:
咱們編譯Android項目時,若是咱們想拿到編譯時產生的Class文件,並在生成Dex以前作一些處理,咱們能夠經過編寫一個Transform
來接收這些輸入(編譯產生的Class文件),並向已經產生的輸入中添加一些東西。
如何使用的?
Transform
//MyCustomPlgin.groovy
public class MyCustomPlgin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().findByType(BaseExtension.class)
.registerTransform(new MyCustomTransform());
}
}
複製代碼
project.extensions.findByType(BaseExtension.class).registerTransform(new MyCustomTransform()); //在build.gradle中直接寫
複製代碼
MatrixTraceTransform 利用編譯期字節碼插樁技術,優化了移動端的FPS、卡頓、啓動的檢測手段。在打包過程當中,hook生成Dex的Task任務,添加方法插樁的邏輯。咱們的hook點是在Proguard以後,Class已經被混淆了,因此須要考慮類混淆的問題。
MatrixTraceTransform
主要邏輯在transform
方法中:
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
long start = System.currentTimeMillis()
//是否增量編譯
final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental()
//transform的結果,重定向輸出到這個目錄
final File rootOutput = new File(project.matrix.output, "classes/${getName()}/")
if (!rootOutput.exists()) {
rootOutput.mkdirs()
}
final TraceBuildConfig traceConfig = initConfig()
Log.i("Matrix." + getName(), "[transform] isIncremental:%s rootOutput:%s", isIncremental, rootOutput.getAbsolutePath())
//獲取Class混淆的mapping信息,存儲到mappingCollector中
final MappingCollector mappingCollector = new MappingCollector()
File mappingFile = new File(traceConfig.getMappingPath());
if (mappingFile.exists() && mappingFile.isFile()) {
MappingReader mappingReader = new MappingReader(mappingFile);
mappingReader.read(mappingCollector)
}
Map<File, File> jarInputMap = new HashMap<>()
Map<File, File> scrInputMap = new HashMap<>()
transformInvocation.inputs.each { TransformInput input ->
input.directoryInputs.each { DirectoryInput dirInput ->
//收集、重定向目錄中的class
collectAndIdentifyDir(scrInputMap, dirInput, rootOutput, isIncremental)
}
input.jarInputs.each { JarInput jarInput ->
if (jarInput.getStatus() != Status.REMOVED) {
//收集、重定向jar包中的class
collectAndIdentifyJar(jarInputMap, scrInputMap, jarInput, rootOutput, isIncremental)
}
}
}
//收集須要插樁的方法信息,每一個插樁信息封裝成TraceMethod對象
MethodCollector methodCollector = new MethodCollector(traceConfig, mappingCollector)
HashMap<String, TraceMethod> collectedMethodMap = methodCollector.collect(scrInputMap.keySet().toList(), jarInputMap.keySet().toList())
//執行插樁邏輯,在須要插樁方法的入口、出口添加MethodBeat的i/o邏輯
MethodTracer methodTracer = new MethodTracer(traceConfig, collectedMethodMap, methodCollector.getCollectedClassExtendMap())
methodTracer.trace(scrInputMap, jarInputMap)
//執行原transform的邏輯;默認transformClassesWithDexBuilderForDebug這個task會將Class轉換成Dex
origTransform.transform(transformInvocation)
Log.i("Matrix." + getName(), "[transform] cost time: %dms", System.currentTimeMillis() - start)
}
複製代碼
看到這裏了,咱們是否是應該總結下APM的核心技術是什麼?
大廠面試之一:APM的核心技術是什麼?作過自研的APM嗎?
APM核心原理一句話總結:依據打包原理,在 class 轉換爲 dex 的過程當中,調用 gradle transform api 遍歷 class 文件,藉助 Javassist、ASM 等框架修改字節碼,插入咱們本身的代碼實現性能數據的統計,這個過程是在編譯器間完成
你掌握了APM的核心原理,也能夠作Android的無痕埋點了,本質是同樣的,不同的是Hook的地方不同。
你本身的定位?有機會在談一談我本身的心得。
App基礎性能指標集中爲8類:網絡性能、崩潰、啓動加載、內存、圖片、頁面渲染、IM和VoIP(業務相關性和你的APP相關)、用戶行爲監控,基礎維度包括App、系統平臺、App版本和時間維度。
網絡性能
網絡服務成功率,平均耗時和訪問量、上下行的速率監控。訪問的連接、耗時等等。思考怎麼結合okhttp來作?
崩潰
崩潰數據的採集和分析,相似於Bugly平臺的功能
啓動加載
App的啓動咱們作了大的力氣進行了優化,多線程等等, Spark的有向無環圖(DAG)來處理業務的依賴性。對App的冷啓動時長、Android安裝後首次啓動時長和Android Bundle(atlas框架)啓動加載時長進行監控。
內存
四大監測目標:內存峯值、內存均值、內存抖動、內存泄露。
IM和VoIP等業務指標
這兩項都屬於業務型技術指標的監控,例如對各種IM消息到達率和VoIP通話的成功率、平均耗時和請求量進行監控。這裏須要根據本身的APP的業務進行鍼對性的梳理。
用戶行爲監控
用於App統計用戶行爲,實際上就是監控全部事件並把事件發送到服務上去。這在之前是埋點作的事情,如今也規整成APM須要作的事情,好比用戶的訪問路徑,相似於PC時代的PV,UV等概念。
圖片
資源文件的監測,好比Bitmap冗餘處理。haha庫處理,索引值。
頁面渲染
界面流暢性監測、FPS的監測、慢函數監測、卡頓監測、文件IO開銷監測等致使頁面渲染的各類問題。
Matrix.Builder builder = new Matrix.Builder(application); // build matrix
builder.patchListener(new TestPluginListener(this)); // add general pluginListener
DynamicConfigImplDemo dynamicConfig = new DynamicConfigImplDemo(); // dynamic config
// init plugin
IOCanaryPlugin ioCanaryPlugin = new IOCanaryPlugin(new IOConfig.Builder()
.dynamicConfig(dynamicConfig)
.build());
//add to matrix
builder.plugin(ioCanaryPlugin);
//init matrix
Matrix.init(builder.build());
// start plugin
ioCanaryPlugin.start();
複製代碼
整理的結構以下:
核心功能:
Resource Canary:
Activity 泄漏
Bitmap 冗餘
Trace Canary
界面流暢性
啓動耗時
頁面切換耗時
慢函數
卡頓
SQLite Lint: 按官方最佳實踐自動化檢測 SQLite 語句的使用質量
IO Canary: 檢測文件 IO 問題
文件 IO 監控
Closeable Leak 監控
複製代碼
總體架構分析:
matrix-android-lib
plugin核心接口
public interface IPlugin {
/** * 用於標識當前的監控,至關於名稱索引(也可用classname直接索引) */
String getTag();
/** * 在Matrix對象構建時被調用 */
void init(Application application, PluginListener pluginListener);
/** * 對activity先後臺轉換的感知能力 */
void onForeground(boolean isForeground);
void start();
void stop();
void destroy();
}
public interface PluginListener {
void onInit(Plugin plugin);
void onStart(Plugin plugin);
void onStop(Plugin plugin);
void onDestroy(Plugin plugin);
void onReportIssue(Issue issue);
}
複製代碼
Matrix對外接口
public class Matrix {
private static final String TAG = "Matrix.Matrix";
/********************************** 單例實現 **********************/
private static volatile Matrix sInstance;
public static Matrix init(Matrix matrix) {
if (matrix == null) {
throw new RuntimeException("Matrix init, Matrix should not be null.");
}
synchronized (Matrix.class) {
if (sInstance == null) {
sInstance = matrix;
} else {
MatrixLog.e(TAG, "Matrix instance is already set. this invoking will be ignored");
}
}
return sInstance;
}
public static boolean isInstalled() {
return sInstance != null;
}
public static Matrix with() {
if (sInstance == null) {
throw new RuntimeException("you must init Matrix sdk first");
}
return sInstance;
}
/**************************** 構造函數 **********************/
private final Application application;
private final HashSet<Plugin> plugins;
private final PluginListener pluginListener;
private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) {
this.application = app;
this.pluginListener = listener;
this.plugins = plugins;
for (Plugin plugin : plugins) {
plugin.init(application, pluginListener);
pluginListener.onInit(plugin);
}
}
/**************************** 控制能力 **********************/
public void startAllPlugins() {
for (Plugin plugin : plugins) {
plugin.start();
}
}
public void stopAllPlugins() {
for (Plugin plugin : plugins) {
plugin.stop();
}
}
public void destroyAllPlugins() {
for (Plugin plugin : plugins) {
plugin.destroy();
}
}
/**************************** get | set **********************/
public Plugin getPluginByTag(String tag) {
for (Plugin plugin : plugins) {
if (plugin.getTag().equals(tag)) {
return plugin;
}
}
return null;
}
public <T extends Plugin> T getPluginByClass(Class<T> pluginClass) {
String className = pluginClass.getName();
for (Plugin plugin : plugins) {
if (plugin.getClass().getName().equals(className)) {
return (T) plugin;
}
}
return null;
}
/**************************** 其餘 **********************/
public static void setLogIml(MatrixLog.MatrixLogImp imp) {
MatrixLog.setMatrixLogImp(imp);
}
}
複製代碼
Utils 輔助功能
IssuePublisher被監控事件觀察者
Issue 被監控事件 type:類型,用於區分同一個tag不一樣類型的上報 tag: 該上報對應的tag stack:該上報對應的堆棧 process:該上報對應的進程名 time:issue 發生的時間
IssuePublisher 觀察者模式
持有一個 發佈Listener(其實現每每是上文的 Plugin)
持有一個 已發佈信息的Map,在一次運行時長內,避免針對同一事件的重複發佈
通常而言,某種監控的監控探測器每每繼承該類,並在檢測到事件發生時,調用publishIssue(Issue)—>IssuePublisher.OnIssueDetectListener接口的onDetectIssue方法—>最終觸發PluginListener#onReportIssue
IO Canary:核心的做用是檢測文件 IO 問題,包括:文件 IO 監控和 Closeable Leak 監控。要想理解IO的監測和看懂開源的代碼,最重要的基礎就是掌握Native和Java層面的Hook。
Java層面的hook主要是基於反射技術,你們都比較熟悉了,那咱們來聊一聊Native層面的Hook。在JVM層面,Android使用Android PLT (Procedure Linkage Table)Hook和Inline Hook、ptrace三種主流的技術。
Matrix採用PLT的技術來實現SO文件API的Hook。
ELF: en.wikipedia.org/wiki/Execut…
refspecs.linuxbase.org/elf/elf.pdf
ELF文件的三種形式:
咱們思考下C的程序是須要進行編譯和連接再到最後的運行的。那麼ELF文件從這個參與程序運行的角度也是分爲2種視圖的。
ELF header 位於文件的最開始處,描述整個文件的組織結構。Program Header Table 告訴系統如何建立進程鏡像,在執行程序時必須存在,在 relocatable files 中則不須要。每一個 program header 分別描述一個 segment,包括 segment 在文件和內存中的大小及地址等等。執行視圖中的 segment 實際上是由不少個 section 組成。在一個進程鏡像中一般具備 text segment 和 data segment 等等。
關於重定位的做用和概念
重定位就是把符號引用與符號定義連接起來的過程,這也是 android linker 的主要工做之一。當程序中調用一個函數時,相關的 call 指令必須在執行期將控制流轉到正確的目標地址。因此,so 文件中必須包含一些重定位相關的信息,linker 據此完成重定位的工做。
android.googlesource.com/platform/bi…
符號表表項的結構爲elf32_sym:
typedef struct elf32_sym {
Elf32_Word st_name; /* 名稱 – index into string table */
Elf32_Addr st_value; /* 偏移地址 */
Elf32_Word st_size; /* 符號長度( 例如,函數的長度) */
unsigned char st_info; /* 類型和綁定類型 */
unsigned char st_other; /* 未定義 */
Elf32_Half st_shndx; /* section header的索引號,表示位於哪一個section中 */
} Elf32_Sym;
複製代碼
重定位核心代碼:
androidxref.com/8.0.0_r4/xr… (具體的重定位類型定義和計算方法能夠參考elf說明文檔的 4.6.1.2 小節)
Android PLT Hook的基本原理
Linux在執行動態連接的ELF的時候,爲了優化性能使用了一個叫延時綁定的策略。當在動態連接的ELF程序裏調用共享庫的函數時,第一次調用時先去查找PLT表中相應的項目,而PLT表中再跳躍到GOT表中但願獲得該函數的實際地址,但這時GOT表中指向的是PLT中那條跳躍指令下面的代碼,最終會執行_dl_runtime_resolve()
並執行目標函數。所以,PLT Hook經過直接修改GOT表,使得在調用該共享庫的函數時跳轉到的是用戶自定義的Hook功能代碼。
IO 監控流程:
JNIEXPORT jboolean JNICALL Java_com_tencent_matrix_iocanary_core_IOCanaryJniBridge_doHook(JNIEnv *env, jclass type) {
__android_log_print(ANDROID_LOG_INFO, kTag, "doHook");
for (int i = 0; i < TARGET_MODULE_COUNT; ++i) {
const char* so_name = TARGET_MODULES[i];
__android_log_print(ANDROID_LOG_INFO, kTag, "try to hook function in %s.", so_name);
loaded_soinfo* soinfo = elfhook_open(so_name);
if (!soinfo) {
__android_log_print(ANDROID_LOG_WARN, kTag, "Failure to open %s, try next.", so_name);
continue;
}
//hook OS
elfhook_replace(soinfo, "open", (void*)ProxyOpen, (void**)&original_open);
elfhook_replace(soinfo, "open64", (void*)ProxyOpen64, (void**)&original_open64);
bool is_libjavacore = (strstr(so_name, "libjavacore.so") != nullptr);
if (is_libjavacore) {
if (!elfhook_replace(soinfo, "read", (void*)ProxyRead, (void**)&original_read)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook read failed, try __read_chk");
if (!elfhook_replace(soinfo, "__read_chk", (void*)ProxyRead, (void**)&original_read)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __read_chk");
elfhook_close(soinfo);
return false;
}
}
//hook OS
if (!elfhook_replace(soinfo, "write", (void*)ProxyWrite, (void**)&original_write)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook write failed, try __write_chk");
if (!elfhook_replace(soinfo, "__write_chk", (void*)ProxyWrite, (void**)&original_write)) {
__android_log_print(ANDROID_LOG_WARN, kTag, "doHook hook failed: __write_chk");
elfhook_close(soinfo);
return false;
}
}
}
//hook OS
elfhook_replace(soinfo, "close", (void*)ProxyClose, (void**)&original_close);
elfhook_close(soinfo);
}
return true;
}
複製代碼
hook的替換核心代碼:(本質上就是置指針替換)
看看代理方法:
很明顯騰訊的人沒有考慮到自線程的問題。這裏能夠優化的。具體的其餘部分的細節請參見源碼。
設計目的:
爲了解決線上監測和後臺分析,Matrix的ResourceCanary最終決定將監測步驟和分析步驟拆成兩個獨立的工具,以知足設計目標。
ResourcePlugin
ResourcePlugin
是該模塊的入口,負責註冊Android生命週期的監聽以及配置部分參數和接口回調。
ActivityRefWatcher ActivityRefWatcher負責的任務有彈出Dump內存的Dialog、Dump內存數據、讀取內存數據裁剪Hprof文件、生成包含裁剪後的Hprof以及泄漏的Activity的信息(進程號、Activity名、時間等)、通知主線程完成內存信息的備份並關閉Dialog。
咱們看下最爲核心的內存泄漏監測代碼:
//ActivityRefWatcher
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {
private int mAppStatusCounter = 0;
private int mUIConfigChangeCounter = 0;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
mCurrentCreatedActivityCount.incrementAndGet();
}
@Override
public void onActivityStarted(Activity activity) {
if (mAppStatusCounter <= 0) {
MatrixLog.i(TAG, "we are in foreground, start watcher task.");
mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
if (mUIConfigChangeCounter < 0) {
++mUIConfigChangeCounter;
} else {
++mAppStatusCounter;
}
}
@Override
public void onActivityStopped(Activity activity) {
if (activity.isChangingConfigurations()) {
--mUIConfigChangeCounter;
} else {
--mAppStatusCounter;
if (mAppStatusCounter <= 0) {
MatrixLog.i(TAG, "we are in background, stop watcher task.");
mDetectExecutor.clearTasks();
}
}
}
@Override
public void onActivityDestroyed(Activity activity) {
//當activity銷燬的時候開始。。。
pushDestroyedActivityInfo(activity);
synchronized (mDestroyedActivityInfos) {
mDestroyedActivityInfos.notifyAll();
}
}
};
複製代碼
private void pushDestroyedActivityInfo(Activity activity) {
final String activityName = activity.getClass().getName();
//該Activity確認存在泄漏,且已經上報
if (isPublished(activityName)) {
MatrixLog.d(TAG, "activity leak with name %s had published, just ignore", activityName);
return;
}
final UUID uuid = UUID.randomUUID();
final StringBuilder keyBuilder = new StringBuilder();
//生成Activity實例的惟一標識
keyBuilder.append(ACTIVITY_REFKEY_PREFIX).append(activityName)
.append('_').append(Long.toHexString(uuid.getMostSignificantBits())).append(Long.toHexString(uuid.getLeastSignificantBits()));
final String key = keyBuilder.toString();
//構造一個數據結構,表示一個已被destroy的Activity
final DestroyedActivityInfo destroyedActivityInfo
= new DestroyedActivityInfo(key, activity, activityName, mCurrentCreatedActivityCount.get());
//放入ConcurrentLinkedQueue數據結構中,用於後續的檢查
mDestroyedActivityInfos.add(destroyedActivityInfo);
}
複製代碼
內存泄漏的核心代碼:
private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {
@Override
public Status execute() {
// If destroyed activity list is empty, just wait to save power.
while (mDestroyedActivityInfos.isEmpty()) {
synchronized (mDestroyedActivityInfos) {
try {
mDestroyedActivityInfos.wait();
} catch (Throwable ignored) {
// Ignored.
}
}
}
// Fake leaks will be generated when debugger is attached.
//Debug調試模式,檢測可能失效,直接return
if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
return Status.RETRY;
}
//建立一個對象的弱引用
final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
triggerGc();
//系統未執行GC,直接return
if (sentinelRef.get() != null) {
// System ignored our gc request, we will retry later.
MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
return Status.RETRY;
}
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
while (infoIt.hasNext()) {
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
//該實例對應的Activity已被標泄漏,跳過該實例
if (isPublished(destroyedActivityInfo.mActivityName)) {
MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
infoIt.remove();
continue;
}
//若不能經過弱引用獲取到Activity實例,表示已被回收,跳過該實例
if (destroyedActivityInfo.mActivityRef.get() == null) {
// The activity was recycled by a gc triggered outside.
MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
infoIt.remove();
continue;
}
//該Activity實例 檢測到泄漏的次數+1
++destroyedActivityInfo.mDetectedCount;
//當前顯示的Activity實例與泄漏的Activity實例相差幾個Activity跳轉
long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount;
//若Activity實例 檢測到泄漏的次數未達到閾值,或者泄漏的Activity與當前顯示的Activity很靠近,可認爲是一種容錯手段(實際應用中有這種場景),跳過該實例
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
|| (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) {
// Although the sentinel tell us the activity should have been recycled,
// system may still ignore it, so try again until we reach max retry times.
MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"
+ "exists in %s times detection with %s created activities during destroy, wait for next detection to confirm.",
destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount, createdActivityCountFromDestroy);
continue;
}
MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance.", destroyedActivityInfo.mKey);
if (mHeapDumper != null) {
final File hprofFile = mHeapDumper.dumpHeap();
if (hprofFile != null) {
markPublished(destroyedActivityInfo.mActivityName);
final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
mHeapDumpHandler.process(heapDump);
infoIt.remove();
} else {
MatrixLog.i(TAG, "heap dump for further analyzing activity with key [%s] was failed, just ignore.",
destroyedActivityInfo.mKey);
infoIt.remove();
}
} else {
// Lightweight mode, just report leaked activity name.
MatrixLog.i(TAG, "lightweight mode, just report leaked activity name.");
markPublished(destroyedActivityInfo.mActivityName);
if (mResourcePlugin != null) {
final JSONObject resultJson = new JSONObject();
try {
resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, destroyedActivityInfo.mActivityName);
} catch (JSONException e) {
MatrixLog.printErrStackTrace(TAG, e, "unexpected exception.");
}
mResourcePlugin.onDetectIssue(new Issue(resultJson));
}
}
}
return Status.RETRY;
}
};
複製代碼
RetryableTaskExecutor
RetryableTaskExecutor
中包含了兩個Handler對象,一個mBackgroundHandler
和mMainHandler
,分別給主線程和後臺的線程提交任務。默認重試次數是3。
AndroidHeapDumper
AndroidHeapDumper
這個其實就是封裝了android.os.Debug
的接口的類。主要是用系統提供的類android.os.Debug
Dump內存信息到本地,android.os.Debug
會在本地生成一個Hprof文件,也是Matrix須要分析和裁剪的原始文件。
注意:通常Dump一次要5s~15s之間,線上建議不要使用,有必定的風險。
Dump的時候,AndroidHeapDumper
會展現一個Dialog提示當前正在Dump中,Dump完畢就會將Dialog關閉。
Debug.dumpHprofData(hprofFile.getAbsolutePath());
複製代碼
Trace Canary: 用於監控界面流暢性、啓動耗時、頁面切換耗時、慢函數及卡頓等問題。(思考一下,技術上怎麼實現)
入口函數探針分析:
public class TracePlugin extends Plugin {
private static final String TAG = "Matrix.TracePlugin";
private final TraceConfig traceConfig;
private EvilMethodTracer evilMethodTracer;//慢函數
private StartupTracer startupTracer; //啓動監測
private FrameTracer frameTracer; //fps
private AnrTracer anrTracer; //anr
public TracePlugin(TraceConfig config) {
this.traceConfig = config;
}
...
複製代碼
【關鍵知識點1】: MessageQueue中的IdleHandler接口有什麼用?
在Android中,咱們能夠處理Message,這個Message咱們能夠當即執行也能夠delay 必定時間執行。Handler線程在執行完全部的Message消息,它會wait,進行阻塞,直到有新的Message到達。若是這樣子,那麼這個線程也太浪費了。MessageQueue提供了另外一類消息,IdleHandler。也就是說當咱們的MessageQueue中的消息被處理完後,就會觸發一次或者屢次回調消息。
應用場景:一、好比主線程在開始加載頁面完成後,若是線程空閒就提早加載些二級頁面的內容。
二、消息觸發器 例如在APM中的做用
三、優化Activity的啓動時間,在Resume中是否是能夠增長idle的監聽
Looper.myQueue().addIdleHandler(() -> {
initializeData();
return false;
});
複製代碼
源碼分析:
Message next(){
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked = true;
continue;
}
if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}
// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler
boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}
//根據IdleHandler中的回掉方法來判斷是否移除
if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}
// Reset the idle handler count to 0 so we do not run them again.
pendingIdleHandlerCount = 0;
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;
}
}
複製代碼
LooperMonitor類監測卡頓問題:
發生在Android主線程的每16ms重繪操做依賴於Main Looper中消息的發送和獲取。若是App一切運行正常,無卡頓無丟幀現象發生,那麼開發者的代碼在主線程Looper消息隊列中發送和接收消息的時間會很短,理想狀況是16ms,這是也是Android系統規定的時間。可是,若是一些發生在主線程的代碼寫的過重,執行任務花費時間過久,就會在主線程延遲Main Looper的消息在16ms尺度範圍內的讀和寫。
咱們如何檢測卡頓的問題?
使用主線程的Looper監測系統發生的卡頓和丟幀。編程技巧是設置一個閾值,看是否能夠打印stack信息。
網絡上說使用Android的Choreographer監測App發生的UI卡頓丟幀問題,本質上仍是利用了Android的主線程的Looper消息機制。Android 系統每隔 16.67 ms 都會發送一個 VSYNC 信號觸發 UI 的渲染,正常狀況下兩個 VSYNC 信號之間是 16.67 ms ,若是超過 16.67 ms 則能夠認爲渲染髮生了卡頓。
Choreographer.getInstance()
.postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long l) {
if(frameTimeNanos - mLastFrameNanos > 100) {
...
}
mLastFrameNanos = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
複製代碼
本質:判斷相鄰的兩次 FrameCallback.doFrame(long l)
間隔是否超過閾值,若是超過閾值則發生了卡頓,則能夠在另一個子線程中 dump 當前主線程的堆棧信息進行分析。
消息處理
UIThreadMonitor類
init():
public void init(TraceConfig config) {
if (Thread.currentThread() != Looper.getMainLooper().getThread()) {
throw new AssertionError("must be init in main thread!");
}
this.isInit = true;
this.config = config;
choreographer = Choreographer.getInstance();
callbackQueueLock = reflectObject(choreographer, "mLock");
callbackQueues = reflectObject(choreographer, "mCallbackQueues"); // 代碼 1
addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 2
addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 3
addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class); // 代碼 4
frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
LooperMonitor.register(new LooperMonitor.LooperDispatchListener() { // 代碼 5
@Override
public boolean isValid() {
return isAlive;
}
@Override
public void dispatchStart() {
super.dispatchStart();
UIThreadMonitor.this.dispatchBegin(); // 代碼 6
}
@Override
public void dispatchEnd() {
super.dispatchEnd();
UIThreadMonitor.this.dispatchEnd(); // 代碼 7
}
});
......
}
複製代碼
- 代碼 1:經過反射拿到了 Choreographer 實例的 mCallbackQueues 屬性,mCallbackQueues 是一個回調隊列數組 CallbackQueue[] mCallbackQueues,其中包括四個回調隊列,
第一個是輸入事件回調隊列 CALLBACK_INPUT = 0,
第二個是動畫回調隊列 CALLBACK_ANIMATION = 1,
第三個是遍歷繪製回調隊列 CALLBACK_TRAVERSAL = 2,
第四個是提交回調隊列 CALLBACK_COMMIT = 3。
這四個階段在每一幀的 UI 渲染中是依次執行的,每一幀中各個階段開始時都會回調 mCallbackQueues 中對應的回調隊列的回調方法。
- 代碼 2:經過反射拿到輸入事件回調隊列的 addCallbackLocked 方法
- 代碼 3:經過反射拿到動畫回調隊列的 addCallbackLocked 方法
- 代碼 4:經過反射拿到遍歷繪製回調隊列的addCallbackLocked 方法
- 代碼 5:經過 LooperMonitor.register(LooperDispatchListener listener) 方法向 LooperMonitor 中設置 LooperDispatchListener listener
- 代碼 6:在 Looper.loop() 中的消息處理開始時的回調
- 代碼 7:在 Looper.loop() 中的消息處理結束時的回調
複製代碼
核心:
private void dispatchBegin() {
//記錄2個時間 線程起始時間 和CPU的開始時間
token = dispatchTimeMs[0] = SystemClock.uptimeMillis();
dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (!observer.isDispatchBegin()) {
observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
}
}
}
}
複製代碼
private void dispatchEnd() { // 代碼 3
if (isBelongFrame) {
doFrameEnd(token);
}
dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
dispatchTimeMs[1] = SystemClock.uptimeMillis();
AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isBelongFrame);
}
}
}
}
複製代碼
核心點總結:
queueStatus
和 queueCost
分別對應着每一幀中輸入事件階段、動畫階段、遍歷繪製階段的狀態和耗時,queueStatus
有三個值:DO_QUEUE_DEFAULT、DO_QUEUE_BEGIN 和 DO_QUEUE_END。UIThreadMonitor
實現 Runnable
接口,也是爲了將 UIThreadMonitor
做爲輸入事件回調 CALLBACK_INPUT
的回調方法,設置到 Choreographer
中去的。看到這裏應該搞明白了卡頓的檢測原理,那麼FPS的計算呢?
每一幀的時間信息經過 HashSet<LooperObserver> observers
回調出去,看一下是在哪裏向 observers
添加 LooperObserver
回調的。主要看一下 FrameTracer
這個類,其中涉及到了幀率 FPS 的計算相關的代碼。
FPSCollector
是 FrameTracer
的一個內部類,實現了 IDoFrameListener
接口,主要邏輯是在 doFrameAsync()
方法中
FrameCollectItem#collect()
,計算幀率 FPS 等一些信息FrameCollectItem#report()
上報統計數據,並從 HashMap 中移除當前 ActivityName 和對應的 FrameCollectItem 對象private class FPSCollector extends IDoFrameListener {
private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());
private HashMap<String, FrameCollectItem> map = new HashMap<>();
@Override
public Handler getHandler() {
return frameHandler;
}
@Override
public void doFrameAsync(String focusedActivityName, long frameCost, int droppedFrames) {
super.doFrameAsync(focusedActivityName, frameCost, droppedFrames);
if (Utils.isEmpty(focusedActivityName)) {
return;
}
FrameCollectItem item = map.get(focusedActivityName); // 代碼 1
if (null == item) {
item = new FrameCollectItem(focusedActivityName);
map.put(focusedActivityName, item);
}
item.collect(droppedFrames); // 代碼 2
if (item.sumFrameCost >= timeSliceMs) { // report // 代碼 3
map.remove(focusedActivityName);
item.report();
}
}
}
複製代碼
FrameCollectItem:計算FPS。
根據 float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost)
計算 fps 值
sumDroppedFrames 統計總的掉幀個數
sumFrameCost 表明總耗時
計算幀率參考資料:
github.com/friendlyrob… 熟讀代碼 並應用到本身的項目中。
關於hook的部分:
利用了反射機制進行了hook,代碼比較清晰,目的很明確就是利用本身寫的HackCallback來替換ActivityThread類裏的mCallback,達到偷樑換柱的效果,這樣作的意義就是能夠攔截mCallback的原有的消息,而後選擇本身要處理的消息。搞清楚Activity的啓動流程 AMS和ActivityThread進程之間的交互。
獲取到Activity的啓動時機。
AppMethodBeat 經過hook記錄每一個方法執行的時間。
/** * hook method when it's called in. * * @param methodId */
public static void i(int methodId) {
if (status <= STATUS_STOPPED) {
return;
}
if (methodId >= METHOD_ID_MAX) {
return;
}
if (status == STATUS_DEFAULT) {
synchronized (statusLock) {
if (status == STATUS_DEFAULT) {
realExecute();
status = STATUS_READY;
}
}
}
if (Thread.currentThread().getId() == sMainThread.getId()) {
if (assertIn) {
android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!");
return;
}
assertIn = true;
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, true);
} else {
sIndex = -1;
}
++sIndex;
assertIn = false;
}
}
/** * hook method when it's called out. * * @param methodId */
public static void o(int methodId) {
if (status <= STATUS_STOPPED) {
return;
}
if (methodId >= METHOD_ID_MAX) {
return;
}
if (Thread.currentThread().getId() == sMainThread.getId()) {
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, false);
} else {
sIndex = -1;
}
++sIndex;
}
}
/** * merge trace info as a long data * * @param methodId * @param index * @param isIn */
private static void mergeData(int methodId, int index, boolean isIn) {
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
//記錄上面2個方法的時間差
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
sBuffer[index] = trueId;
checkPileup(index);
sLastIndex = index;
}
複製代碼
源碼讀到這裏是否是能夠想想,咱們應該怎麼找方法的調用?
利用ASM技術完成在方法前執行「com/tencent/matrix/trace/core/AppMethodBeat」這個class裏的i()
方法,在每一個方法最後執行o()
方法。
鄭重聲明
本文原做者爲課程主講師禪宗
,由Android研習社
代發
版權©️歸Android 研習社
全部,侵權必究