Administrator
发布于 2026-08-25 / 27 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 56 篇:自定义 Gradle 插件与 AGP API:从 transformClassesWith 说起

上一篇讲了 AMS/WMS 系统服务,回到编译期生态——Gradle 插件是 Android 工程效率的底层能力。学会写自定义插件,能把团队里"人肉执行"的流程一键自动化。这一篇从 AGP 的字节码插桩 API 入手。

Gradle 插件的能力边界

Gradle 插件能做的事情:

时机

能做什么

配置阶段

修改 Project 配置、添加 task 依赖

执行阶段

改 task 输入输出、修改字节码、生成资源

产物阶段

修改 APK/AAR/dex 内容

常见的应用:

  • 自动打版本号、修改包名

  • 编译时生成代码(KSP、kapt)

  • 字节码插桩

  • 一键签名打包、自动化截图

  • 上传 Maven 私有仓库

最简单的 Gradle 插件

// buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
    gradlePluginPortal()
}
// buildSrc/src/main/kotlin/MyPlugin.kt
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.task("helloWorld") {
            group = "mygroup"
            description = "打印 hello"
            doLast {
                println("Hello from MyPlugin!")
            }
        }
    }
}

使用:

./gradlew helloWorld

放在 buildSrc 里就够了。复杂插件再独立 module。

注册 Transform API

字节码插桩接入 AGP 8.x 之后,要用 AndroidComponentsExtension 配合 AsmClassVisitorFactory。

// app/build.gradle.kts
plugins {
    id("com.android.application")
    id("com.example.transform-plugin")
}

androidComponents {
    onVariants(selector().all()) { variant ->
        // 注册字节码插桩
        variant.instrumentation.transformClassesWith(
            MethodTracerVisitorFactory::class.java,
            InstrumentationScope.PROJECT
        ) { params ->
            params.logLevel.set("INFO")
        }

        // 必须设置栈帧计算模式,否则 ASM 改的代码 JVM 跑不起来
        variant.instrumentation.setAsmFramesComputationMode(
            FramesComputationMode.COPY_FRAMES
        )
    }
}

三个 InstrumentationScope:

Scope

含义

PROJECT

只处理本项目的类

ALL

处理本项目 + 所有依赖

EXTERNAL

只处理外部依赖(第三方库)

写一个 ASM Visitor Factory

// buildSrc/src/main/kotlin/MethodTracerVisitorFactory.kt
import com.android.build.api.instrumentation.*
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes

interface MethodTracerParams : InstrumentationParameters {
    @get:PropertyName("logLevel")
    val logLevel: Property<String>
}

abstract class MethodTracerVisitorFactory : AsmClassVisitorFactory<MethodTracerParams> {

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        val logLevel = parameters.get().logLevel.get()
        return MethodTracerClassVisitor(nextClassVisitor, classContext.fullClassName, logLevel)
    }

    override fun isInstrumentable(classData: ClassData): Boolean {
        // 排除生成的类、注解、接口
        val name = classData.className
        if (name.contains("$\$")) return false
        if (classData.annotations.contains("Generated")) return false
        return true
    }
}

class MethodTracerClassVisitor(
    next: ClassVisitor,
    private val className: String,
    private val logLevel: String
) : ClassVisitor(Opcodes.ASM9, next) {

    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)

        // 抽象方法、构造方法跳过
        if (access and Opcodes.ACC_ABSTRACT != 0) return mv
        if (name == "<init>") return mv
        // private 方法通常不需要统计
        if (access and Opcodes.ACC_PRIVATE != 0) return mv

        return MethodTracerMethodVisitor(mv, className, name)
    }
}

class MethodTracerMethodVisitor(
    next: MethodVisitor,
    private val className: String,
    private val methodName: String
) : MethodVisitor(Opcodes.ASM9, next) {

    private var startIndex: Int = -1

    override fun visitCode() {
        // 在方法开始插入 System.nanoTime()
        super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
        super.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "java/lang/System",
            "nanoTime",
            "()J"
        )
        startIndex = newLocal(org.objectweb.asm.Type.LONG_TYPE)
        super.visitVarInsn(Opcodes.LSTORE, startIndex)
        super.visitCode()
    }

    override fun visitInsn(opcode: Int) {
        // 在 RETURN 系列指令前插入日志
        if (isReturn(opcode)) {
            val label = super.visitLabel()
            super.visitVarInsn(Opcodes.LLOAD, startIndex)
            super.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "java/lang/System",
                "nanoTime",
                "()J"
            )
            super.visitInsn(Opcodes.LSUB)
            super.visitLdcInsn("$className.$methodName")
            super.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                "com/example/MethodLogger",
                "log",
                "(JLjava/lang/String;)V"
            )
        }
        super.visitInsn(opcode)
    }

    private fun isReturn(opcode: Int) = opcode in setOf(
        Opcodes.RETURN, Opcodes.IRETURN, Opcodes.LRETURN,
        Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN
    )
}

运行时记录 SDK:

// app/src/main/kotlin/MethodLogger.kt
object MethodLogger {
    @JvmStatic
    fun log(startNanos: Long, methodName: String) {
        val cost = (System.nanoTime() - startNanos) / 1_000_000
        if (cost > 16) { // 大于 1 帧 16ms 的方法记录
            Log.w("MethodTrace", "$methodName took ${cost}ms")
        }
    }
}

插件发布的两种姿势

内嵌 buildSrc

适合"只在这一个项目用"的插件:

project/
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotlin/MyPlugin.kt
└── app/
    └── build.gradle.kts

独立插件 module(发布到 Maven)

适合"多个项目复用"的插件:

// plugin/build.gradle.kts
plugins {
    `java-gradle-plugin`
    `kotlin-dsl`
    `maven-publish`
}

gradlePlugin {
    plugins {
        register("myPlugin") {
            id = "com.example.transform-plugin"
            implementationClass = "com.example.MyPlugin"
        }
    }
}

publishing {
    publications {
        register<MavenPublication>("plugin") {
            from(components["javaGradlePlugin"])
        }
    }
}

发布到私有 Maven:

./gradlew :plugin:publish -Purl=https://maven.example.com/...

调试 Gradle 插件

插件 bug 比业务代码难排查。几个技巧:

# 1. 显示所有 plugin 日志
./gradlew app:assembleDebug --info

# 2. 调试模式(加断点时用)
./gradlew app:assembleDebug -Dorg.gradle.debug=true

# 3. Profile 整个构建看哪一步慢
./gradlew app:assembleDebug --profile
# 生成 build/report/profile/index.html

# 4. 在 ID 里 attach debug
# Run > Edit Configurations > + Remote

--scan 用 build scan 上传到 Gradle 官方分析平台(很强大但要联网)。

实战技巧

  1. 修改字节码前先备份:gradle clean 后从 build/intermediates 里拿原始 class

  2. 配置缓存要小心:transformClassesWith 调用不应该有 dependsOn 之类的依赖,会破坏增量

  3. scope 要选对:只处理自己的代码用 PROJECT,全埋点用 ALL

  4. 类名前缀:自己插件生成的类加前缀(比如 __MyPlugin_xxx),避免业务代码冲突

不写插件也能做的事

不是所有需求都要写插件。先问几个问题:

  • 能不能用现有插件(JaCoCo、ProGuard、ktlint)?

  • 能不能用 ./gradlew task + shell 脚本?

  • 能不能在 build.gradle 里写 DSL?

只有重复性高、跨项目、必须自动化的事才值得写插件。

小结

Gradle 插件在 Android 工程效率里是"硬通货"。从简单的版本号注入,到字节码插桩、自动化部署,都需要这套基础。AGP 8.x 的 AsmClassVisitorFactory 是当前正确姿势。理解 InstrumentationScope、栈帧计算模式、增量编译兼容性,写出稳定可推广的工程插件。

下一篇聊聊 Android 车载系统(Android Automotive)开发,这是车机场景的全新赛道。


评论