是性能优化系列之matrix框架的第3篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的小伙伴可以去我主页查看所有关于matrix的分享。

前言

对matrix框架的分析,第一篇文章 Android性能优化系列-腾讯matrix-IO监控-IOCanaryPlugin 用来分析io监控与优化的方向。接下来准备从卡顿优化入手,卡顿是项目中最容易影响用户体验的一个问题,所以也是至关重要的一个优化点。卡顿优化功能对应于matrix中的matrix-trace-canary模块,包含了多方面的卡顿监控,如启动监控、慢方法监控、Anr监控等等,而这些都依赖于matrix底层的一个基础能力-字节码插桩,所以在进行卡顿优化的代码分析前,有必要对这个基础能力的实现有一个直观的了解。

插件入口

在源码中找到matrix-gradle-plugin这个模块,找到插件的入口。
resources/META-INF/gradle-plugins/com.tencent.matrix-plugin.properties

implementation-class=com.tencent.matrix.plugin.MatrixPlugin

搜索MatrixPlugin,开始分析源码,今天的分析着重于matrix插桩原理,而不关注gradle插件的实现,所以有些gradle插件相关的内容会一笔带过,读者可以自行搜索相关内容。

MatrixPlugin-apply

apply是插件执行的入口,在这里会读取到build.gradle文件中的配置,配置内容包含两个方面,一是trace,一是removeUnusedResources, 本篇只分析trace任务,removeUnusedResources会在后边的文章中进行分析。

override fun apply(project: Project) {
    ...
    //进入MatrixTasksManager
    MatrixTasksManager().createMatrixTasks(
            project.extensions.getByName("android") as AppExtension,
            project,
            traceExtension,
            removeUnusedResourcesExtension
    )
}

traceExtension和removeUnusedResourcesExtension对应的正是build.gradle中的配置。

matrix {
    trace {
    }
    removeUnusedResources {
    }
}

createMatrixTasks

createMatrixTraceTask和createRemoveUnusedResourcesTask是插件的两个核心点。

fun createMatrixTasks(android: AppExtension,
                      project: Project,
                      traceExtension: MatrixTraceExtension,
                      removeUnusedResourcesExtension: MatrixRemoveUnusedResExtension) {
    createMatrixTraceTask(android, project, traceExtension)
    createRemoveUnusedResourcesTask(android, project, traceExtension)
}

createMatrixTraceTask

方法中针对不同gradle版本创建了两个不同的transform:

  • MatrixTraceTransform
  • MatrixTraceLegacyTransform

最终这两个transform会汇集到一个入口,那就是MatrixTrace.

MatrixTrace(
        ignoreMethodMapFilePath = config.ignoreMethodMapFilePath,
        methodMapFilePath = config.methodMapFilePath,
        baseMethodMapPath = config.baseMethodMapPath,
        blockListFilePath = config.blockListFilePath,
        mappingDir = config.mappingDir,
        project = project
).doTransform(
        classInputs = inputFiles,
        changedFiles = changedFiles,
        isIncremental = isIncremental,
        skipCheckClass = config.skipCheckClass,
        traceClassDirectoryOutput = outputDirectory,
        inputToOutput = inputToOutput,
        legacyReplaceChangedFile = null,
        legacyReplaceFile = null,
        uniqueOutputName = true
)

来看doTransform方法,官方注释很清楚,分为关键的三步,接下来我们一步一步来读一下代码。

fun doTransform() {
    ...
    /**
     * step 1
     */
    futures.add(executor.submit(ParseMappingTask(
            mappingCollector, collectedMethodMap, methodId, config)))
    for (file in classInputs) {
        if (file.isDirectory) {
            futures.add(executor.submit(CollectDirectoryInputTask()))
        } else {
            futures.add(executor.submit(CollectJarInputTask()))
        }
    }
    /**
     * step 2
     */
    val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
    methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)
    /**
     * step 3
     */
    val methodTracer = MethodTracer(executor, mappingCollector, config, methodCollector.collectedMethodMap, methodCollector.collectedClassExtendMap)
    methodTracer.trace(dirInputOutMap, jarInputOutMap, traceClassLoader, skipCheckClass)
}

第一步

包含三项任务

ParseMappingTask

这个任务是用来解析mapping.txt文件的,通过调用一个名为MappingReader的类去解析文件,解析的内容又可以分为类解析和类成员解析。mapping文件解析之后,我们就获得了混淆前和混淆后类的映射关系以及混淆前和混淆后方法的映射关系。

val mappingFile = File(config.mappingDir, "mapping.txt")
if (mappingFile.isFile) {
    val mappingReader = MappingReader(mappingFile)
    mappingReader.read(mappingCollector)
}

mappingReader.read()

if (!line.startsWith("#")) {
    // a class mapping
    if (line.endsWith(":")) {
        className = parseClassMapping(line, mappingProcessor);
    } else if (className != null) { 
        // a class member mapping
        parseClassMemberMapping(className, line, mappingProcessor);
    }
} 
parseClassMapping

解析出混淆前的类名和混淆后的类名,将映射关系保存在MappingProcessor(实现类MappingCollector)映射表中,对应于下边的三个map。

private String parseClassMapping(String line, MappingProcessor mappingProcessor) {
    ...
    boolean ret = mappingProcessor.processClassMapping(className, newClassName);
}

这两个集合中的className可能包含包名

HashMap<String, String> mObfuscatedRawClassMap

key value
混淆后的类名 原类名

HashMap<String, String> mRawObfuscatedClassMap

key value
原类名 混淆后的类名

HashMap<String, String> mRawObfuscatedPackageMap

key value
包名 混淆后的包名
parseClassMemberMapping

逻辑也是比较直接的,解析出每个类下的方法方法信息, 最终还是保存在了MappingProcessor中的映射表中,对应于下边的两个map。

private void parseClassMemberMapping(String className, String line, MappingProcessor mappingProcessor) {
    ...
    mappingProcessor.processMethodMapping(className, type, name, arguments, newClassName, newName);
}

Map<String, Map<String, Set>> mObfuscatedClassMethodMap

这个map用来记录一个类中所有的方法信息

key value
混淆后的类名为key 一个以混淆后的方法名为key, 以MethodInfo集合为value的map(注意:MethodInfo中的类名方法名都是未混淆的)

Map<String, Map<String, Set>> mOriginalClassMethodMap

key value
未混淆的类名为key 一个未混淆的方法名为key, 以MethodInfo集合为value的map(注意:MethodInfo中的类名方法名都是混淆后的)

下面两个任务针对directory和jar类型的文件分别处理

CollectDirectoryInputTask

此任务的输入是一个map映射表, 记录输入到数据的映射关系,对于支持增量编译的情况下,记录的是所有发生改变的文件的映射,未改变的文件不做记录。

resultOfDirInputToOut: MutableMap<File, File>

CollectJarInputTask

这个类的作用也类似,只不过操作对象是一个jar包,同样输入一个map集合记录映射关系。

第二步

MethodCollector

MethodCollector是用来收集所有需要被trace的方法的,它会过滤掉一些没有trace价值的方法,如构造方法、空方法,get、set方法等,还有一些指定不需要trace的类或者指定包下的类也会被过滤掉。

注意,methodId是一个比较关键的点,是一个从0开始递增的值,每一个值表示一个方法名,用数字表示方法名,并记录数字和方法名映射关系,用于后期分析时解析,是matrix内很巧妙的一个做法,感兴趣可以深入研究一下,这里不过多的解释了。

val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)

collect方法

  • 从文件夹中遍历得到所有文件,针对每个文件执行CollectSrcTask任务
  • 遍历所有jar,针对每个jar执行CollectJarTask
  • 上边两个任务执行完成后,再执行saveIgnoreCollectedMethod和saveCollectedMethod,等待全部完成后返回。
public void collect(Set<File> srcFolderList, Set<File> dependencyJarList) throws ExecutionException, InterruptedException {
    ...
    futures.add(executor.submit(new CollectSrcTask(classFile)));
    ...
    futures.add(executor.submit(new CollectJarTask(jarFile)));
    ...
    saveIgnoreCollectedMethod(mappingCollector);
    ...
    saveCollectedMethod(mappingCollector);
}
CollectSrcTask、CollectJarTask

相同的逻辑,只不过一个针对class,一个针对jar包。

这里使用了Asm(一个字节码插桩的库,自行百度了解其用法,这里默认读者具备了相关知识),所以关键操作在TraceClassAdapter中。

InputStream is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
classReader.accept(visitor, 0);
TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        //如果是接口或者抽象类,则isABSClass为true
        if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
            this.isABSClass = true;
        }
        //又一个map记录类和父类
        collectedClassExtendMap.put(className, superName);
    }
    @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 {
            //这里是记录类中是否含有onWindowFocusChanged,这是Activity的一个回调,matrix将此用于页面可见的记录时机。
            if (!hasWindowFocusMethod) {
                hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
            }
            //进入CollectMethodNode中
            return new CollectMethodNode(className, access, name, desc, signature, exceptions);
        }
    }
}
CollectMethodNode

只看它的visitEnd方法

@Override
public void visitEnd() {
    super.visitEnd();
    TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
    if ("<init>".equals(name)) {
        isConstructor = true;
    }
    //判断方法是否需要插桩,哪些方法可以插桩呢,看下边
    boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
    // 空方法,get、set方法,single method都会被过滤掉,并记录数量,加入map中存储
    if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
            && isNeedTrace) {
        ignoreCount.incrementAndGet();
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
        return;
    }
    if (isNeedTrace && !collectedMethodMap.containsKey(traceMethod.getMethodName())) {
        //需要插桩的方法记录到collectedMethodMap中
        traceMethod.id = methodId.incrementAndGet();
        collectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
        incrementCount.incrementAndGet();
    } else if (!isNeedTrace && !collectedIgnoreMethodMap.containsKey(traceMethod.className)) {
        ignoreCount.incrementAndGet();
        //不需要插桩的方法记录到collectedIgnoreMethodMap中
        collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
    }
}

isNeedTrace

public static boolean isNeedTrace(Configuration configuration, String clsName, MappingCollector mappingCollector) {
    boolean isNeed = true;
    //指定不需要插桩的方法,过滤掉
    if (configuration.blockSet.contains(clsName)) {
        isNeed = false;
    } else {
        if (null != mappingCollector) {
            //从上边的分析,我们知道,这是从mObfuscatedRawClassMap中获取未混淆的方法名
            clsName = mappingCollector.originalClassName(clsName, clsName);
        }
        clsName = clsName.replaceAll("/", ".");
        for (String packageName : configuration.blockSet) {
            //指定包名下的类也被过滤掉
            if (clsName.startsWith(packageName.replaceAll("/", "."))) {
                isNeed = false;
                break;
            }
        }
    }
    return isNeed;
}

最终还是产出了两个map映射表:collectedMethodMap、collectedIgnoreMethodMap,结构相同,但是代表的含义不同,collectedMethodMap存储需要被插桩的方法,collectedIgnoreMethodMap存储不需要插桩的方法。

key value
方法名 TraceMethod

此时再回头看看MethodCollector这个类的确像它的命名一样,只是方法的收集者,并不做插桩,而真正执行插桩的,是MethodTracer。

saveIgnoreCollectedMethod

逻辑很简单,就是将上边收集到的collectedIgnoreMethodMap中记录的不需要插桩的方法信息写入到指定的ignoreMethodMapFilePath文件中,方便查找,不属于本次分析的核心,不做过多解释。

private void saveIgnoreCollectedMethod(MappingCollector mappingCollector) {
    ...
}
saveCollectedMethod

和saveIgnoreCollectedMethod方法类似,只不过保存的是collectedMethodMap中的方法信息,保存在配置的methodMapFilePath文件路径中。有些特殊的是,它主动将Handler的dispatchMessage也加进去了,目的是什么,暂不看了。

private void saveCollectedMethod(MappingCollector mappingCollector) {
    ...
    TraceMethod extra = TraceMethod.create(TraceBuildConstants.METHOD_ID_DISPATCH, Opcodes.ACC_PUBLIC, "android.os.Handler",
        "dispatchMessage", "(Landroid.os.Message;)V");
    collectedMethodMap.put(extra.getMethodName(), extra);
    ...
}

第三步

接下来才是整个插件的重中之重,真正要开始插桩了

MethodTracer

trace

public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList, ClassLoader classLoader, boolean ignoreCheckClass) throws ExecutionException, InterruptedException {
    ...
    traceMethodFromSrc(srcFolderList, futures, classLoader, ignoreCheckClass);
    traceMethodFromJar(dependencyJarList, futures, classLoader, ignoreCheckClass);
    ...
}

traceMethodFromSrc

只保留核心代码

private void innerTraceMethodFromSrc(File input, File output, ClassLoader classLoader, boolean ignoreCheckClass) {
    ...
    is = new FileInputStream(classFile);
    ClassReader classReader = new ClassReader(is);
    ClassWriter classWriter = new TraceClassWriter(ClassWriter.COMPUTE_FRAMES, classLoader);
    ClassVisitor classVisitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
    is.close();
    ...
    if (!ignoreCheckClass) {
        try {
             ClassReader cr = new ClassReader(data);
             ClassWriter cw = new ClassWriter(0);
             ClassVisitor check = new CheckClassAdapter(cw);
             cr.accept(check, ClassReader.EXPAND_FRAMES);
        } catch (Throwable e) {
        }
    }
    ...
}

TraceClassAdapter

private class TraceClassAdapter extends ClassVisitor {
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
        this.isActivityOrSubClass = isActivityOrSubClass(className, collectedClassExtendMap);
        this.isNeedTrace = MethodCollector.isNeedTrace(configuration, className, mappingCollector);
        if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
            this.isABSClass = true;
        }
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        //类中是否包含onWindowFocusChanged方法,Activity中的方法,matrix将它作为页面可见的时机。
        if (!hasWindowFocusMethod) {
            hasWindowFocusMethod = MethodCollector.isWindowFocusChangeMethod(name, desc);
        }
        //是否是抽象类或接口,是则直接跳过,不插桩
        if (isABSClass) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        } else {
            MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
            return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
                    hasWindowFocusMethod, isActivityOrSubClass, isNeedTrace);
        }
    }
}

TraceMethodAdapter

private class TraceMethodAdapter extends AdviceAdapter {
    ...
    @Override
    protected void onMethodEnter() {
        //在方法入口插入AppMethodBeat.i(timestamp)方法
        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if (traceMethod != null) {
            traceMethodCount.incrementAndGet();
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
            if (checkNeedTraceWindowFocusChangeMethod(traceMethod)) {
                //在onWindowFocusChanged方法插入AppMethodBeat.at(timestamp)方法,
                //用于记录onWindowFocusChanged的执行时间,分析启动耗时。
                traceWindowFocusChangeMethod(mv, className);
            }
        }
    }
    @Override
    protected void onMethodExit(int opcode) {
        //在方法出口插入AppMethodBeat.o(timestamp)方法
        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if (traceMethod != null) {
            traceMethodCount.incrementAndGet();
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
        }
    }
}

总结

插件的核心内容在于TraceMethodAdapter中的三项操作:

  • 在符合条件的方法入口插入AppMethodBeat.i()
  • 在符合条件的方法出口插入AppMethodBeat.o()
  • 在Activity的onWindowFocusChanged方法中插入AppMethodBeat.at()

在编译期间,除被排除掉的方法外,大量方法的入口和出口处被插入了AppMethodBeat的方法,意在能通过这两个方法计算出方法执行的耗时,于是,每一个方法执行的耗时情况就清晰的展现在我们开发者眼前,借助这些数据才能更好的发现卡顿问题的原因。

有了这些基础,后边进行启动优化或者anr分析的时候就有迹可循,matrix会帮我们将方法按耗时时长排列出来,方便我们有的放矢的去解决问题。