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 中指定的方法不须要统计
相关文章
相关标签/搜索