Android性能优化系列-腾讯matrix-卡顿监控-gradle插件- 字节码插桩代码分析

稀土掘金地址:https://juejin.cn/post/7276412839585415187

前言

对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<MethodInfo>>> mObfuscatedClassMethodMap

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

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

Map<String, Map<String, Set<MethodInfo>>> 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会帮我们将方法按耗时时长排列出来,方便我们有的放矢的去解决问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容