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

1,286 阅读6分钟

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

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

OK,准备好了就开始吧。

如何发布一个 AGP 插件

创建 Module 和添加依赖

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

create_module.png

然后我列一下我的 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 过后就能够看到以下的发布任务:

publising_task.png

这个任务的名字是根据我们定义的 maven 仓库名字(LocalMaven)和 publication 的名字 (Default) 生成的。

如果你的配置没有问题,执行完成后就能够在项目的 maven 目录下看到以下的内容:

maven_dir.png

在应用中使用我们的插件

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