手把手教你在 AGP 8.+ 上发布插件和代码插桩
本篇文章算是开发 AGP 插件的新手教程,大佬就直接跳过了,构建的脚本根据 Kotlin DSL,开发插件语言根据 Kotlin,插桩运用的接口是 AGP 8.+ 以上的新接口。
OK,预备好了就开始吧。
如何发布一个 AGP 插件
创立 Module 和增加依靠
AGP 插件是属于 Kotlin/Java Library,咱们构建一个 Plugin 的 module:
然后我列一下我的 libs.versions 文件,现在 Android 官方都推荐经过这个来办理依靠的库和插件,不了解的同学去找一下相关的资料,很简略的。
[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" }
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:
class TansPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("Hello, here is tans plugin..")
}
}
插件是界说好了,可是咱们得让 Gralde 知道咱们界说了这么一个插件,通常的做法是创立一个文件来标记这个插件,许多人也都是这么做的,其实咱们有更好的办法来处理,咱们能够经过 java-gradle-plugin 插件来简化这个过程,只需求在 kts 脚本中声明就好了。
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 插件:
plugins {
// ...
id("maven-publish")
// ...
}
然后增加一个 publishing 的闭包:
publishing {
// ...
}
首要咱们需求在 pulishing 的闭包中增加需求上报的 maven 库房:
publishing {
repositories {
// Local
maven {
name = "LocalMaven"
url = uri(localMavenDir.canonicalPath)
}
// // Remote
// maven {
// name = "RemoteMaven"
// credentials {
// username = ""
// password = ""
// }
// url = uri("")
// }
}
}
我界说的是一个本地目录的 maven,姓名叫 LocalMaven,本地的目录是项目下的 maven 目录,我注释的代码是增加远端 maven 的,其中还包括认证的用户名和密码。
接下来咱们需求界说一个 MavenPublication 来描绘咱们上传的的库:
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。
增加咱们的源码和对应的打包使命:
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):
afterEvaluate {
artifact(tasks.getByName("jar"))
}
咱们还上传了源码文件,这个也是能够不上传的,这个都取决于你自己,假如上传了源码文件,别人在运用你的库的时分,点进去办法就还能看到源码的实现。
val sourceJar by tasks.creating(Jar::class.java) {
archiveClassifier.set("sources")
from(sourceSets.getByName("main").allSource)
}
// source Code.
artifact(sourceJar)
假如要显得你更加专业,你还能够增加库的姓名,库的描绘,开源协议,开发者信息等等:
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 的异常。
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 脚本文件:
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 库房:
pluginManagement {
repositories {
// ...
maven {
url = uri(".${File.separator}maven")
}
}
}
在 Project 等级的 build.kts 中增加咱们的插件依靠:
plugins {
// ...
alias(libs.plugins.tansDemo) apply false
}
然后在咱们的 app 的 module 中的 build.kts 中引用插件:
plugins {
// ...
alias(libs.plugins.tansDemo)
}
假如你的步骤没有错的话,这个时分 sync 项目的时分就能够看到咱们插件打印的内容了。
运用 AGP 新的接口来完成插桩
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 的实现:
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 目标几乎太好用了,他包括了当前类的一切承继信息接口信息等等:
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,现在运用起来太简略了。
我这儿列出来一下我的插桩代码:
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("/", ".")
}
}
}
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



