为什么要适配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 这个代码里边你是找不到的

你的插件想适配Transform Action? 可能还早了点

下面咱们就简略剖析下,arouter是怎样做的插桩,只要搞清楚他本来的逻辑,你才知道怎样用transform action去重写它

咱们来到插件的入口:

你的插件想适配Transform Action? 可能还早了点

其实首要做了两件事,注册了一个 transform, 别的便是初始化了一个 scan list,注意这个list里边 其实放的便是 arouter的 3个接口的路径

持续看Transform里边做了啥

你的插件想适配Transform Action? 可能还早了点

这儿也首要分为两块:

榜首步: 扫描悉数的输入,生成一个重要的class name 数据,注意是悉数的输入,一次性扫描完

扫描的规矩很简略: 必须得是 下面这个包下的

你的插件想适配Transform Action? 可能还早了点

同时 还要判别这个class 是不是有继承接口,如果有 就要看一下这个接口 是不是归于咱们前面那个scan list中的一个,如果是 那就把这个class name 放到 一个list中即可,

你的插件想适配Transform Action? 可能还早了点

你的插件想适配Transform Action? 可能还早了点

这儿其实我觉得arouter原作者写杂乱了,彻底没有必要用list,直接用set就行了,免的判别是否存在这个逻辑

拿到了classList 这个数据 咱们就能够进行第二步了:

第二步其实便是找到LogisticsCenter这个类,然后在这个类的loadRouterMap这个办法里边做插入代码的作业

你的插件想适配Transform Action? 可能还早了点

看下面这个图,classList便是咱们榜首步作业的成果 生成的那个classList

你的插件想适配Transform Action? 可能还早了点

到这arouter 的asm 流程就剖析完毕了,其实代码仍是有点多的,毕竟老的registerTransform 便是如此难用

这个classList的成果 打个日志看下吧:

你的插件想适配Transform Action? 可能还早了点

用transform action 去改写

前面剖析过了arouter的根本流程,现在便是改写的时分了,考虑到arouter之前的插件代码是groovy写的,咱们首要把kotlin引进进来,

这儿不要忘掉参加一下sourceSets 不然编译的时分 会忽略你的kotlin代码

你的插件想适配Transform Action? 可能还早了点

改写的思路也很简略,咱们首要去生成咱们需求的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 其实便是存储的咱们扫描到的类称号

你的插件想适配Transform Action? 可能还早了点

在能够插桩成功的时分,发现一个规律,在gradle 的装备阶段,这个name set的值就现已是7了 按道理来说,你transform的使命都没履行呢,这个值默许应该是0 才对

你的插件想适配Transform Action? 可能还早了点

这儿猜想或许跟gradle 构建的缓存机制有关, 所以咱们仍是尽量把需求的值做成文件保存吧

静态变量不太靠谱,容易出现这种奇奇怪怪的问题。

我这儿为了简略 就在每次config的阶段 直接把前面的值给clear掉就行了。

之后,我就能必现这个插桩失利的问题了,尽管是插桩失利,可是编译成功,咱们细心看下日志即可:

榜首段日志:

你的插件想适配Transform Action? 可能还早了点
第二段日志:
你的插件想适配Transform Action? 可能还早了点
第三段日志:
你的插件想适配Transform Action? 可能还早了点

看出问题来了嘛?

在新版别的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了

能够看一下:

你的插件想适配Transform Action? 可能还早了点

这个base.jar 里边啥都没有, 指望从这个目录下面去扫描悉数的class 也不可了

现在这个问题我还没有找到解决计划, 一直找不到一个拿到悉数class的契机,transform action不给我 其他的task 使命现在也没有相似的输出, 有知道计划的大佬能够谈论区留言。