前言

信任许多小伙伴项目还没有升级AGP7.0,可是最新的AGP现已到8.2了,适配AGP8.0也要提上日程了,尤其是一些插件项目,因为8.0删除了transform API,所以需求提前做好适配作业。 假如你是一个插件小白,本篇能够教你从0开端在AGP7.0以上怎么开发插件。 假如你是一个插件开发者,信任本篇也能够给你适配AGP8.0带来一些帮助。

从零开端,构建一个兼容AGP8.0的插件

首要咱们新建一个空项目,然后在项目中开端增加模块。 由于as没有创立插件模块的选项,所以这儿咱们选择手动增加。 第一步:在app同级目录创立如下文件

从开发一个插件看,安卓gradle插件适配AGP8.0
然后在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版别呢,这个稍后再解说,咱们继续插件的创立。 接着咱们开端增加插件的源文件:

从开发一个插件看,安卓gradle插件适配AGP8.0
在TestPlugin.properties装备中指定插件进口类,一起该装备文件的称号xxx.properties的xxx即为插件的称号,也便是后期咱们运用引进该插件时的称号

implementation-class=com.cs.plugin.TestPlugin

这儿还需求注意一点,便是创立META-INF.gradle-plugins的文件夹时,一定要创立两个文件夹,千万不要这样创立

从开发一个插件看,安卓gradle插件适配AGP8.0
从开发一个插件看,安卓gradle插件适配AGP8.0
接下来开端真实的插件代码逻辑了 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.LSTORE, startTimeLocal)
                }
                @Override
                override 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 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'
}
publishing {
    repositories { RepositoryHandler handler ->
        handler.maven { MavenArtifactRepository mavenArtifactRepository -> //正式库房
            url '..\\localmaven'
            allowInsecureProtocol = true
            if (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履行使命

从开发一个插件看,安卓gradle插件适配AGP8.0
从开发一个插件看,安卓gradle插件适配AGP8.0
点击履行即可发布插件到maven库房
从开发一个插件看,安卓gradle插件适配AGP8.0
第二种即是直接运用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

从开发一个插件看,安卓gradle插件适配AGP8.0
我们都知道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能够指定该字节码转化器在全局收效,包含第三方lib
            variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
        }

接着maven发布,在空项目运用跑起来看一下

从开发一个插件看,安卓gradle插件适配AGP8.0
好家伙,还真行,替换注册次序,也没缺点
从开发一个插件看,安卓gradle插件适配AGP8.0

从开发一个插件看,安卓gradle插件适配AGP8.0

InstrumentationParameters,插件装备参数

完成AsmClassVisitorFactory接口需求带着一个InstrumentationParameters类型,看这个类型的描绘是参数的意思。也便是说插件运转能够带着一些装备参数。另外咱们能够看到注册转化使命时的办法,第三个参数instrumentationParamsConfig也是用作初始化参数装备的

从开发一个插件看,安卓gradle插件适配AGP8.0
接下来我详细和我们介绍一下怎么运用,以及和传统的办法有何差异。 首要咱们创立装备文件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:Input
    val packageNames: ListProperty<String>
    @get:Input
    val methodNames: ListProperty<String>
    @get:Input
    val 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能够指定该字节码转化器在全局收效,包含第三方lib
            variant.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
            }
        }
        //从装备中读取tag
        val 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)
                }
                @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.LSTORE, startTimeLocal)
                }
                @Override
                override 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
                    );
                    //从装备中读取tag
                    mv.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")
    }
}

运转代码,查看日志

从开发一个插件看,安卓gradle插件适配AGP8.0
很好,两个装备块都收效了,看到这儿肯定有人说,这两种装备不是都能收效吗?为什么要有新的装备办法。别急,立刻咱们就能够看到他们之间的差异。 接下来咱们修正TestPluginNew 装备块中的装备为:

TestPluginNew {
    includePackages = ['com.cs.supportagp80']
    includeMethods = ["test"]
    logTag = 'Plugin-new'
}

然后重新履行代码,日志打印如下:

从开发一个插件看,安卓gradle插件适配AGP8.0
咱们能够看到,装备收效了,tag变成了Plugin-new,且只打印了test办法的履行时间。 然后咱们再修正TestPlugin装备块中的装备为:

TestPlugin {
    includePackages = ['com.cs.supportagp80']
    includeMethods = ["test"]
    logTag = 'Plugin-old'
}

然后履行代码,日志打印如下:

从开发一个插件看,安卓gradle插件适配AGP8.0
咱们能够看到,TestPlugin 的装备块修正并没有收效,依旧是上一次的装备。 到这儿我信任我们应该现已看理解了两种装备文件的装备办法到底有何差异,便是新办法设置装备,在修正后能够及时收效,老办法却不能。 终究经过我多次实验,总结得到的一个不太谨慎的结论是,老办法的装备,只要在对应的类文件发生变化,需求重新编译时才会收效。

总结

以上便是怎么开发兼容AGP8.0插件的悉数内容了。 本文详细介绍了怎么运用gradle7.4.2版别开发一个兼容gradle8.0的插件,而且剖析了怎么运用transformClassesWith注册多个转化使命,次序履行。最后剖析了新API下怎么装备插件的装备参数,与原有办法装备参数有何差异。

实例代码

本篇悉数代码可见:supportAGP8.0

参阅链接

Transform 被废弃,ASM 怎么适配?

现在准备好告别Transform了吗? | 拥抱AGP7.0

神策数据官方 Android 埋点插件