上一篇讲了 AMS/WMS 系统服务,回到编译期生态——Gradle 插件是 Android 工程效率的底层能力。学会写自定义插件,能把团队里"人肉执行"的流程一键自动化。这一篇从 AGP 的字节码插桩 API 入手。
Gradle 插件的能力边界
Gradle 插件能做的事情:
常见的应用:
自动打版本号、修改包名
编译时生成代码(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:
写一个 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 官方分析平台(很强大但要联网)。
实战技巧
修改字节码前先备份:gradle clean 后从 build/intermediates 里拿原始 class
配置缓存要小心:
transformClassesWith调用不应该有 dependsOn 之类的依赖,会破坏增量scope 要选对:只处理自己的代码用 PROJECT,全埋点用 ALL
类名前缀:自己插件生成的类加前缀(比如
__MyPlugin_xxx),避免业务代码冲突
不写插件也能做的事
不是所有需求都要写插件。先问几个问题:
能不能用现有插件(JaCoCo、ProGuard、ktlint)?
能不能用
./gradlew task+ shell 脚本?能不能在 build.gradle 里写 DSL?
只有重复性高、跨项目、必须自动化的事才值得写插件。
小结
Gradle 插件在 Android 工程效率里是"硬通货"。从简单的版本号注入,到字节码插桩、自动化部署,都需要这套基础。AGP 8.x 的 AsmClassVisitorFactory 是当前正确姿势。理解 InstrumentationScope、栈帧计算模式、增量编译兼容性,写出稳定可推广的工程插件。
下一篇聊聊 Android 车载系统(Android Automotive)开发,这是车机场景的全新赛道。