今天来分析下 Matrix 框架的 matrix-trace-canary 部分,相关文档可查看《 Matrix Android TraceCanary 》,这份文档是阅读源码的关键,强烈建议先看文档再读源码。
TraceCanary 是检测应用卡顿的模块,在该模块下又细分了如下几个模块:
- StartupTracer : 启动时间检测模块
- FrameTracer : FPS 帧率检测模块
- EvilMethodTracer :耗时函数检测模块
- AnrTracer :ANR 检测模块
在这些模块当中,又是依赖 MethodBeat 的提前插桩来达到检测的效果,所以,我们在分析这几个模块之前,得先分析 MethodBeat 干了些什么,他是如何辅助以上几个模块来进行检测的。
为了不使文章变得枯燥无味,上面的四个模块会另起新的文章标题进行讲解,那么本文将会对 matrix-gradle-plugin 的插桩部分先进行一个大概的讲解,细节部分可能不会去分析,因为牵扯起来东西太多了。
来看下文档中对编译插桩部分的描述:
通过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,高效地对所有 class 文件进行扫描及插桩。
插桩过程有几个关键点: 1、选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。
2、为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。
3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。
4、为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持
分析
apply plugin: 'om.tencent.matrix-plugin' 对应的是 matrix-gradle-plugin 中的MatrixPlugin 类:
implementation-class=com.tencent.matrix.plugin.MatrixPlugin
MatrixPlugin.java
该插件做了如下操作:
1、关联 apply MatrixPlugin 设置的 extensions 参数,该参数可以查看 sample 的 app build.gradle
2、插入 transfrom task,该 task 就如文档所描述的 transformClassesWithDexTask
3、移除未使用的资源文件
我们进入 MatrixTraceTransform.inject 看看:
MatrixTraceTransform.java
配置的部分我省略掉了,由于 sample app build.gradle 中未设置 customDexTransformName 的,所以,hardTask 会返回 transformClassesWithDexBuilderForDebug 和 transformClassesWithDexForDebug,在运行 sample 项目时,复制了所有的 build 过程的日志信息,在查找的过程中,只有 transformClassesWithDexBuilderForDebug 参与了活动,并且,也打印了如上的 successfully inject task:transformClassesWithDexBuilderForDebug
日志信息。
最后会 hook 掉 transformClassesWithDexBuilderForDebug 的 task,替换成自己的 MatrixTraceTransform,MatrixTraceTransform 的构造方法传入了 transformClassesWithDexBuilderForDebug task,在 MatrixTraceTransform 使用完成后,是需要传递给下一个 transfrom 继续运行的,所以,在 MatrixTraceTransform 的 transfrom 方法中,会看到:
origTransform.transform(transformInvocation);
这个 origTransform 就是系统的 transformClassesWithDexBuilderForDebug
这时候,我们可以直接来看 MatrixTraceTransform task 了, doTransfrom 是最重要的方法,这个部分我大概去讲解一下,因为涉及跳转太多,篇幅就会非常有影响,好在 Matrix 的开发者留给了我们三个 step 注释,我们就按照这三个步骤去解释:
1、step1:
这个地方需要关注几个 executor.submit 的执行,第一个 ParseMappingTask,这个 task 从名称上就能看出是一个解析 Mapping 的任务,这个任务做了如下的操作:
- 读取 output 下面的 mapping.txt 文件,还原混淆前的类和方法,存放在 MappingCollector 类中 mObfuscatedClassMethodMap 和 mOriginalClassMethodMap 集合中
- 读取自定义的 baseMethodMapFile 配置,直接生成 TraceMethod 存放到 collectedMethodMap 中,在 sample 中,baseMethodMapFile 未设置配置文件
接着看 CollectDirectoryInputTask 和 CollectJarInputTask 任务,这两个任务主要收集 class 原文件和 traceClassOut 文件,class 原文件我们都知道,就是扫描到的 class 文件,那 traceClassOut 文件是什么呢?在默认的配置中,会设置一份 traceClassOut 路径,这个配置路径指向的是 output/traceClassOut 路径,我们可以打开 sample 下面的这个路径:
2、step2
主要看 collect 方法,该方法做了如下的几个操作:
- 将 DirectoryInput 收集的 class 原文件和 CollectJarInputTask 收集的原文件进行遍历,利用 TraceClassAdapter 来收集需要插入字节码(collectedClassExtendMap)和类和需要忽略掉的类(collectedIgnoreMethodMap)
- 分类保存上面收集到的两个集合,在写入文件时,会根据原 class 混淆文件 revert 还原 class,然后一并写入到文件中,这样,我们在查看混淆与对应文件时会非常的方便:
collectedClassExtendMap 集合中的数据是需要做插桩的类,该集合中的数据保存在 output/mapping/methodMapping 下面:
collectedIgnoreMethodMap 集合中的数据是需要忽略掉的类,该集合中的数据保存在 output/mapping/ignoreMethodMapping 下面 :
3、step3
拿到混淆类集合和还原的类集合,然后拿到在 step1 中收集到的原文件和 traceOut 文件,一并参与 trace 操作。所做的内容有:
- 遍历原 class 文件,并创建 traceOut 路径文件
- 对原 class 文件进行插桩操作,插桩的内容有:
- Activity 的 onWindowFocusChanged 方法插入 AppMethodBeat.getInstance().at 方法
- 满足类的方法在开头和结尾插入 i/o 方法
- 对插桩后的 class 文件 copy 一份到 traceOut 路径文件
总结
补充部分:
- matrix-gradle-plugin 是可以设置黑名单的,在 sample app build.gradle 下可以看到设置的 balckMethodList,对于设置了黑名单的 package 或是 class,是不会被插桩操作
- 如果想看插桩之后的代码效果,可以直接查看 output/mapping/traceClassOut 路径下的效果类,确实很方便,建议做插桩效果的插件都能像 matrix 这样去做,在看实现效果时,再也不用去看 transfrom 下面各种各种的 jar 或是反编译 apk 才能看到自己是否真正被插入成功
- 对于未被插桩的函数,可以直接查看 output/mapping/ignoreMethodMapping.txt 文件
- 每个插桩函数都有对应的 methodId,如果想看对应关系的话,可以直接查看 output/mapping/methodMapping.txt 文件