Transform 被废弃,ASM 如何适配?

8,572 阅读4分钟

前言

Transform APIAGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 ClassDex的过程中修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,然后可以借助ASM 等字节码编辑工具进行修改,插入自定义逻辑。

国内很多团队都或多或少的用 AGPTransform API 来搞点儿黑科技,比如无痕埋点,耗时统计,方法替换等。但是在AGP7.0Transform已经被标记为废弃了,并且将在AGP8.0中移除。而AGP8.0应该会在今年内发布,可以说是已经近在眼前了。所以现在应该是时候了解一下,在Transform被废弃之后,该怎么适配了。

Transform Action介绍

Transform API是由AGP提供的,而Transform Action则是由Gradle提供。不光是 AGP 需要 TransformJava 也需要,所以由 Gradle 来提供统一的 Transform API 也合情合理。
这应该也是Transform API被废弃的原因,既然Gradle已经统一提供了APIAGP也就没必要自定义一套了。

关于 TransformAction 如何使用,Gradle 官方已经提供了很详细的文档–Transforming dependency artifacts on resolution,具体使用可以直接参考文档

AsmClassVisitorFactory介绍

直接使用Transform Action的话还是有些麻烦,跟Transform API一样,需要手动处理增量编译的逻辑。AGP很贴心的为我们又做了一层封装,提供了AsmClassVisitorFactory来方便我们使用Transform Action进行ASM操作。 根据官方的说法,AsmClassVisitoFactory会带来约18%的性能提升,同时可以减少约5倍代码

代码实战

接下来我们利用AGPAsmClassVisitorFactory API,来实现方法执行耗时的插桩。

实现AsmClassVisitorFactory

abstract class TimeCostTransform: AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {
        return TimeCostClassVisitor(nextClassVisitor)
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        return true
    }
}
  1. AsmClassVisitorFactory即创建ClassVisitor对象的工厂。此接口的实现必须是一个抽象类,
  2. createClassVisitor返回我们自定义的ClassVisitor,在自定义Visitor处理完成后,需要传内容传递给下一个Visitor,因此我们将其放在构造函数中传入
  3. isInstrumentable用于控制我们的自定义Visitor是否需要处理这个类,通过这个方法可以过滤我们不需要的类,加快编译速度

自定义ClassVisitor

class TimeCostClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, nextVisitor) {
    override fun visitMethod(
        access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?
    ): MethodVisitor {
        val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
        val newMethodVisitor =
            object : AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {

                @Override
                override fun onMethodEnter() {
                    // 方法开始
                    if (isNeedVisiMethod(name)) {
                        mv.visitLdcInsn(name);
                        mv.visitMethodInsn(
                            INVOKESTATIC, "com/zj/android_asm/TimeCache", "putStartTime","(Ljava/lang/String;)V", false
                        );
                    }
                    super.onMethodEnter();
                }

                @Override
                override fun onMethodExit(opcode: Int) {
                    // 方法结束
                    if (isNeedVisiMethod(name)) {
                        mv.visitLdcInsn(name);
                        mv.visitMethodInsn(
                            INVOKESTATIC, "com/zj/android_asm/TimeCache", "putEndTime","(Ljava/lang/String;)V", false
                        );
                    }
                    super.onMethodExit(opcode);
                }
            }
        return newMethodVisitor
    }

    private fun isNeedVisiMethod(name: String?):Boolean {
        return name != "putStartTime" && name != "putEndTime" && name != "<clinit>" && name != "printlnTime" && name != "<init>"
    }
}

这里就跟普通的ASM操作没什么不同了,主要是通过ASM字节码插桩,在方法的前后插入如下代码,通过计算两者的时间差来得出方法的耗时

    fun timeMethod(){
        TimeCache.putStartTime("timeMethod") //方法开始插入的代码
        Thread.sleep(1000)
        TimeCache.putEndTime("timeMethod") //方法结束插入的代码
    }

注册Transform

老版本的Transform是注册在AppExtension中的,新版本则是注册在AndroidComponentsExtension

class TimeCostPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            variant.instrumentation.transformClassesWith(TimeCostTransform::class.java,
                    InstrumentationScope.PROJECT) {}
            variant.instrumentation.setAsmFramesComputationMode(
                    FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
        }
    }
}
  1. 基于variant可实现不同的变种不同的处理逻辑
  2. transformClassesWith通过InstrumentationScope控制是否需要扫描依赖库代码
  3. setAsmFramesComputationMode可设置不同的栈帧计算模式,具体可查看源码

AsmClassVisitorFactory的优势

通过以上步骤,一个简单的通过插桩计算方法执行耗时的功能就完成了,这比起老版的Transform API其实简化了不少,老版本处理增量更新就需要处理一大堆的逻辑。
可以看到我们这里并没有手动处理增量逻辑,这是因为调用AsmClassVisitorFactoryTransformClassesWithAsmTask继承自NewIncrementalTask,已经处理了增量逻辑,不需要我们再手动处理了

同时老版本的Transform每个Transfrom各自独立,如果每个Transform编译构建耗时+10s,各个Transform叠在一起,编译耗时就会呈线性增长
而新版本可以看出我们也没有手动进行IO操作,这是因为AsmInstrumentationManager中已经做了统一处理,只需要进行一次IO操作,然后交给ClassVisitor链表处理,完成后统一交给ClassWriter写入
通过这种方式,可以有效地减少IO操作,这也是新版本API性能提升的原因

总结

总得来说,由于Transform APIAGP7.0已标记为废弃,并且将在AGP8.0中移除,是时候了解一下如何迁移Transform API

AsmClassVisitorFactory相比Transform API,使用起来更加简单,不需要手动处理增量逻辑,可以专注于字节码插桩操作。同时AsmClassVisitorFactory通过减少IO的方式,可以得到约20%的性能提升,加快编译速度。

示例代码

本文所有源码可见:github.com/shenzhen201…

参考资料

其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)
现在准备好告别Transform了吗? | 拥抱AGP7.0