Android 性能監控框架 Matrix(6)插樁

以前說到,Matrix 的卡頓監控關鍵在於插樁,下面來看一下它是怎麼實現的。java

Gradle 插件配置

Matrix 的 Gradle 插件的實現類爲 MatrixPlugin,主要作了三件事:android

  1. 添加 Extension,用於提供給用戶自定義配置選項
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 // 指定不須要刪除的資源
}
複製代碼
  1. 讀取配置,若是啓用插樁,則執行 MatrixTraceTransform,統計方法並插樁
// 在編譯期執行插樁任務(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
    }
}
複製代碼
  1. 讀取配置,若是啓用 removeUnusedResources 功能,則執行 RemoveUnusedResourcesTask,刪除不須要的資源

方法統計及插樁

配置 Transform

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

  1. 處理範圍爲整個項目(包括當前項目、子項目、依賴庫等)
  2. 處理類型爲 Class 文件
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 主要分三步執行:優化

  1. 根據配置文件分析方法統計規則,好比混淆後的類名和原始類名之間的映射關係、不須要插樁的方法黑名單等
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
    // 用於分析和方法統計相關的文件,如 mapping.txt、blackMethodList.txt 等
    // 並將映射規則保存到 mappingCollector、collectedMethodMap 中
    futures.add(executor.submit(new ParseMappingTask(mappingCollector, collectedMethodMap, methodId)));
}
複製代碼
  1. 統計方法及其 ID,並寫入到文件中
private void doTransform(TransformInvocation transformInvocation) {
    MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
    methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());
}
複製代碼
  1. 插樁
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,並寫入到文件中。

須要注意的細節有:

  1. 統計的方法包括應用自身的、JAR 依賴包中的,以及額外添加的 ID 固定的 dispatchMessage 方法
  2. 抽象類或接口類不須要統計
  3. 空方法、get & set 方法等簡單方法不須要統計
  4. blackMethodList.txt 中指定的方法不須要統計

插樁

和方法統計同樣,插樁也是基於 ASM 實現的,首先一樣要找到全部 Class 文件,再針對文件中的每個方法進行處理。

處理流程主要包含四步:

  1. 進入方法時執行 AppMethodBeat.i,傳入方法 ID,記錄時間戳
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);
        }
    }
}
複製代碼
  1. 退出方法時執行 AppMethodBeat.o,傳入方法 ID,記錄時間戳
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);
        }
    }
}
複製代碼
  1. 若是是 Activity,而且沒有 onWindowFocusChanged 方法,則插入該方法
private class TraceClassAdapter extends ClassVisitor {

    @Override
    public void visitEnd() {
        // 若是是 Activity,而且不存在 onWindowFocusChanged 方法,則插入該方法,用於統計 Activity 啓動時間
        if (!hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
            insertWindowFocusChangeMethod(cv, className);
        }
        super.visitEnd();
    }
}
複製代碼
  1. 跟蹤 onWindowFocusChanged 方法,退出時執行 AppMethodBeat.at,計算啓動耗時
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,主要作了三件事:

  1. 添加 Extension,用於提供給用戶自定義配置選項
  2. 讀取 extension 配置,若是啓用 trace 功能,則執行 MatrixTraceTransform,統計方法並插樁
  3. 讀取 extension 配置,若是啓用 removeUnusedResources 功能,則執行 RemoveUnusedResourcesTask,刪除不須要的資源

須要注意的是,插樁任務是在編譯期執行的,這是爲了不對混淆操做產生影響。由於 proguard 操做是在該任務以前就完成的,意味着插樁時的 class 文件已經被混淆過的。而選擇 proguard 以後去插樁,是由於若是提早插樁會形成部分方法不符合內聯規則,無法在 proguard 時進行優化,最終致使程序方法數沒法減小,從而引起方法數過大問題

transform 主要分三步執行:

  1. 根據配置文件(mapping.txt、blackMethodList.txt、baseMethodMapFile)分析方法統計規則,好比混淆後的類名和原始類名之間的映射關係、不須要插樁的方法黑名單等
  2. 藉助 ASM 訪問全部 Class 文件的方法,記錄其 ID,並寫入到文件中(methodMapping.txt)
  3. 插樁

插樁處理流程主要包含四步:

  1. 進入方法時執行 AppMethodBeat.i,傳入方法 ID,記錄時間戳
  2. 退出方法時執行 AppMethodBeat.o,傳入方法 ID,記錄時間戳
  3. 若是是 Activity,而且沒有 onWindowFocusChanged 方法,則插入該方法
  4. 跟蹤 onWindowFocusChanged 方法,退出時執行 AppMethodBeat.at,計算啓動耗時

值得注意的細節有:

  1. 統計的方法包括應用自身的、JAR 依賴包中的,以及額外添加的 ID 固定的 dispatchMessage 方法
  2. 抽象類或接口類不須要統計
  3. 空方法、get & set 方法等簡單方法不須要統計
  4. blackMethodList.txt 中指定的方法不須要統計
相關文章
相關標籤/搜索