为什么要适配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 使命现在也没有相似的输出, 有知道计划的大佬能够谈论区留言。