transform API没学会?不用学了,AsmClassVisitorFactory更简单
- 前言
- 从零开始,构建一个兼容AGP8.0的插件
- 插件发布
- 为什么适配AGP8.0没用8.0.0版本?
- 同一插件如何注册多个转换任务/顺序执行多个转换任务
- InstrumentationParameters,插件配置参数
- 总结
- 实例代码
- 参考链接
前言
相信很多小伙伴项目还没有升级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 sdkimplementation gradleApi()//groovy sdkimplementation 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可以指定该字节码转换器在全局生效,包括第三方libvariant.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)}@Overrideoverride fun onMethodEnter() {super.onMethodEnter();// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false)startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTimemv.visitVarInsn(Opcodes.LSTORE, startTimeLocal)}@Overrideoverride fun onMethodExit(opcode: Int) {// 在onMethodExit中插入代码 Log.e("tag", "Method: $name, timecost: " + (System.currentTimeMillis() - startTime))mv.visitTypeInsn(Opcodes.NEW,"java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitLdcInsn("Method: $name, timecost: ");mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","(Ljava/lang/String;)V",false);mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false);mv.visitVarInsn(Opcodes.LLOAD, startTimeLocal);mv.visitInsn(Opcodes.LSUB);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder;",false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false);mv.visitLdcInsn("plugin")mv.visitInsn(Opcodes.SWAP)mv.visitMethodInsn(Opcodes.INVOKESTATIC,"android/util/Log","e","(Ljava/lang/String;Ljava/lang/String;)I",false)mv.visitInsn(POP)super.onMethodExit(opcode);}}return newMethodVisitor}
}
由于本篇的重点是插件的逻辑,所以字节码转换部分这里不做过多解释。到这里基本就完成了一个简单的插件的开发了。
插件发布
接下来发布插件,由于maven插件在AGP7.0已经废弃了,所以需要使用maven-publish插件来发布我们的插件代码到仓库,这里我们直接发布到本地仓库,修改后插件build.gradle代码如下:
plugins {id 'java'id 'groovy'id 'kotlin'id 'maven-publish'
}dependencies {//gradle sdkimplementation gradleApi()//groovy sdkimplementation 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'
}publishing {repositories { RepositoryHandler handler ->handler.maven { MavenArtifactRepository mavenArtifactRepository -> //正式仓库url '..\\localmaven'allowInsecureProtocol = trueif (url.toString().startsWith("http")) {credentials {username = ''password = ''}}}}publications { PublicationContainer publication ->maven(MavenPublication) {version '0.0.1'artifactId 'Plugin'groupId 'com.cs.testplugin'from components.java}}
}
发布到仓库有两种方式
一种是,在as中添加一个gradle执行任务
点击执行即可发布插件到maven仓库
第二种即是直接使用gradle命令发布。
首先需要在项目的gradle.properties配置文件中配置本地jdk路径(仅命令行操作需要)
org.gradle.java.home=D\:\\Android Studio\\jre
在项目文件夹下执行命令.\gradlew publish即可发布
.\gradlew publish
为什么适配AGP8.0没用8.0.0版本?
以上代码,如果将gradle版本替换为8.0.0也完全没有问题,但是这里有一个坑,那就是如果插件使用了8.0以上版本,那就必须使用jdk17来编译。而在应用中引用插件的时候,也必须使用jdk17才可以编译。这样就造成了如果要使用8.0编译的插件,还得把应用升级到使用jdk17,而对于大多数项目可能才刚刚升级到gradle7.0,因为gradle7.0或AS新版本的关系,才使用了jdk11。所以目前来说jdk17的应用普及率还比较低,这样的要求暂时还不太合适。
因此目前市面上已经兼容AGP8.0的插件几乎都是使用AGP7.x的版本来编译的。
到这里我相信大家对使用AsmClassVisitorFactory来替换transform API还有一些疑问,比如使用transform 时,可以在一个插件中注册多个转换任务,现在应该怎么做呢?
AsmClassVisitorFactory接口携带的类型是干嘛的?
接下来一一为大家解答:
同一插件如何注册多个转换任务/顺序执行多个转换任务
这个问题目前官方文档目前没有做任何说明,我目前也没有找到其他相关文章。抱着试一试的想法我问了下chatGPT
大家都知道chatGPT有时候喜欢一本正经的胡说八道,所以我也只能亲自尝试一下。
我copy了TimeCostTransform和TimeCostClassVisitor,改名为MethodTimeTransform和MethodTimeClassVisitor,接着修改了MethodTimeClassVisitor的打印log以区分,最终注册两个transform:
appExtension.onVariants { variant ->//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {}variant.instrumentation.transformClassesWith(MethodTimeTransform::class.java, InstrumentationScope.ALL) {}//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方libvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)}
接着maven发布,在空项目应用跑起来看一下
好家伙,还真行,替换注册顺序,也没毛病
InstrumentationParameters,插件配置参数
实现AsmClassVisitorFactory接口需要携带一个InstrumentationParameters类型,看这个类型的描述是参数的意思。也就是说插件运行可以携带一些配置参数。另外我们可以看到注册转换任务时的方法,第三个参数instrumentationParamsConfig也是用作初始化参数配置的
接下来我详细和大家介绍一下如何使用,以及和传统的方式有何区别。
首先我们创建配置文件ConfigExtension和ConfigExtensionNew,内容完全相同
open class ConfigExtension {public var logTag: String = "cs"public var includePackages: Array<String> = arrayOf()public var includeMethods: Array<String> = arrayOf()
}
创建PluginHelper,传统方式,使用单例存储配置
object PluginHelper {var extension: ConfigExtension? = null
}
创建TimeCostConfig,新api的方式,存储配置
interface TimeCostConfig : InstrumentationParameters {@get:Inputval packageNames: ListProperty<String>@get:Inputval methodNames: ListProperty<String>@get:Inputval logTag: Property<String>
}
TestPlugin中添加配置文件相关逻辑
class TestPlugin : Plugin<Project> {override fun apply(project: Project) {//这里appExtension获取方式与原transform api不同,可自行对比val appExtension = project.extensions.getByType(AndroidComponentsExtension::class.java)//读取配置文件project.extensions.create("TestPlugin", ConfigExtension::class.java)project.extensions.create("TestPluginNew", ConfigExtensionNew::class.java)//这里通过transformClassesWith替换了原registerTransform来注册字节码转换操作appExtension.onVariants { variant ->//传统方式,配置获取后直接使用单例存储,使用时读取PluginHelper.extension = project.extensions.getByType(ConfigExtension::class.java)val extensionNew = project.extensions.getByType(ConfigExtensionNew::class.java)//可以通过variant来获取当前编译环境的一些信息,最重要的是可以 variant.name 来区分是debug模式还是release模式编译variant.instrumentation.transformClassesWith(MethodTimeTransform::class.java, InstrumentationScope.ALL) {}variant.instrumentation.transformClassesWith(TimeCostTransform::class.java, InstrumentationScope.ALL) {//配置通过指定配置的类,携带到TimeCostTransform中it.packageNames.set(extensionNew.includePackages.toList())it.methodNames.set(extensionNew.includeMethods.toList())it.logTag.set(extensionNew.logTag)}//InstrumentationScope.ALL 配合 FramesComputationMode.COPY_FRAMES可以指定该字节码转换器在全局生效,包括第三方libvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)}}
}
在应用中可以设置两个配置代码块,分别对应TestPlugin和TestPluginNew
接下来修改transform任务,添加通过配置执行不同的asm操作
TimeCostTransform中
abstract class TimeCostTransform() : AsmClassVisitorFactory<TimeCostConfig> {override fun createClassVisitor(classContext: ClassContext,nextClassVisitor: ClassVisitor): ClassVisitor {//指定真正的ASM转换器,传入配置return TimeCostClassVisitor(nextClassVisitor, parameters.get())}//通过classData中的当前类的信息,用来过滤哪些类需要执行字节码转换,这里支持通过类名,包名,注解,接口,父类等属性来组合判断override fun isInstrumentable(classData: ClassData): Boolean {//指定包名执行//通过parameters.get()来获取传递的配置val packageConfig = parameters.get().packageNames.get()if (packageConfig.isNotEmpty()) {return packageConfig.any { classData.className.contains(it) }}return true}
}
TimeCostClassVisitor中增加读取配置,过滤配置中的方法名,指定log打印配置读取的tag
class TimeCostClassVisitor(nextVisitor: ClassVisitor,val config: TimeCostConfig) : 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 methodNameConfig = config.methodNames.get()if (methodNameConfig.isNotEmpty()) {if (methodNameConfig.none { name == it }) {return methodVisitor}}//从配置中读取tagval tag = config.logTag.get()val newMethodVisitor =object : AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, descriptor) {private var startTimeLocal = -1 // 保存 startTime 的局部变量索引override fun visitInsn(opcode: Int) {super.visitInsn(opcode)}@Overrideoverride fun onMethodEnter() {super.onMethodEnter();// 在onMethodEnter中插入代码 val startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false)startTimeLocal = newLocal(Type.LONG_TYPE) // 创建一个新的局部变量来保存 startTimemv.visitVarInsn(Opcodes.LSTORE, startTimeLocal)}@Overrideoverride fun onMethodExit(opcode: Int) {// 在onMethodExit中插入代码 Log.e("tag", "Method: $name, timecost: " + (System.currentTimeMillis() - startTime))mv.visitTypeInsn(Opcodes.NEW,"java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);mv.visitLdcInsn("Method: $name, timecost: ");mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder","<init>","(Ljava/lang/String;)V",false);mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false);mv.visitVarInsn(Opcodes.LLOAD, startTimeLocal);mv.visitInsn(Opcodes.LSUB);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","append","(J)Ljava/lang/StringBuilder;",false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder","toString","()Ljava/lang/String;",false);//从配置中读取tagmv.visitLdcInsn(tag)mv.visitInsn(Opcodes.SWAP)mv.visitMethodInsn(Opcodes.INVOKESTATIC,"android/util/Log","e","(Ljava/lang/String;Ljava/lang/String;)I",false)mv.visitInsn(POP)super.onMethodExit(opcode);}}return newMethodVisitor}
}
MethodTimeTransform中和MethodTimeClassVisitor中大同小异,只是将配置读取变为了从PluginConfingHelper中读取,这里就不贴代码了。
接下来我们发布仓库,然后在应用中添加配置文件:
TestPlugin {includePackages = ['com.cs.supportagp80']includeMethods = ["test","onCreate"]logTag = 'Plugin'
}
TestPluginNew {includePackages = ['com.cs.supportagp80']includeMethods = ["test","onCreate"]logTag = 'Plugin'
}
在MainActivity中添加一个测试方法
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)test()}private fun test(){print("test")}
}
运行代码,查看日志
很好,两个配置块都生效了,看到这里肯定有人说,这两种配置不是都能生效吗?为什么要有新的配置方式。别急,马上我们就可以看到他们之间的区别。
接下来我们修改TestPluginNew 配置块中的配置为:
TestPluginNew {includePackages = ['com.cs.supportagp80']includeMethods = ["test"]logTag = 'Plugin-new'
}
然后重新执行代码,日志打印如下:
我们可以看到,配置生效了,tag变成了Plugin-new,且只打印了test方法的执行时间。
然后我们再修改TestPlugin配置块中的配置为:
TestPlugin {includePackages = ['com.cs.supportagp80']includeMethods = ["test"]logTag = 'Plugin-old'
}
然后执行代码,日志打印如下:
我们可以看到,TestPlugin 的配置块修改并没有生效,依旧是上一次的配置。
到这里我相信大家应该已经看明白了两种配置文件的配置方式到底有何区别,就是新方式设置配置,在修改后可以及时生效,老方式却不能。
最终经过我多次试验,总结得到的一个不太严谨的结论是,老方式的配置,只有在对应的类文件发生变化,需要重新编译时才会生效。
总结
以上就是如何开发兼容AGP8.0插件的全部内容了。
本文详细介绍了如何使用gradle7.4.2版本开发一个兼容gradle8.0的插件,并且分析了如何使用transformClassesWith注册多个转换任务,顺序执行。最后分析了新API下如何配置插件的配置参数,与原有方式配置参数有何区别。
实例代码
本篇全部代码可见:supportAGP8.0
参考链接
Transform 被废弃,ASM 如何适配?
现在准备好告别Transform了吗? | 拥抱AGP7.0
神策数据官方 Android 埋点插件