侧边栏壁纸
博主头像
落叶人生博主等级

走进秋风,寻找秋天的落叶

  • 累计撰写 130562 篇文章
  • 累计创建 28 个标签
  • 累计收到 9 条评论
标签搜索

目 录CONTENT

文章目录

Android 性能监控框架 Matrix(6)插桩

2022-07-30 星期六 / 0 评论 / 0 点赞 / 232 阅读 / 23188 字

之前说到,Matrix 的卡顿监控关键在于插桩,下面来看一下它是怎么实现的。Gradle 插件配置Matrix 的 Gradle 插件的实现类为 MatrixPlugin,主要做了三件事:添加 Ext

之前说到,Matrix 的卡顿监控关键在于插桩,下面来看一下它是怎么实现的。

Gradle 插件配置

Matrix 的 Gradle 插件的实现类为 MatrixPlugin,主要做了三件事:

  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 可选配置如下:

public class MatrixTraceExtension {    boolean enable; // 是否启用插桩功能    String baseMethodMapFile;  // 自定义的方法映射文件,下面会说到    String blackListFile; // 该文件指定的方法不会被插桩    String customDexTransformName; }

removeUnusedResources 可选配置如下:

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:

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 的主要配置如下:

  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 中。

mapping.txt 是 ProGuard 生成的,用于映射混淆前后的类名/方法名,内容如下:

MTT.ThirdAppInfoNew -> MTT.ThirdAppInfoNew: // oldClassName -> newClassName    java.lang.String sAppName -> sAppName // oldMethodName -> newMethodName    java.lang.String sTime -> sTime    ...

blackMethodList.txt 则用于避免对特定的方法插桩,内容如下:

[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 ()V2,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 中指定的方法不需要统计
.
.

广告 广告

评论区