为什么要适配Transform Action?
因为registerTransform 这个api 在8.0的agp版本中要被删除了啊,到时候你的工程中的插件如果还有这个api 就编译失败了
适配这个东西有什么好处?
简单来说 以前 字节码修改的功能是 谷歌的agp团队给我们对外暴露了接口,后来gradle的团队发现 既然大家对这个需求这么强烈,那我在gradle 中直接暴露这个接口好了,所以谷歌就把这个register的api删了,大家一起直接基于gradle的api 来做asm 就可以了。
好处嘛,其实就是速度更快了,运行速度快 因为action的api是 只做一次io的,不像以前我们transform的api 你项目里有多少个transform 哪些class 就要copy多少次
另外还有一个就是编写速度会快不少,在新版本api的基础上,我们可以直接拿到 class的数据,而不需要像之前那样还需要处理transform中的输入和输出了,新版本 通过新的api 你可以直接拿到class数据,非常方便
arouter 中 利用asm做了啥?
其实就是下面这张图,我们反编译arouter以后 可以看一下这个方法里面,一共有7个register方法语句 这个其实就是在transform的过程中 做了字节码插桩了, 这7条语句你在arouter-api 这个代码里面你是找不到的
下面我们就简单分析下,arouter是怎么做的插桩,只有搞清楚他原来的逻辑,你才知道怎么用transform action去重写它
我们来到插件的入口:
其实主要做了两件事,注册了一个 transform, 另外就是初始化了一个 scan list,注意这个list里面 其实放的就是 arouter的 3个接口的路径
继续看Transform里面做了啥
这里也主要分为两块:
第一步: 扫描全部的输入,生成一个重要的class name 数据,注意是全部的输入,一次性扫描完
扫描的规则很简单: 必须得是 下面这个包下的
同时 还要判断这个class 是不是有继承接口,如果有 就要看一下这个接口 是不是属于我们前面那个scan list中的一个,如果是 那就把这个class name 放到 一个list中即可,
这里其实我觉得arouter原作者写复杂了,完全没有必要用list,直接用set就行了,免的判断是否存在这个逻辑
拿到了classList 这个数据 我们就可以进行第二步了:
第二步其实就是找到LogisticsCenter这个类,然后在这个类的loadRouterMap这个方法里面做插入代码的工作
看下面这个图,classList就是我们第一步工作的结果 生成的那个classList
到这arouter 的asm 流程就分析完毕了,其实代码还是有点多的,毕竟老的registerTransform 就是如此难用
这个classList的结果 打个日志看下吧:
用transform action 去改写
前面分析过了arouter的基本流程,现在就是改写的时候了,考虑到arouter之前的插件代码是groovy写的,我们首先把kotlin引入进来,
这里不要忘记加入一下sourceSets 不然编译的时候 会忽略你的kotlin代码
改写的思路也很简单,我们首先去生成我们需要的classList
abstract class FindArouterClassTransform : AsmClassVisitorFactory<None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
// 看下这个类的接口 是不是 那3个arouter的接口中的一个
classContext.currentClassData.interfaces.forEach {
// 这个ArouterInterfaceSet 就等于老代码中的scan list
if (UsedData.ArouterInterfaceSet.contains(it)) {
// 这个FindUsedInterfaceClassNameSet 就等于老代码中的class List
UsedData.FindUsedInterfaceClassNameSet.add(classContext.currentClassData.className)
}
}
return nextClassVisitor
}
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className.startsWith("com.alibaba.android.arouter.routes")
}
}
可以看下代码,是不是清爽很多? 就这么几行代码就可以完成重要的搜集信息工作了
这里唯一要注意的是,对于classData来说 它的class 信息都是. 而不是asm中的/ 这一点要注意了
第二步,找到我们logis那个类中的load方法,然后直接插桩
abstract class AddRegisterCodeClassTransform : AsmClassVisitorFactory<None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return object : ClassVisitor(Opcodes.ASM5, nextClassVisitor){
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
var mv = super.visitMethod(access, name, descriptor, signature, exceptions)
if (name == "loadRouterMap") {
mv = object : MethodVisitor(Opcodes.ASM5, mv) {
override fun visitInsn(opcode: Int) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
println(" UsedData.FindUsedInterfaceClassNameSet:${ UsedData.FindUsedInterfaceClassNameSet}")
UsedData.FindUsedInterfaceClassNameSet.forEach{ name ->
mv.visitLdcInsn(name)//类名
// generate invoke register method into LogisticsCenter.loadRouterMap()
mv.visitMethodInsn(Opcodes.INVOKESTATIC
, "com/alibaba/android/arouter/core/LogisticsCenter"
, "register"
, "(Ljava/lang/String;)V"
, false)
}
}
super.visitInsn(opcode) }
override fun visitMaxs(maxStack: Int, maxLocals: Int) {
super.visitMaxs(maxStack + 4, maxLocals)
}
}
}
return mv
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className == "com.alibaba.android.arouter.core.LogisticsCenter"
}
}
这个也很简单,其实就是之前的asm 代码 稍微改一下即可。
这两步做完以后就是最后的 plugin 初始化了
androidComponents.onVariants { variant ->
variant.instrumentation.transformClassesWith(
FindArouterClassTransform::class.java,
InstrumentationScope.ALL
){
}
variant.instrumentation.transformClassesWith(
AddRegisterCodeClassTransform::class.java,
InstrumentationScope.ALL
){}
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}
怎么样,看了这些代码 是不是觉得 action的API 比之前的transform API 要简洁不少?
大功告成了嘛?
我们运行起来一看,完蛋了,反编译以后 并没有插桩的代码,这是咋回事?再重新运行几次,发现有的时候能插桩成功,有的时候不行。 这下给我搞懵了
我们首先看下 这个代码,这个nameSet 其实就是存储的我们扫描到的类名称
在能够插桩成功的时候,发现一个规律,在gradle 的配置阶段,这个name set的值就已经是7了
按道理来说,你transform的任务都没执行呢,这个值默认应该是0 才对
这里猜想可能跟gradle 构建的缓存机制有关, 所以大家还是尽量把需要的值做成文件保存吧
静态变量不太靠谱,容易出现这种奇奇怪怪的问题。
我这里为了简单 就在每次config的阶段 直接把前面的值给clear掉就行了。
之后,我就能必现这个插桩失败的问题了,虽然是插桩失败,但是编译成功,我们仔细看下日志即可:
第一段日志:
第二段日志:
第三段日志:
看出问题来了嘛?
在新版本的transform action的逻辑中。
你虽然可以在一个插件中 transform 多次,但是 对于action来说, 它是 对每个jar包 来进行transform的,而不是全量jar包 进行transform
这里有点绕 我们仔细看下日志就可以:
它首先扫描的是 arouter-api这个 jar包,这个jar包中含有 我们要插桩的方法,也就是load方法 但是 此时 其他jar包还没扫描呢,因为其他jar包没扫描的缘故,我们这里的 nameSet 这个集合还是没有值的,那自然就插桩无效了
再看后面几段日志 我们已经扫描到了nameSet中的值了,但是此时arouter-api 已经没有重新扫描的机会了 也就没有办法继续插桩了
真是坑啊!
总结一下,假设你的project 在构建到transform的时候 有 abc 3个jar包,还有f1 和f2 2个 transform,
新版本的逻辑是:
对a.jar 进行f1和f2- 对b.jar 进行f1和f2 -对c.jar 进行f1和f2
而老版本的逻辑是 对a b c 3个jar 一起进行f1 对a b c 3个jar 一起进行f2
大概就是这样
问题能解决吗?
所以问题的核心就在在于 :如果你的字节码修改操作的前提条件是 需要扫描全部的class信息以后得出一个重要的值,那这种需求在transform action中就很难做了
因为新版本的transform action没有给你全盘扫描的机会
包括在新版本的agp中 mergeJavaRes这个任务 的产物输出 里面也拿不到全部的class了
可以看一下:
这个base.jar 里面啥都没有, 指望从这个目录下面去扫描全部的class 也不行了
目前这个问题我还没有找到解决方案, 始终找不到一个拿到全部class的契机,transform action不给我 其他的task 任务目前也没有类似的输出, 有知道方案的大佬可以评论区留言。