以前說到,Matrix 的卡頓監控關鍵在於插樁,下面來看一下它是怎麼實現的。java
Matrix 的 Gradle 插件的實現類爲 MatrixPlugin,主要作了三件事:android
class MatrixPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create("matrix", MatrixExtension)
project.matrix.extensions.create("trace", MatrixTraceExtension)
project.matrix.extensions.create("removeUnusedResources", MatrixDelUnusedResConfiguration)
}
}
複製代碼
其中 trace 可選配置以下:markdown
public class MatrixTraceExtension {
boolean enable; // 是否啓用插樁功能
String baseMethodMapFile; // 自定義的方法映射文件,下面會說到
String blackListFile; // 該文件指定的方法不會被插樁
String customDexTransformName;
}
複製代碼
removeUnusedResources 可選配置以下:app
class MatrixDelUnusedResConfiguration {
boolean enable // 是否啓用
String variant // 指定某一個構建變體啓用插樁功能,若是爲空,則全部的構建變體都啓用
boolean needSign // 是否須要簽名
boolean shrinkArsc // 是否裁剪 arsc 文件
String apksignerPath // 簽名文件的路徑
Set<String> unusedResources // 指定要刪除的不使用的資源
Set<String> ignoreResources // 指定不須要刪除的資源
}
複製代碼
// 在編譯期執行插樁任務(project.afterEvaluate 表明 build.gradle 文件執行完畢),這是由於 proguard 操做是在該任務以前就完成的
project.afterEvaluate {
android.applicationVariants.all { variant ->
if (configuration.trace.enable) { // 是否啓用,可在 gradle 文件中配置
MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())
}
... // RemoveUnusedResourcesTask
}
}
複製代碼
MatrixTraceTransform 的 inject 方法主要用於讀取配置,代理 transformClassesWithDexTask:ide
public class MatrixTraceTransform extends Transform {
public static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {
... // 根據參數生成 Configuration 變量 config
String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());
for (Task task : project.getTasks())
for (String str : hardTask)
if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {
Field field = TransformTask.class.getDeclaredField("transform");
field.set(task, new MatrixTraceTransform(config, task.getTransform()));
break;
}
}
// 這兩個 Transform 用於把 Class 文件編譯成 Dex 文件
// 所以,須要在這兩個 Transform 執行以前完成插樁等工做
private static String[] getTransformTaskName(String customDexTransformName, String buildTypeSuffix) {
return new String[] {
"transformClassesWithDexBuilderFor" + buildTypeSuffix,
"transformClassesWithDexFor" + buildTypeSuffix,
};;
}
}
複製代碼
MatrixTraceTransform 的主要配置以下:gradle
public class MatrixTraceTransform extends Transform {
@Override
public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; }
@Override
public Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT; }
}
複製代碼
transform 主要分三步執行:優化
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
// 用於分析和方法統計相關的文件,如 mapping.txt、blackMethodList.txt 等
// 並將映射規則保存到 mappingCollector、collectedMethodMap 中
futures.add(executor.submit(new ParseMappingTask(mappingCollector, collectedMethodMap, methodId)));
}
複製代碼
private void doTransform(TransformInvocation transformInvocation) {
MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());
}
複製代碼
private void doTransform(TransformInvocation transformInvocation) {
MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config,
methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
methodTracer.trace(dirInputOutMap, jarInputOutMap);
}
複製代碼
ParseMappingTask 主要用於分析方法統計相關的文件,如 mapping.txt(ProGuard 生成的)、blackMethodList.txt 等,並將映射規則保存到 HashMap 中。ui
mapping.txt 是 ProGuard 生成的,用於映射混淆先後的類名/方法名,內容以下:lua
MTT.ThirdAppInfoNew -> MTT.ThirdAppInfoNew: // oldClassName -> newClassName
java.lang.String sAppName -> sAppName // oldMethodName -> newMethodName
java.lang.String sTime -> sTime
...
複製代碼
blackMethodList.txt 則用於避免對特定的方法插樁,內容以下:spa
[package]
-keeppackage com/huluxia/logger/
-keepmethod com/example/Application attachBaseContext (Landroid/content/Context;)V
...
複製代碼
若是有須要,還能夠指定 baseMethodMapFile,將自定義的方法及其對應的方法 id 寫入到一個文件中,內容格式以下:
// 方法 id、訪問標誌、類名、方法名、描述
1,1,eu.chainfire.libsuperuser.Application$1 run ()V
2,9,eu.chainfire.libsuperuser.Application toast (Landroid.content.Context;Ljava.lang.String;)V
複製代碼
上述選項可在 gradle 文件配置,示例以下:
matrix {
trace {
enable = true
baseMethodMapFile = "{projectDir.absolutePath}/baseMethodMapFile.txt"
blackListFile = "{projectDir.absolutePath}/blackMethodList.txt"
}
}
複製代碼
顧名思義,MethodCollector 用於收集方法,它首先會把方法封裝爲 TraceMethod,並分配方法 id,再保存到 HashMap,最後寫入到文件中。
爲此,首先須要獲取全部 class 文件:
public void collect(Set<File> srcFolderList, Set<File> dependencyJarList) throws ExecutionException, InterruptedException {
for (File srcFile : srcFolderList) {
...
for (File classFile : classFileList) {
futures.add(executor.submit(new CollectSrcTask(classFile)));
}
}
for (File jarFile : dependencyJarList) {
futures.add(executor.submit(new CollectJarTask(jarFile)));
}
}
複製代碼
接着,藉助 ASM 訪問每個 Class 文件:
class CollectSrcTask implements Runnable {
@Override
public void run() {
InputStream is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
classReader.accept(visitor, 0);
}
}
複製代碼
及 Class 文件中的方法:
private class TraceClassAdapter extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (isABSClass) { // 抽象類或接口不須要統計
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
return new CollectMethodNode(className, access, name, desc, signature, exceptions);
}
}
}
複製代碼
最後,記錄方法數據,並保存到 HashMap 中:
private class CollectMethodNode extends MethodNode {
@Override
public void visitEnd() {
super.visitEnd();
// 將方法數據封裝爲 TraceMethod
TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
// 是否須要插樁,blackMethodList.txt 中指定的方法不會被插樁
boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
// 過濾空方法、get & set 方法等簡單方法
if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod()) && isNeedTrace) {
return;
}
// 保存到 HashMap 中
if (isNeedTrace && !collectedMethodMap.containsKey(traceMethod.getMethodName())) {
traceMethod.id = methodId.incrementAndGet();
collectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
incrementCount.incrementAndGet();
} else if (!isNeedTrace && !collectedIgnoreMethodMap.containsKey(traceMethod.className)) {
... // 記錄不須要插樁的方法
}
}
}
複製代碼
統計完畢後,將上述方法及其 ID 寫入到一個文件中——由於以後上報問題只會上報 method id,所以須要根據該文件來解析具體的方法名及其耗時。
雖然上面的代碼很長,但做用實際很簡單:訪問全部 Class 文件中的方法,記錄方法 ID,並寫入到文件中。
須要注意的細節有:
和方法統計同樣,插樁也是基於 ASM 實現的,首先一樣要找到全部 Class 文件,再針對文件中的每個方法進行處理。
處理流程主要包含四步:
public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";
private class TraceMethodAdapter extends AdviceAdapter {
@Override
protected void onMethodEnter() {
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) { // 省略空方法、set & get 等簡單方法
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
}
複製代碼
private class TraceMethodAdapter extends AdviceAdapter {
@Override
protected void onMethodExit(int opcode) {
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
... // 跟蹤 onWindowFocusChanged 方法,計算啓動耗時
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
}
複製代碼
private class TraceClassAdapter extends ClassVisitor {
@Override
public void visitEnd() {
// 若是是 Activity,而且不存在 onWindowFocusChanged 方法,則插入該方法,用於統計 Activity 啓動時間
if (!hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
insertWindowFocusChangeMethod(cv, className);
}
super.visitEnd();
}
}
複製代碼
public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";
private void traceWindowFocusChangeMethod(MethodVisitor mv, String classname) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "at", "(Landroid/app/Activity;Z)V", false);
}
複製代碼
public class AppMethodBeat implements BeatLifecycle {
public static void at(Activity activity, boolean isFocus) {
for (IAppMethodBeatListener listener : listeners) {
listener.onActivityFocused(activityName);
}
}
}
複製代碼
StartupTracer 就是 IAppMethodBeatListener 的實現類。
Matrix 的 Gradle 插件的實現類爲 MatrixPlugin,主要作了三件事:
須要注意的是,插樁任務是在編譯期執行的,這是爲了不對混淆操做產生影響。由於 proguard 操做是在該任務以前就完成的,意味着插樁時的 class 文件已經被混淆過的。而選擇 proguard 以後去插樁,是由於若是提早插樁會形成部分方法不符合內聯規則,無法在 proguard 時進行優化,最終致使程序方法數沒法減小,從而引起方法數過大問題
transform 主要分三步執行:
插樁處理流程主要包含四步:
值得注意的細節有: