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

2,689 阅读7分钟

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

image.png

下面我们就简单分析下,arouter是怎么做的插桩,只有搞清楚他原来的逻辑,你才知道怎么用transform action去重写它

我们来到插件的入口:

image.png

其实主要做了两件事,注册了一个 transform, 另外就是初始化了一个 scan list,注意这个list里面 其实放的就是 arouter的 3个接口的路径

继续看Transform里面做了啥

image.png

这里也主要分为两块:

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

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

image.png

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

image.png

image.png

这里其实我觉得arouter原作者写复杂了,完全没有必要用list,直接用set就行了,免的判断是否存在这个逻辑

拿到了classList 这个数据 我们就可以进行第二步了:

第二步其实就是找到LogisticsCenter这个类,然后在这个类的loadRouterMap这个方法里面做插入代码的工作 image.png

看下面这个图,classList就是我们第一步工作的结果 生成的那个classList image.png

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

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

image.png

用transform action 去改写

前面分析过了arouter的基本流程,现在就是改写的时候了,考虑到arouter之前的插件代码是groovy写的,我们首先把kotlin引入进来,

这里不要忘记加入一下sourceSets 不然编译的时候 会忽略你的kotlin代码 image.png

改写的思路也很简单,我们首先去生成我们需要的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 其实就是存储的我们扫描到的类名称 image.png

在能够插桩成功的时候,发现一个规律,在gradle 的配置阶段,这个name set的值就已经是7了 按道理来说,你transform的任务都没执行呢,这个值默认应该是0 才对 image.png

这里猜想可能跟gradle 构建的缓存机制有关, 所以大家还是尽量把需要的值做成文件保存吧

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

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

之后,我就能必现这个插桩失败的问题了,虽然是插桩失败,但是编译成功,我们仔细看下日志即可:

第一段日志: image.png 第二段日志: image.png 第三段日志: image.png

看出问题来了嘛?

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

可以看一下:

image.png

这个base.jar 里面啥都没有, 指望从这个目录下面去扫描全部的class 也不行了

目前这个问题我还没有找到解决方案, 始终找不到一个拿到全部class的契机,transform action不给我 其他的task 任务目前也没有类似的输出, 有知道方案的大佬可以评论区留言。