手把手教你在 AGP 8.+ 上发布插件和代码插桩
本篇文章算是开发 AGP 插件的新手教程,大佬就直接跳过了,构建的脚本基于 Kotlin DSL,开发插件语言基于 Kotlin,插桩使用的接口是 AGP 8.+ 以上的新接口。
OK,准备好了就开始吧。
如何发布一个 AGP 插件
创建 Module 和添加依赖
AGP 插件是属于 Kotlin/Java Library,我们构建一个 Plugin 的 module:

然后我列一下我的 libs.versions 文件,现在 Android 官方都推荐通过这个来管理依赖的库和插件,不了解的同学去找一下相关的资料,很简单的。
text
[versions]
agp = "8.3.1"
kotlin = "1.9.0"
// ...
asm = "9.6"
tansDemo = "1.0.0"
[libraries]
// ...
agp-core = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
agp-api = { group = "com.android.tools.build", name = "gradle-api", version.ref = "agp" }
asm = { group = "org.ow2.asm", name = "asm", version.ref = "asm" }
asm-commons = { group = "org.ow2.asm", name = "asm-commons", version.ref = "asm" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
jetbrainsKotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
tansDemo = { id = "com.tans.agpplugin", version.ref = "tansDemo" }
Kotlin
dependencies {
implementation(libs.agp.core)
implementation(libs.agp.api)
implementation(libs.asm)
implementation(libs.asm.commons)
}
我们的插件开发需要依赖 AGP 和 ASM 库,我使用的版本分别是 8.3.1 和 9.6。
定义一个插件
首先我们需要定义一个插件的实现类,只需要继承 org.gradle.api.Plugin,范形参数的类型是 org.gradle.api.Project:
Kotlin
class TansPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("Hello, here is tans plugin..")
}
}
插件是定义好了,但是我们得让 Gralde 知道我们定义了这么一个插件,通常的做法是创建一个文件来标记这个插件,很多人也都是这么做的,其实我们有更好的方法来处理,我们可以通过 java-gradle-plugin 插件来简化这个过程,只需要在 kts 脚本中声明就好了。
Kotlin
plugins {
id("java-library")
id("java-gradle-plugin")
// ..
}
// ...
gradlePlugin {
plugins {
val myPlugin = this.create("TansPlugin")
myPlugin.id = properties["GROUP_ID"].toString()
myPlugin.implementationClass = "com.tans.agpplugin.plugin.TansPlugin"
}
}
// ...
只需要通过上面的代码,指定 Plugin 的 id 和实现的类就好了,我的插件的 id 是 com.tans.agpplugin,写在 gradle.properties 文件中。
到这里其实一个 Gradle 插件就写好了,把编译好的 jar 文件添加到特定的目录下,然后指定特定的目录,然后在需要用的地方添加好我们定义的 Plugin 的 id 就可以使用了,如果没有任何问题,我们就可以在编译的控制台中看到我们输出的 Hello, here is tans plugin.. 文本。虽然使用没有问题,但是用起来特别麻烦,我们不能到处拷贝 jar 包吧?这是多么落伍的方式,通常的做法是我们把我们的库发布到远端的 maven 仓库中,那么如何发布到 maven 仓库中呢?
将我们的插件发布到 maven 仓库
无论是 jar 包还是 aar 包,发布到 maven 仓库的方式都是类似的。
首先我们要添加 maven-publish 插件:
Kotlin
plugins {
// ...
id("maven-publish")
// ...
}
然后添加一个 publishing 的闭包:
Kotlin
publishing {
// ...
}
首先我们需要在 pulishing 的闭包中添加需要上报的 maven 仓库:
Kotlin
publishing {
repositories {
// Local
maven {
name = "LocalMaven"
url = uri(localMavenDir.canonicalPath)
}
// // Remote
// maven {
// name = "RemoteMaven"
// credentials {
// username = ""
// password = ""
// }
// url = uri("")
// }
}
}
我定义的是一个本地目录的 maven,名字叫 LocalMaven,本地的目录是项目下的 maven 目录,我注释的代码是添加远端 maven 的,其中还包含认证的用户名和密码。
接下来我们需要定义一个 MavenPublication 来描述我们上传的的库:
Kotlin
publishing {
// ...
publications {
val defaultPublication: MavenPublication = this.create("Default", MavenPublication::class.java)
with(defaultPublication) {
groupId = properties["GROUP_ID"].toString()
artifactId = properties["PLUGIN_ARTIFACT_ID"].toString()
version = properties["VERSION"].toString()
// ...
}
}
}
// ..
我们创建的 Publication 的名字叫 Default,我们指定了对应的发布库时需要的 groupId,artifactId 和 version。 对应到我们库的值就分别是:com.tans.agpplugin,com.tans.agpplugin.gradle.plugin 和 1.0.0。简写就是 com.tans.agpplugin:com.tans.agpplugin.gradle.plugin:1.0.0。
添加我们的源码和对应的打包任务:
Kotlin
publishing {
// ...
val sourceJar by tasks.creating(Jar::class.java) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}
publications {
val defaultPublication: MavenPublication = this.create("Default", MavenPublication::class.java)
with(defaultPublication) {
// ...
// For aar
// afterEvaluate {
// artifact(tasks.getByName("bundleReleaseAar"))
// }
afterEvaluate {
artifact(tasks.getByName("jar"))
}
// source Code.
artifact(sourceJar)
}
}
}
// ..
我们的打包的 task 是 jar,所以添加以下代码来定义我们发布所依赖的打包任务(如果是 aar 的打包任务就是 bundleReleaseAar):
Kotlin
afterEvaluate {
artifact(tasks.getByName("jar"))
}
我们还上传了源码文件,这个也是可以不上传的,这个都取决于你自己,如果上传了源码文件,别人在使用你的库的时候,点进去方法就还能看到源码的实现。
Kotlin
val sourceJar by tasks.creating(Jar::class.java) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}
// source Code.
artifact(sourceJar)
如果要显得你更加专业,你还可以添加库的名字,库的描述,开源协议,开发者信息等等:
Kotlin
publishing {
// ...
publications {
val defaultPublication: MavenPublication = this.create("Default", MavenPublication::class.java)
with(defaultPublication) {
// ...
pom {
name = "tans-plugin"
description = "Plugin demo for AGP."
licenses {
license {
name = "The Apache License, Version 2.0"
url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
developers {
developer {
id = "Tans5"
name = "Tans Tan"
email = "tans.tan096@gmail.com"
}
}
}
// ...
}
}
}
// ..
还有一件非常重要的事情要做,那就是需要添加我们的库的依赖信息,因为我们如果不告诉使用库的人我们的库还依赖了哪些别的库,在使用过程中就可能会出现 ClassNotFound 的异常。
Kotlin
publishing {
// ...
publications {
// ...
pom.withXml {
val dependencies = asNode().appendNode("dependencies")
configurations.implementation.get().allDependencies.all {
val dependency = this
if (dependency.group == null || dependency.version == null) {
return@all
}
val dependencyNode = dependencies.appendNode("dependency")
dependencyNode.appendNode("groupId", dependency.group)
dependencyNode.appendNode("artifactId", dependency.name)
dependencyNode.appendNode("version", dependency.version)
dependencyNode.appendNode("scope", "implementation")
}
}
}
}
}
// ..
到这里 maven 的上报信息就配置好了,我再给一下完整的 gradle.kts 脚本文件:
Kotlin
plugins {
id("java-library")
id("java-gradle-plugin")
id("maven-publish")
alias(libs.plugins.jetbrainsKotlinJvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
gradlePlugin {
plugins {
val myPlugin = this.create("TansPlugin")
myPlugin.id = properties["GROUP_ID"].toString()
myPlugin.implementationClass = "com.tans.agpplugin.plugin.TansPlugin"
}
}
dependencies {
implementation(libs.agp.core)
implementation(libs.agp.api)
implementation(libs.asm)
implementation(libs.asm.commons)
}
val localMavenDir = File(rootProject.rootDir, "maven")
if (!localMavenDir.exists()) {
localMavenDir.mkdirs()
}
publishing {
repositories {
// Local
maven {
name = "LocalMaven"
url = uri(localMavenDir.canonicalPath)
}
// // Remote
// maven {
// name = "RemoteMaven"
// credentials {
// username = ""
// password = ""
// }
// url = uri("")
// }
}
val sourceJar by tasks.creating(Jar::class.java) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}
publications {
val defaultPublication: MavenPublication = this.create("Default", MavenPublication::class.java)
with(defaultPublication) {
groupId = properties["GROUP_ID"].toString()
artifactId = properties["PLUGIN_ARTIFACT_ID"].toString()
version = properties["VERSION"].toString()
// For aar
// afterEvaluate {
// artifact(tasks.getByName("bundleReleaseAar"))
// }
// jar
// artifact("${layout.buildDirectory.asFile.get().absolutePath}${File.separator}libs${File.separator}plugin.jar")
afterEvaluate {
artifact(tasks.getByName("jar"))
}
// source Code.
artifact(sourceJar)
pom {
name = "tans-plugin"
description = "Plugin demo for AGP."
licenses {
license {
name = "The Apache License, Version 2.0"
url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
}
}
developers {
developer {
id = "Tans5"
name = "Tans Tan"
email = "tans.tan096@gmail.com"
}
}
}
pom.withXml {
val dependencies = asNode().appendNode("dependencies")
configurations.implementation.get().allDependencies.all {
val dependency = this
if (dependency.group == null || dependency.version == null) {
return@all
}
val dependencyNode = dependencies.appendNode("dependency")
dependencyNode.appendNode("groupId", dependency.group)
dependencyNode.appendNode("artifactId", dependency.name)
dependencyNode.appendNode("version", dependency.version)
dependencyNode.appendNode("scope", "implementation")
}
}
}
}
}
//project.afterEvaluate {
// val buildTask = tasks.getByName("build")
// tasks.all {
// if (group == "publishing") {
// this.dependsOn(buildTask)
// }
// }
//}
如果你的配置没有问题,在 gradle 执行 sync 过后就能够看到以下的发布任务:

这个任务的名字是根据我们定义的 maven 仓库名字(LocalMaven)和 publication 的名字 (Default) 生成的。
如果你的配置没有问题,执行完成后就能够在项目的 maven 目录下看到以下的内容:

在应用中使用我们的插件
在 settings.kts 中添加我们本地的 maven 仓库:
Kotlin
pluginManagement {
repositories {
// ...
maven {
url = uri(".${File.separator}maven")
}
}
}
在 Project 级别的 build.kts 中添加我们的插件依赖:
Kotlin
plugins {
// ...
alias(libs.plugins.tansDemo) apply false
}
然后在我们的 app 的 module 中的 build.kts 中引用插件:
Kotlin
plugins {
// ...
alias(libs.plugins.tansDemo)
}
如果你的步骤没有错的话,这个时候 sync 项目的时候就能够看到我们插件打印的内容了。
使用 AGP 新的接口来完成插桩
Kotlin
class TansPlugin : Plugin<Project> {
override fun apply(project: Project) {
val appPlugin = try {
project.plugins.getPlugin("com.android.application")
} catch (e: Throwable) {
null
}
if (appPlugin != null) {
Log.d(msg = "Find android app plugin")
val androidExt = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidExt.onVariants { variant ->
Log.d(msg = "variant=${variant.name}")
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
variant.instrumentation.transformClassesWith(AndroidActivityClassVisitorFactory::class.java, InstrumentationScope.ALL) {}
}
} else {
Log.e(msg = "Do not find android app plugin.")
}
}
}
上面的代码首先通过判断是否有 com.android.application 插件来判断该 module 是否是一个 Android App 的 moudle,我们只处理 Android APP。
然后通过 project.extensions.getByType(AndroidComponentsExtension::class.java) 来拿到 Android 的 Extension,通过他的 onVariants() 方法来遍历所有的变体信息,然后通过他的 instrumentation 对象来处理插桩的参数。
通过方法 variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES) 选择方法 Frame 的计算方式和插桩时 MaxStack 的计算方式,我们选择直接复制原来的方法中的这两个值。
通过方法 variant.instrumentation.transformClassesWith(AndroidActivityClassVisitorFactory::class.java, InstrumentationScope.ALL) {} 来注册插桩,第一个参数就是我们定义的插桩实现,他必须是抽象的对象,第二个参数是插桩的范围,可以选择只插桩应用字节码,也可以选择也包含库的字节码,我们选择的是都插桩。
我们再来看看我们的 AndroidActivityClassVisitorFactory 的实现:
Kotlin
abstract class AndroidActivityClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return AndroidActivityClassVisitor(classContext, nextClassVisitor)
}
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.superClasses.contains("android.app.Activity")
}
}
createClassVisitor() 方法就是创建我们自定义的 ClassVisitor 这也没有什么好说的了,会插桩的同学对这个方法一定不陌生。
isInstrumentable() 方法是用来判断是不是需要插桩该 class,这个 ClassData 对象简直太好用了,他包含了当前类的所有继承信息接口信息等等:
Kotlin
interface ClassData {
/**
* Fully qualified name of the class.
*/
val className: String
/**
* List of the annotations the class has.
*/
val classAnnotations: List<String>
/**
* List of all the interfaces that this class or a superclass of this class implements.
*/
val interfaces: List<String>
/**
* List of all the super classes that this class or a super class of this class extends.
*/
val superClasses: List<String>
}
要是以前的接口我们需要,先通过 ClassVistor 先扫描一遍才能够获取类的继承信息,看到这里我的眼泪和鼻涕一起流了出来 T_T,现在使用起来太简单了。
我这里列出来一下我的插桩代码:
Kotlin
class AndroidActivityClassVisitor(
private val classContext: ClassContext,
outputVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM9, outputVisitor) {
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
super.visit(version, access, name, signature, superName, interfaces)
Log.d(msg = "-----------------------------")
Log.d(msg = "name=${name.moveAsmTypeClassNameToSourceCode()}, signature=${signature}, superName=${superName.moveAsmTypeClassNameToSourceCode()}, interfaces=${interfaces?.map { it.moveAsmTypeClassNameToSourceCode() }}")
Log.d(msg = "Parents:")
val parents = classContext.currentClassData.superClasses
for (p in parents) {
println(" $p")
}
Log.d(msg = "-----------------------------")
}
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)!!
return AndroidActivityMethodVisitor(
classContext = classContext,
outputVisitor = mv,
access = access,
name = name!!,
des = descriptor!!
)
}
companion object {
fun String?.moveAsmTypeClassNameToSourceCode(): String? {
return this?.replace("/", ".")
}
}
}
Kotlin
class AndroidActivityMethodVisitor(
private val classContext: ClassContext,
private val outputVisitor: MethodVisitor,
access: Int,
private val name: String,
des: String
) : AdviceAdapter(
ASM9,
outputVisitor,
access,
name,
des
) {
override fun onMethodEnter() {
super.onMethodEnter()
Log.d(msg = "Hook method in: className=${classContext.currentClassData.className}, method=${name}")
outputVisitor.visitMethodInsn(
Opcodes.INVOKESTATIC,
METHOD_IN_OUT_HOOK_CLASS_NAME,
IN_HOOK_METHOD_NAME,
IN_HOOK_METHOD_DES,
false
)
}
override fun onMethodExit(opcode: Int) {
Log.d(msg = "Hook method out: className=${classContext.currentClassData.className}, method=${name}")
outputVisitor.visitMethodInsn(
Opcodes.INVOKESTATIC,
METHOD_IN_OUT_HOOK_CLASS_NAME,
OUT_HOOK_METHOD_NAME,
OUT_HOOK_METHOD_DES,
false
)
super.onMethodExit(opcode)
}
companion object {
const val METHOD_IN_OUT_HOOK_CLASS_NAME = "com/tans/agpplugin/demo/MethodInOutHook"
const val IN_HOOK_METHOD_NAME = "methodIn"
const val IN_HOOK_METHOD_DES = "()V"
const val OUT_HOOK_METHOD_NAME = "methodOut"
const val OUT_HOOK_METHOD_DES = "()V"
}
}
如果熟悉 Android 插桩的同学,看我上面的代码应该是没有一点压力。其实就是在 Activity 的所有方法中开始时和结束时分别调用 com.tans.agpplugin.demo.MethodInOutHook#methodIn() 方法和 com.tans.agpplugin.demo.MethodInOutHook#methodOut() 方法。
我认为要使用好 ASM 插桩,首先得学习好 Jvm 字节码,我之前有文章介绍过:JVM 字节码。
我前面文章还介绍过旧版的 AGP 插桩和一些插桩的实现场景: 手把手教你通过 AGP + ASM 实现 Android 应用插桩
最后
当我使用过 Kotlin DSL 后,我再也不想碰 Groovy,使用 Kotlin DSL 来写 Gradle 构建脚本真的要人性化很多。
新版的 AGP 插桩接口也是要比旧版的插桩接口要简化了太多了,新版的不用管增量编译还是全量编译,也不用管是 Class 文件还是 Jar 文件,也不用手动创建修改后的 Class 文件和 Jar 文件,也不用再手动扫描类的继承关系了,新版的插桩你只需要实现你自己的 ClassVisitor 就好了。
最后还忘了一点,源码在这里,如果觉得对你有帮助,欢迎 Star:agpplugin-demo