手把手教你在 AGP 8.+ 上发布插件和代码插桩

本篇文章算是开发 AGP 插件的新手教程,大佬就直接跳过了,构建的脚本根据 Kotlin DSL,开发插件语言根据 Kotlin,插桩运用的接口是 AGP 8.+ 以上的新接口。

OK,预备好了就开始吧。

如何发布一个 AGP 插件

创立 Module 和增加依靠

AGP 插件是属于 Kotlin/Java Library,咱们构建一个 Pluginmodule

手把手教你在 AGP 8.+ 上发布插件和代码插桩

然后我列一下我的 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)
}

咱们的插件开发需求依靠 AGPASM 库,我运用的版别别离是 8.3.19.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"
    }
}
// ...

只需求经过上面的代码,指定 Pluginid 和实现的类就好了,我的插件的 idcom.tans.agpplugin,写在 gradle.properties 文件中。

到这儿其实一个 Gradle 插件就写好了,把编译好的 jar 文件增加到特定的目录下,然后指定特定的目录,然后在需求用的当地增加好咱们界说的 Pluginid 就能够运用了,假如没有任何问题,咱们就能够在编译的控制台中看到咱们输出的 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,咱们指定了对应的发布库时需求的 groupIdartifactIdversion。 对应到咱们库的值就别离是:com.tans.agpplugincom.tans.agpplugin.gradle.plugin1.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)
       }
    }
}  
// ..

咱们的打包的 taskjar,所以增加以下代码来界说咱们发布所依靠的打包使命(假如是 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 往后就能够看到以下的发布使命:

手把手教你在 AGP 8.+ 上发布插件和代码插桩

这个使命的姓名是根据咱们界说的 maven 库房姓名(LocalMaven)和 publication 的姓名 (Default) 生成的。

假如你的装备没有问题,履行完成后就能够在项目的 maven 目录下看到以下的内容:

手把手教你在 AGP 8.+ 上发布插件和代码插桩

在运用中运用咱们的插件

settings.kts 中增加咱们本地的 maven 库房:

pluginManagement {
    repositories {
        // ...
        maven {
            url = uri(".${File.separator}maven")
        }
    }
}

在 Project 等级的 build.kts 中增加咱们的插件依靠:

plugins {
    // ...
    alias(libs.plugins.tansDemo) apply false
}

然后在咱们的 appmodule 中的 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 Appmoudle,咱们只处理 Android APP
然后经过 project.extensions.getByType(AndroidComponentsExtension::class.java) 来拿到 AndroidExtension,经过他的 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