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

手把手教你在 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

相关推荐
jason.zeng@150220716 分钟前
使用vscode+flutter开发android程序(入门)
android·vscode·flutter
事圆则缓20 分钟前
Kotlin学习导航:从知识树走到项目与面试
android·开发语言·kotlin
天空之城--1 小时前
近一周Android开发动态:AI编码、Compose与自适应实践
android
mmsx1 小时前
Android 混淆挡不住二次打包:一条完整的攻击链与突破口
android·混淆
聚美智数10 小时前
智能结构化识别-图片结构解析-图像结构化解析-照片结构化解析
android·java·数据库
mmsx15 小时前
Android 插件化架构:40 个 Gradle 模块的拆分、通信与共存
android·kotlin
mmsx15 小时前
Android 不改一行源码:用 AspectJ 做三类拦截
android·kotlin
TST-魚爷15 小时前
易视讯会议摄像头 + 全向麦克风实操讲解稿
android·电脑·钉钉·腾讯会议·zoom会议
嘟哩DuliDuli16 小时前
AI创作工作区的项目上下文与多模态任务边界
android·人工智能·安全·ai·软件工程