transform API没学会?不用学了,AsmClassVisitorFactory更简单
前言
相信很多小伙伴项目还没有升级AGP7.0,可是最新的AGP已经到8.2了,适配AGP8.0也要提上日程了,尤其是一些插件项目,因为8.0删除了transform API,所以需要提前做好适配工作。
如果你是一个插件小白,本篇可以教你从0开始在AGP7.0以上如何开发插件。
如果你是一个插件开发者,相信本篇也可以给你适配AGP8.0带来一些帮助。
从零开始,构建一个兼容AGP8.0的插件
首先我们新建一个空项目,然后在项目中开始添加模块。
由于as没有创建插件模块的选项,所以这里我们选择手动添加。
第一步:在app同级目录创建如下文件
然后在setting.gradle配置文件中引入插件
include ':app'
include ':plugin'
接着我们在插件目录的build.gradle文件中添加一些必要的依赖:
plugins {
id 'java'
id 'groovy'
id 'kotlin'
}
dependencies {
//gradle sdk
implementation gradleApi()
//groovy sdk
implementation localGroovy()
implementation 'com.android.tools.build:gradle:7.4.2'
implementation 'com.android.tools.build:gradle-api:7.4.2'
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-util:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}
大家可能注意到了,这里我们依赖的gradle版本并非8.0版本,而是gradle7.4.2版本,为啥不用8.0.0版本呢,这个稍后再解释,我们继续插件的创建。
接着我们开始添加插件的源文件:
在TestPlugin.properties配置中指定插件入口类,同时该配置文件的名称xxx.properties的xxx即为插件的名称,也就是后期我们应用引入该插件时的名称
implementation-class=com.cs.plugin.TestPlugin
这里还需要注意一点,就是创建META-INF.gradle-plugins的文件夹时,一定要创建两个文件夹,千万不要这样创建
接下来开始真正的插件代码逻辑了
TestPlugin中添加如下代码:
class TestPlugin : Plugin<Project> {
override fun apply(project: Project) {
//这里appExtension获取方式与原transform api不同,可自行对比
val appExtension = project.extensions.getByType(
AndroidComponentsExtension::class.java
)
//这里通过transformClassesWith替换了原registerTransform来注册字节码转换操作
appExtension.onVariants {
variant ->
//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译
variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {
}
//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方lib
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
}
}
这里我们注册一个TimeCostTransform的字节码转换功能,用来统计方法执行的时长。TimeCostTransform需要实现AsmClassVisitorFactory这个接口,该接口正是用于替换原Transform的API,新API中只需要关注ASM操作的实现即ClassVisitor,大大简化了插件开发的工作。
TimeCostTransform中添加如下代码
abstract class TimeCostTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
//指定真正的ASM转换器
return TimeCostClassVisitor(nextClassVisitor)
}
//通过classData中的当前类的信息,用来过滤哪些类需要执行字节码转换,这里支持通过类名,包名,注解,接口,父类等属性来组合判断
override fun isInstrumentable(classData: ClassData): Boolean {
//指定包名执行
return classData.className.startsWith("com.cs.supportagp80")
}
}
接着我们创建一个TimeCostClassVisitor的字节码转换器,用来执行在方法开始时及结束时分别插入代码来统计方法耗时,并且打印出来的逻辑
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)
if (name == "<clinit>" || name == "<init>") {
return methodVisitor
}
val newMethodVisitor =
object : AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {
private var startTimeLocal = -1 // 保存 startTime 的局部变量索引
override fun visitInsn(opcode: Int) {
super.visitInsn(opcode)
}
@Override
override fun onMethodEnter() {
super.onMethodEnter();
// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/System",
"currentTimeMillis",
"()J",
false
)
startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTime
mv.visitVarInsn(Opcodes.LSTO