通过 Gradle 插件做全局性能监控
一、为什么需要 Gradle 插件级别的性能监控
1.1 传统监控方案的痛点
在移动端性能监控领域,常见的接入方式大致有以下几类:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
零侵入、全覆盖、可定制 |
Gradle 插件方案的核心优势在于:在编译阶段自动修改所有字节码,业务代码无需感知监控逻辑。这是大型 App 性能基建的标准选择(Matrix、ByteX、Booster 均采用此方案)。
1.2 整体方案架构
┌─────────────────────────────────────────────────────────────────┐
│ Gradle 构建流程 │
│ │
│ .java/.kt ──javac/kotlinc──> .class ──Transform/ASM──> .class │
│ ↑ │
│ Gradle 插件注入点 │
│ │
│ 插入逻辑: │
│ • 方法入口:记录开始时间 / 调用栈 │
│ • 方法出口:计算耗时 / 上报 │
│ • 异常出口:catch 块中补报 │
│ │
│ ──D8/R8──> .dex ──打包──> .apk │
└─────────────────────────────────────────────────────────────────┘
二、Gradle 插件开发基础
2.1 三种插件组织方式
方式一:buildSrc(推荐本地开发调试)
项目根目录/
├── buildSrc/
│ ├── build.gradle.kts
│ └── src/main/
│ ├── java/
│ │ └── com/example/plugin/
│ │ ├── PerfMonitorPlugin.kt
│ │ └── PerfTransform.kt
│ └── resources/META-INF/gradle-plugins/
│ └── com.example.perf-monitor.properties
└── app/
└── build.gradle.kts
buildSrc/build.gradle.kts:
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}
dependencies {
// AGP API
implementation("com.android.tools.build:gradle:8.2.0")
// ASM 字节码操作库
implementation("org.ow2.asm:asm:9.6")
implementation("org.ow2.asm:asm-commons:9.6")
implementation("org.ow2.asm:asm-util:9.6")
}
META-INF/gradle-plugins/com.example.perf-monitor.properties:
implementation-class=com.example.plugin.PerfMonitorPlugin
方式二:独立插件模块(推荐发布共享)
项目根目录/
├── perf-monitor-plugin/ ← 独立 Gradle 模块
│ ├── build.gradle.kts
│ └── src/main/kotlin/...
├── app/
│ └── build.gradle.kts
└── settings.gradle.kts ← includeBuild("perf-monitor-plugin")
方式三:发布到 Maven(推荐多项目共享)
将插件发布到公司私有 Maven 仓库,其他项目通过 classpath 依赖引入。
2.2 插件入口类结构
// PerfMonitorPlugin.kt
classPerfMonitorPlugin : Plugin<Project> {
overridefunapply(project: Project) {
// 1. 注册插件扩展(DSL 配置)
val extension = project.extensions.create(
"perfMonitor",
PerfMonitorExtension::class.java
)
// 2. 确保只在 Android 项目中应用
project.plugins.withId("com.android.application") {
val androidExtension = project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java)
// 3. 注册 Transform(AGP 8.x 新 API)
androidExtension.onVariants { variant ->
variant.instrumentation.transformClassesWith(
PerfAsmClassVisitorFactory::class.java,
InstrumentationScope.ALL // ALL = 自身代码 + 依赖库
) { params ->
// 将插件配置传递给 Transform
params.enableMethodTrace.set(extension.enableMethodTrace)
params.enableClickMonitor.set(extension.enableClickMonitor)
params.tracePackages.set(extension.tracePackages)
}
// 设置排除规则(不插桩的类)
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}
}
}
}
2.3 插件扩展 DSL 定义
// PerfMonitorExtension.kt
abstractclassPerfMonitorExtension{
// 是否开启方法耗时追踪
var enableMethodTrace: Boolean = false
// 是否开启点击事件监控
var enableClickMonitor: Boolean = true
// 是否开启启动阶段深度追踪
var enableLaunchTrace: Boolean = true
// 方法耗时阈值(ms),超过才记录
var methodTraceThresholdMs: Long = 100L
// 需要插桩的包名前缀
var tracePackages: List<String> = emptyList()
// 排除插桩的类名前缀
var excludeClasses: List<String> = listOf(
"com/example/monitor", // 监控 SDK 本身排除,防止递归
"androidx/",
"kotlin/"
)
}
在 app/build.gradle.kts 中使用:
plugins {
id("com.example.perf-monitor")
}
perfMonitor {
enableMethodTrace = true
enableClickMonitor = true
methodTraceThresholdMs = 50L
tracePackages = listOf("com/example/myapp")
excludeClasses = listOf("com/example/myapp/monitor")
}
三、AGP Transform API 演进
3.1 历史版本对比
|
|
|
|
|---|---|---|
|
|
Transform
|
已废弃 |
|
|
AsmClassVisitorFactory
Instrumentation |
推荐 |
|
|
AsmClassVisitorFactory
|
当前标准 |
3.2 旧 Transform API(了解原理)
// 旧版,AGP 7+ 已废弃,仅做原理说明
classOldPerfTransform(privateval extension: PerfMonitorExtension)
: Transform() {
overridefungetName() = "PerfMonitorTransform"
overridefungetInputTypes(): Set<QualifiedContent.ContentType> =
setOf(QualifiedContent.DefaultContentType.CLASSES)
overridefungetScopes(): MutableSet<QualifiedContent.Scope> =
mutableSetOf(
QualifiedContent.Scope.PROJECT,
QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES
)
overridefunisIncremental() = true
overridefuntransform(transformInvocation: TransformInvocation) {
transformInvocation.inputs.forEach { input ->
// 处理 jar 包
input.jarInputs.forEach { jarInput ->
val dest = transformInvocation.outputProvider
.getContentLocation(jarInput.name, jarInput.contentTypes,
jarInput.scopes, Format.JAR)
processJar(jarInput.file, dest)
}
// 处理目录(本工程 .class)
input.directoryInputs.forEach { dirInput ->
val dest = transformInvocation.outputProvider
.getContentLocation(dirInput.name, dirInput.contentTypes,
dirInput.scopes, Format.DIRECTORY)
processDir(dirInput.file, dest)
}
}
}
}
旧 API 痛点:
-
无法感知增量,每次全量处理导致构建慢 -
需手动处理 jar/dir 两种输入,样板代码多 -
多个 Transform 串联,影响构建速度
3.3 新 AsmClassVisitorFactory API(推荐)
新 API 将”遍历 class”与”修改逻辑”解耦,AGP 负责遍历,开发者只需专注于修改逻辑:
// 参数接口 —— 用于向 Factory 传递配置
interfacePerfInstrumentationParams : InstrumentationParameters {
@get:Input
val enableMethodTrace: Property<Boolean>
@get:Input
val enableClickMonitor: Property<Boolean>
@get:Input
val tracePackages: ListProperty<String>
@get:Input
val methodTraceThresholdMs: Property<Long>
}
// Factory —— 决定哪些类需要处理,并创建对应的 ClassVisitor
abstractclassPerfAsmClassVisitorFactory
: AsmClassVisitorFactory<PerfInstrumentationParams> {
overridefuncreateClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
val params = instrumentationContext.config.get()
return PerfClassVisitor(
api = Opcodes.ASM9,
nextVisitor = nextClassVisitor,
className = classContext.currentClassData.className,
params = params
)
}
// 关键:过滤需要插桩的类(返回 false 则跳过该类,性能更好)
overridefunisInstrumentable(classData: ClassData): Boolean {
val params = instrumentationContext.config.get()
val className = classData.className
// 1. 排除系统类、三方库(按包名前缀过滤)
val excludes = listOf(
"android.", "androidx.", "kotlin.", "kotlinx.",
"com.google.", "com.example.monitor."
)
if (excludes.any { className.startsWith(it) }) returnfalse
// 2. 只处理指定包名
val packages = params.tracePackages.get()
if (packages.isNotEmpty()) {
return packages.any { className.startsWith(it) }
}
returntrue
}
}
四、核心:ClassVisitor 与 MethodVisitor
4.1 ASM 访问者模式
ASM 使用经典的访问者模式处理字节码:
ClassReader
└── ClassVisitor (我们的 PerfClassVisitor)
├── visit() → 类定义信息
├── visitField() → 字段
├── visitMethod() → 返回 MethodVisitor
│ ├── visitCode() → 方法体开始
│ ├── visitInsn() → 具体指令
│ ├── visitMethodInsn() → 方法调用指令
│ └── visitMaxs() → 栈深度/局部变量数
└── visitEnd() → 类结束
4.2 PerfClassVisitor 实现
classPerfClassVisitor(
api: Int,
nextVisitor: ClassVisitor,
privateval className: String,
privateval params: PerfInstrumentationParams
) : ClassVisitor(api, nextVisitor) {
privatevar superName: String = ""
privatevar isActivity = false
privatevar isFragment = false
overridefunvisit(
version: Int, access: Int, name: String,
signature: String?, superName: String?, interfaces: Array<String>?
) {
this.superName = superName ?: ""
this.isActivity = isSubclassOf(superName, "android/app/Activity")
this.isFragment = isSubclassOf(superName, "androidx/fragment/app/Fragment")
super.visit(version, access, name, signature, superName, interfaces)
}
overridefunvisitMethod(
access: Int, name: String, descriptor: String,
signature: String?, exceptions: Array<String>?
): MethodVisitor {
val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
// 跳过抽象方法、桥接方法、native 方法
if (access and Opcodes.ACC_ABSTRACT != 0 ||
access and Opcodes.ACC_BRIDGE != 0 ||
access and Opcodes.ACC_NATIVE != 0) {
return mv
}
// 根据配置决定使用哪种监控 Visitor
returnwhen {
params.enableMethodTrace.get() ->
MethodTraceVisitor(api, mv, access, name, descriptor, className,
params.methodTraceThresholdMs.get())
params.enableClickMonitor.get() && isClickListener(name, descriptor) ->
ClickMonitorVisitor(api, mv, access, name, descriptor, className)
isActivity && isLifecycleMethod(name) ->
ActivityLifecycleVisitor(api, mv, access, name, descriptor, className)
else -> mv
}
}
privatefunisClickListener(name: String, descriptor: String) =
name == "onClick" && descriptor == "(Landroid/view/View;)V"
privatefunisLifecycleMethod(name: String) =
name in setOf("onCreate", "onResume", "onPause", "onStop", "onDestroy")
privatefunisSubclassOf(superName: String?, targetClass: String): Boolean {
// 简化实现,实际需要递归检查类继承链
return superName == targetClass
}
}
五、监控场景一:方法耗时全量追踪
5.1 插桩逻辑设计
方法耗时插桩需要处理三种出口:
-
正常 return -
异常 throw(未捕获) -
有 try-catch 块的方法
原始字节码逻辑:
┌─────────────────────────────┐
│ methodBody │
│ RETURN │
└─────────────────────────────┘
插桩后字节码逻辑(try-finally 模式):
┌─────────────────────────────┐
│ long _startTime = TraceHelper.begin("className#method") │
│ try { │
│ methodBody │
│ } finally { │
│ TraceHelper.end(_startTime, threshold) │
│ } │
└─────────────────────────────┘
5.2 MethodTraceVisitor 实现
classMethodTraceVisitor(
api: Int,
mv: MethodVisitor,
access: Int,
privateval methodName: String,
descriptor: String,
privateval className: String,
privateval thresholdMs: Long
) : AdviceAdapter(api, mv, access, methodName, descriptor) {
// 局部变量:存储开始时间
privatevar startTimeVarIndex = -1
// try-finally 的标签
privateval startLabel = Label()
privateval endLabel = Label()
overridefunonMethodEnter() {
// 分配局部变量槽存储开始时间
startTimeVarIndex = newLocal(Type.LONG_TYPE)
// 插入:long _startTime = System.nanoTime();
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/System",
"nanoTime",
"()J",
false
)
mv.visitVarInsn(Opcodes.LSTORE, startTimeVarIndex)
// 标记 try 块开始
mv.visitLabel(startLabel)
}
overridefunonMethodExit(opcode: Int) {
if (opcode == Opcodes.ATHROW) return// 异常由 finally 处理
insertEndTrace()
}
overridefunvisitMaxs(maxStack: Int, maxLocals: Int) {
// 插入 finally 块(异常路径)
mv.visitLabel(endLabel)
// 异常对象在栈顶,先存储
val exceptionVarIndex = newLocal(Type.getType(Throwable::class.java))
mv.visitVarInsn(Opcodes.ASTORE, exceptionVarIndex)
// 执行结束追踪
insertEndTrace()
// 重新抛出异常
mv.visitVarInsn(Opcodes.ALOAD, exceptionVarIndex)
mv.visitInsn(Opcodes.ATHROW)
// 注册 try-finally 区间
mv.visitTryCatchBlock(startLabel, endLabel, endLabel, null)
super.visitMaxs(maxStack + 4, maxLocals)
}
privatefuninsertEndTrace() {
// 插入:TraceHelper.end(_startTime, "com/example/Foo#bar", thresholdMs);
mv.visitVarInsn(Opcodes.LLOAD, startTimeVarIndex)
mv.visitLdcInsn("$className#$methodName")
mv.visitLdcInsn(thresholdMs)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/TraceHelper",
"end",
"(JLjava/lang/String;J)V",
false
)
}
}
5.3 运行时 TraceHelper 实现
// TraceHelper.kt(在 app 模块的 monitor 包中)
object TraceHelper {
// 使用 ThreadLocal 支持多线程
privateval callStack = ThreadLocal<ArrayDeque<Long>>()
@JvmStatic
funbegin(tag: String): Long {
return System.nanoTime()
}
@JvmStatic
funend(startTime: Long, methodTag: String, thresholdMs: Long) {
val costNs = System.nanoTime() - startTime
val costMs = costNs / 1_000_000L
if (costMs >= thresholdMs) {
PerfReporter.reportSlowMethod(
method = methodTag,
costMs = costMs,
threadName = Thread.currentThread().name,
isMainThread = Looper.getMainLooper().thread == Thread.currentThread()
)
}
}
}
六、监控场景二:主线程卡顿精准定位
6.1 方案选型对比
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
| 插桩 + 采样 |
|
|
|
|
|
|
|
|
6.2 Looper 消息监控插桩
通过插桩在 Application.onCreate() 自动注入监控代码:
classApplicationLifecycleVisitor(
api: Int, mv: MethodVisitor, access: Int,
name: String, descriptor: String, className: String
) : AdviceAdapter(api, mv, access, name, descriptor) {
overridefunonMethodExit(opcode: Int) {
if (methodName == "onCreate") {
// 注入:BlockMonitor.install()
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/BlockMonitor",
"install",
"()V",
false
)
}
}
}
运行时 BlockMonitor:
object BlockMonitor {
privateconstval BLOCK_THRESHOLD_MS = 100L
privatevar blockStartTime = 0L
@JvmStatic
funinstall() {
Looper.getMainLooper().setMessageLogging { log ->
when {
log.startsWith(">>>>> Dispatching") -> {
blockStartTime = SystemClock.uptimeMillis()
}
log.startsWith("<<<<< Finished") -> {
val cost = SystemClock.uptimeMillis() - blockStartTime
if (cost > BLOCK_THRESHOLD_MS) {
// 采集当前主线程调用栈
val stackTrace = Looper.getMainLooper().thread
.stackTrace
.joinToString("\n") { "\t$it" }
PerfReporter.reportBlock(costMs = cost, stack = stackTrace)
}
}
}
}
}
}
6.3 高精度卡顿检测:插桩 + 后台采样
这是 Matrix-TraceCanary 的核心思路:
主线程
┌────────────────────────────────────────────────────────┐
│ 方法A进入 ──────────────────────────── 方法A退出 │
│ 方法B进入 ─────────────── 方法B退出 │
│ 方法C进入 ──── 方法C退出 │
└────────────────────────────────────────────────────────┘
↑ ↑
采样线程定时抓取主线程栈(每 20ms 一次)
插桩后每个方法在全局环形缓冲区写入:
[方法ID, 进入时间戳]
[方法ID, 退出时间戳]
当检测到卡顿时,回溯缓冲区还原调用序列
// 方法级插桩写入环形缓冲区(极低开销)
classFastMethodTraceVisitor(...) : AdviceAdapter(...) {
privateval methodId: Int = MethodIdManager.getOrAssign("$className#$methodName")
overridefunonMethodEnter() {
// 插入:MethodBuffer.i(methodId)
mv.visitLdcInsn(methodId)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/MethodBuffer",
"i", // "i" = enter,极短方法名减小开销
"(I)V",
false
)
}
overridefunonMethodExit(opcode: Int) {
// 插入:MethodBuffer.o(methodId)
mv.visitLdcInsn(methodId)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/MethodBuffer",
"o", // "o" = exit
"(I)V",
false
)
}
}
// 环形缓冲区(无锁高性能)
object MethodBuffer {
privateconstval BUFFER_SIZE = 1 shl 17// 131072 条记录
// 每条记录 = methodId(int) + 进出标记(1bit) + 时间戳(long)
// 使用 long[] 存储:高 32 位 = methodId | 进出标记,低 32 位 = 时间戳低位
privateval buffer = LongArray(BUFFER_SIZE)
privateval index = AtomicInteger(0)
@JvmStatic
funi(methodId: Int) {
val idx = index.getAndIncrement() and (BUFFER_SIZE - 1)
buffer[idx] = encodeRecord(methodId, isEnter = true)
}
@JvmStatic
funo(methodId: Int) {
val idx = index.getAndIncrement() and (BUFFER_SIZE - 1)
buffer[idx] = encodeRecord(methodId, isEnter = false)
}
privatefunencodeRecord(methodId: Int, isEnter: Boolean): Long {
val flag = if (isEnter) 0Lelse (1L shl 32)
return flag or (methodId.toLong() and 0xFFFFFFFFL) or
(SystemClock.uptimeMillis() shl 33)
}
fundumpAndAnalyze(): List<SlowMethodRecord> {
// 导出缓冲区,还原调用树,找到耗时方法
TODO("实际实现见下文")
}
}
七、监控场景三:启动耗时精准分段
7.1 启动阶段定义
App 启动时间线:
─────────────────────────────────────────────────────────────────────
进程创建 Application.attachBaseContext Application.onCreate Activity.onCreate
│ │ │ │
│<── 进程启动 ──>│<────── Application 阶段 ───>│<──── Activity 阶段 ─>│
│ │ │ │
│ First Frame
│
│<─────── TTFD ──────>│
│<──────────────────── TTID ─────────>│
7.2 通过插桩自动埋点
目标:无需业务代码感知,自动在关键生命周期方法插入计时点。
// ApplicationTraceVisitor.kt
classApplicationTraceVisitor(...) : AdviceAdapter(...) {
overridefunonMethodEnter() {
val tracePoint = when (methodName) {
"attachBaseContext" -> "app_attachBaseContext"
"onCreate" -> "app_onCreate"
else -> return
}
insertTraceBegin(tracePoint)
}
overridefunonMethodExit(opcode: Int) {
val tracePoint = when (methodName) {
"attachBaseContext" -> "app_attachBaseContext"
"onCreate" -> "app_onCreate"
else -> return
}
insertTraceEnd(tracePoint)
}
privatefuninsertTraceBegin(tag: String) {
mv.visitLdcInsn(tag)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/LaunchTracer",
"begin",
"(Ljava/lang/String;)V",
false
)
}
privatefuninsertTraceEnd(tag: String) {
mv.visitLdcInsn(tag)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/LaunchTracer",
"end",
"(Ljava/lang/String;)V",
false
)
}
}
Activity 生命周期监控 — 重点追踪首个 Activity:
classActivityLifecycleVisitor(
api: Int, mv: MethodVisitor, access: Int,
name: String, descriptor: String,
privateval className: String
) : AdviceAdapter(api, mv, access, name, descriptor) {
overridefunonMethodEnter() {
mv.visitLdcInsn(className)
mv.visitLdcInsn(methodName)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/LaunchTracer",
"onActivityMethodEnter",
"(Ljava/lang/String;Ljava/lang/String;)V",
false
)
}
overridefunonMethodExit(opcode: Int) {
mv.visitLdcInsn(className)
mv.visitLdcInsn(methodName)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/LaunchTracer",
"onActivityMethodExit",
"(Ljava/lang/String;Ljava/lang/String;)V",
false
)
}
}
7.3 LaunchTracer 实现
object LaunchTracer {
dataclassTraceEvent(
val tag: String,
val timestampMs: Long,
val isEnter: Boolean
)
privateval events = CopyOnWriteArrayList<TraceEvent>()
privatevar isFirstFrameReported = false
@JvmStatic
funbegin(tag: String) {
events.add(TraceEvent(tag, SystemClock.uptimeMillis(), isEnter = true))
}
@JvmStatic
funend(tag: String) {
events.add(TraceEvent(tag, SystemClock.uptimeMillis(), isEnter = false))
}
@JvmStatic
funonActivityMethodEnter(className: String, methodName: String) {
begin("$className#$methodName")
}
@JvmStatic
funonActivityMethodExit(className: String, methodName: String) {
end("$className#$methodName")
}
// 在首帧渲染完成时调用(通过 Choreographer 监听)
funonFirstFrameRendered() {
if (isFirstFrameReported) return
isFirstFrameReported = true
val report = buildReport()
PerfReporter.reportLaunchTrace(report)
}
privatefunbuildReport(): LaunchReport {
// 分析事件列表,计算各阶段耗时
val appOnCreateCost = calcCost("app_onCreate")
val firstActivityOnCreateCost = events
.filter { it.tag.contains("#onCreate") }
.firstOrNull()
?.let { enterEvent ->
val exitEvent = events.find { !it.isEnter && it.tag == enterEvent.tag }
exitEvent?.let { it.timestampMs - enterEvent.timestampMs }
} ?: 0L
return LaunchReport(
appOnCreateMs = appOnCreateCost,
firstActivityOnCreateMs = firstActivityOnCreateCost,
totalTtidMs = SystemClock.uptimeMillis() - ProcessStartTimeHelper.getProcessStartTime()
)
}
privatefuncalcCost(tag: String): Long {
val enter = events.find { it.isEnter && it.tag == tag }?.timestampMs ?: return0L
val exit = events.find { !it.isEnter && it.tag == tag }?.timestampMs ?: return0L
return exit - enter
}
}
八、监控场景四:点击事件全埋点
8.1 点击插桩策略
全埋点需要覆盖所有点击入口:
|
|
|
|
|---|---|---|
View.OnClickListener |
onClick(View)V |
|
|
|
|
|
View.OnLongClickListener |
onLongClick(View)Z |
|
|
|
Modifier.clickable |
|
8.2 接口实现类的识别
classPerfClassVisitor(...) : ClassVisitor(...) {
privateval implementedInterfaces = mutableListOf<String>()
overridefunvisit(..., interfaces: Array<String>?) {
interfaces?.let { implementedInterfaces.addAll(it) }
super.visit(...)
}
overridefunvisitMethod(
access: Int, name: String, descriptor: String, ...
): MethodVisitor {
val mv = super.visitMethod(...)
// 检查是否实现了 OnClickListener
if ("android/view/View\$OnClickListener"in implementedInterfaces
&& name == "onClick"
&& descriptor == "(Landroid/view/View;)V") {
return ClickEventVisitor(api, mv, access, name, descriptor, className)
}
return mv
}
}
8.3 ClickEventVisitor 实现
classClickEventVisitor(...) : AdviceAdapter(...) {
overridefunonMethodEnter() {
// 插入:ClickTracker.onClickEnter(view)
// 此时栈顶:..., this, view
// 需要 load 参数 view(参数1,非静态方法 this 是参数0)
val isStatic = (methodAccess and Opcodes.ACC_STATIC) != 0
val viewArgIndex = if (isStatic) 0else1
mv.visitVarInsn(Opcodes.ALOAD, viewArgIndex)
mv.visitLdcInsn(className)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/monitor/ClickTracker",
"track",
"(Landroid/view/View;Ljava/lang/String;)V",
false
)
}
}
运行时采集 View 信息:
object ClickTracker {
@JvmStatic
funtrack(view: View, listenerClass: String) {
val event = ClickEvent(
viewId = view.id,
viewIdName = runCatching { view.resources.getResourceEntryName(view.id) }.getOrNull(),
viewClass = view.javaClass.simpleName,
listenerClass = listenerClass,
activityName = getCurrentActivity()?.javaClass?.name,
timestamp = System.currentTimeMillis()
)
PerfReporter.reportClick(event)
}
privatefungetCurrentActivity(): Activity? {
// 通过 ActivityLifecycleCallbacks 维护当前 Activity 引用
return ActivityTracker.currentActivity?.get()
}
}
九、监控场景五:网络请求监控
9.1 OkHttp EventListener 自动注入
通过插桩在 OkHttpClient.Builder 的 build() 调用处,自动插入 EventListener:
// 目标:将
// val client = OkHttpClient.Builder().build()
// 修改为:
// val client = OkHttpClient.Builder()
// .eventListener(NetMonitorEventListener.INSTANCE)
// .build()
classOkHttpBuilderVisitor(...) : MethodVisitor(...) {
overridefunvisitMethodInsn(
opcode: Int, owner: String, name: String,
descriptor: String, isInterface: Boolean
) {
// 拦截 OkHttpClient.Builder.build() 调用
if (owner == "okhttp3/OkHttpClient\$Builder" && name == "build") {
// 在 build() 之前,栈顶是 Builder 对象
// 插入:builder.eventListener(NetMonitorEventListener.INSTANCE)
mv.visitFieldInsn(
Opcodes.GETSTATIC,
"com/example/monitor/NetMonitorEventListener",
"INSTANCE",
"Lcom/example/monitor/NetMonitorEventListener;"
)
mv.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"okhttp3/OkHttpClient\$Builder",
"eventListener",
"(Lokhttp3/EventListener;)Lokhttp3/OkHttpClient\$Builder;",
false
)
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
}
NetMonitorEventListener 实现:
classNetMonitorEventListener : EventListener() {
companionobject {
@JvmField
val INSTANCE = NetMonitorEventListener()
}
privateval callStartTimes = ConcurrentHashMap<Call, Long>()
overridefuncallStart(call: Call) {
callStartTimes[call] = SystemClock.uptimeMillis()
}
overridefunresponseBodyEnd(call: Call, byteCount: Long) {
val startTime = callStartTimes.remove(call) ?: return
val costMs = SystemClock.uptimeMillis() - startTime
PerfReporter.reportNetwork(
url = call.request().url.toString(),
method = call.request().method,
costMs = costMs,
responseBodyBytes = byteCount
)
}
overridefuncallFailed(call: Call, ioe: IOException) {
val startTime = callStartTimes.remove(call) ?: return
PerfReporter.reportNetworkError(
url = call.request().url.toString(),
costMs = SystemClock.uptimeMillis() - startTime,
error = ioe.message
)
}
}
十、完整插件架构
10.1 目录结构
perf-monitor-plugin/
├── build.gradle.kts
└── src/main/kotlin/com/example/plugin/
├── PerfMonitorPlugin.kt ← 插件入口
├── PerfMonitorExtension.kt ← DSL 配置
├── transform/
│ ├── PerfAsmClassVisitorFactory.kt ← AGP Transform 入口
│ └── PerfInstrumentationParams.kt ← 参数接口
├── visitor/
│ ├── PerfClassVisitor.kt ← 类级访问者(分发入口)
│ ├── MethodTraceVisitor.kt ← 方法耗时插桩
│ ├── ClickEventVisitor.kt ← 点击事件插桩
│ ├── ActivityLifecycleVisitor.kt ← Activity 生命周期插桩
│ ├── ApplicationLifecycleVisitor.kt← Application 生命周期插桩
│ └── OkHttpBuilderVisitor.kt ← OkHttp 网络监控插桩
└── util/
├── MethodIdManager.kt ← 方法 ID 管理(用于低开销追踪)
└── ClassUtils.kt ← 类型工具函数
perf-monitor-runtime/ ← 运行时依赖(app 模块引入)
└── src/main/kotlin/com/example/monitor/
├── TraceHelper.kt
├── BlockMonitor.kt
├── LaunchTracer.kt
├── ClickTracker.kt
├── NetMonitorEventListener.kt
├── MethodBuffer.kt ← 环形缓冲区
├── PerfReporter.kt ← 统一上报接口
└── ActivityTracker.kt
10.2 完整 Plugin 入口
classPerfMonitorPlugin : Plugin<Project> {
overridefunapply(project: Project) {
val extension = project.extensions.create(
"perfMonitor", PerfMonitorExtension::class.java
)
project.plugins.withId("com.android.application") {
applyToAndroid(project, extension)
}
project.plugins.withId("com.android.library") {
applyToAndroid(project, extension)
}
}
privatefunapplyToAndroid(project: Project, extension: PerfMonitorExtension) {
val androidComponents = project.extensions
.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
// 根据 buildType 决定是否启用(Release 才真正插桩)
val isRelease = variant.buildType == "release"
val shouldInstrument = extension.enableMethodTrace ||
extension.enableClickMonitor ||
extension.enableLaunchTrace
if (!shouldInstrument) return@onVariants
variant.instrumentation.transformClassesWith(
PerfAsmClassVisitorFactory::class.java,
InstrumentationScope.ALL
) { params ->
params.enableMethodTrace.set(
extension.enableMethodTrace && isRelease
)
params.enableClickMonitor.set(extension.enableClickMonitor)
params.enableLaunchTrace.set(extension.enableLaunchTrace)
params.methodTraceThresholdMs.set(extension.methodTraceThresholdMs)
params.tracePackages.set(extension.tracePackages)
params.excludeClasses.set(extension.excludeClasses)
}
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}
}
}
十一、增量构建优化
11.1 为什么增量构建很重要
全量插桩会在每次代码改动后处理所有 .class 文件(包括三方库),这在大型项目中可能增加数十秒构建时间。
新 AsmClassVisitorFactory API 天然支持增量:AGP 会跟踪哪些 class 发生了变化,只对变化的 class 调用 createClassVisitor。
11.2 关键注意点
// 让 AGP 知道哪些输入影响插桩行为
// @Input 注解确保参数变化时触发重新插桩
interfacePerfInstrumentationParams : InstrumentationParameters {
@get:Input// ← 必须!否则增量时参数变化不会触发重新处理
val enableMethodTrace: Property<Boolean>
@get:Input
val tracePackages: ListProperty<String>
}
11.3 isInstrumentable 过滤优化
overridefunisInstrumentable(classData: ClassData): Boolean {
val className = classData.className
// 精确的包名过滤,减少需要处理的类数量
// 这个方法在增量构建中对"未变化"的类不会被调用
return params.tracePackages.get().any { pkg ->
className.replace('.', '/').startsWith(pkg)
}
}
十二、R8/ProGuard 兼容性处理
12.1 混淆后方法名还原
方法耗时上报中包含方法名(如 com/example/app/MainActivity#onCreate),混淆后方法名会变成 a/b/c#a,需要在服务端通过 mapping 文件还原。
构建时自动上传 mapping 文件:
// 在插件中注册上传 mapping 任务
androidComponents.onVariants { variant ->
if (variant.buildType == "release") {
project.tasks.register("uploadPerfMapping${variant.name.capitalize()}") {
dependsOn("minify${variant.name.capitalize()}WithR8")
doLast {
val mappingFile = project.file(
"build/outputs/mapping/${variant.name}/mapping.txt"
)
if (mappingFile.exists()) {
uploadMappingToServer(mappingFile, variant.versionName)
}
}
}
}
}
12.2 保护监控类不被混淆
在 proguard-rules.pro 中(或通过 Consumer ProGuard rules 自动注入):
# 保护监控 SDK 所有类
-keep class com.example.monitor.** { *; }
# 保护被插桩调用的静态方法
-keepclassmembers class com.example.monitor.TraceHelper {
public static *** begin(...);
public static *** end(...);
}
-keepclassmembers class com.example.monitor.ClickTracker {
public static *** track(...);
}
-keepclassmembers class com.example.monitor.LaunchTracer {
public static *** begin(...);
public static *** end(...);
public static *** onActivityMethodEnter(...);
public static *** onActivityMethodExit(...);
}
通过插件自动注入 ProGuard 规则(无需业务手动配置):
// 在 Plugin.apply() 中
project.afterEvaluate {
project.extensions.getByType(BaseExtension::class.java).apply{
defaultConfig {
consumerProguardFile(
// 将插件 resources 中的规则文件复制出来
extractProguardRules(project)
)
}
}
}
十三、生产环境避坑指南
13.1 常见崩溃:StackOverflowError
原因:监控代码的辅助方法(如 TraceHelper)本身也被插桩,导致递归调用。
解决:在 isInstrumentable 或 excludeClasses 中排除监控包名。
overridefunisInstrumentable(classData: ClassData): Boolean {
val className = classData.className
// 排除监控 SDK 本身,防止无限递归
if (className.startsWith("com.example.monitor")) returnfalse
// ...
}
13.2 常见错误:VerifyError / ClassFormatError
原因:插桩后字节码的栈帧(Stack Frame)计算错误,Java 验证器拒绝加载。
解决:使用 COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS 让 AGP 自动重新计算栈帧。
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
但注意这会增加构建时间,只对实际插桩的方法生效(不是 ALL_CLASSES)。
13.3 Kotlin 内联函数无法插桩
原因:inline fun 在编译后不存在对应的方法体,其代码被内联到调用处。
影响:高阶函数(如 apply, let, run)内部耗时被归到调用方方法名下。
解决:记录这是已知限制,在数据分析时注意。或使用 noinline 参数声明关键函数。
13.4 Lambda 类名不稳定
原因:Lambda 编译后的类名包含数字序号(如 Foo$$Lambda$1),混淆后更难识别。
解决:上报时额外记录所在文件名(通过 visitSource() 获取):
classPerfClassVisitor(...) : ClassVisitor(...) {
privatevar sourceFile: String = ""
overridefunvisitSource(source: String?, debug: String?) {
this.sourceFile = source ?: ""
super.visitSource(source, debug)
}
// 在方法插桩中将 sourceFile 一并上报
}
13.5 接口方法不要插桩
接口的 default 方法理论上可以插桩,但在 Android API 23 及以下会引发兼容性问题。
overridefunvisitMethod(access: Int, name: String, ...) : MethodVisitor {
// 跳过接口方法
if (classAccess and Opcodes.ACC_INTERFACE != 0) {
returnsuper.visitMethod(access, name, ...)
}
// ...
}
13.6 构建速度劣化
全量插桩(InstrumentationScope.ALL)会处理所有三方库,影响构建速度。
优化策略:
// 仅处理本工程代码,不处理三方库
variant.instrumentation.transformClassesWith(
PerfAsmClassVisitorFactory::class.java,
InstrumentationScope.PROJECT // 而非 ALL
) { ... }
或在 isInstrumentable 中精确过滤,只处理业务包名。
十四、监控数据上报设计
14.1 PerfReporter 统一上报接口
object PerfReporter {
// 允许外部注册自定义 Reporter(可对接 APM 平台)
privateval reporters = mutableListOf<IPerfReporter>()
funregister(reporter: IPerfReporter) {
reporters.add(reporter)
}
funreportSlowMethod(method: String, costMs: Long, threadName: String, isMainThread: Boolean) {
val event = PerfEvent.SlowMethod(method, costMs, threadName, isMainThread)
dispatch(event)
}
funreportBlock(costMs: Long, stack: String) {
val event = PerfEvent.Block(costMs, stack)
dispatch(event)
}
funreportLaunchTrace(report: LaunchReport) {
val event = PerfEvent.Launch(report)
dispatch(event)
}
funreportClick(event: ClickEvent) {
dispatch(PerfEvent.Click(event))
}
funreportNetwork(url: String, method: String, costMs: Long, responseBodyBytes: Long) {
val event = PerfEvent.Network(url, method, costMs, responseBodyBytes)
dispatch(event)
}
privatefundispatch(event: PerfEvent) {
reporters.forEach { it.onEvent(event) }
}
}
interfaceIPerfReporter{
funonEvent(event: PerfEvent)
}
sealedclassPerfEvent{
dataclassSlowMethod(val method: String, val costMs: Long,
val threadName: String, val isMainThread: Boolean) : PerfEvent()
dataclassBlock(val costMs: Long, val stack: String) : PerfEvent()
dataclassLaunch(val report: LaunchReport) : PerfEvent()
dataclassClick(val event: ClickEvent) : PerfEvent()
dataclassNetwork(val url: String, val method: String,
val costMs: Long, val responseBytes: Long) : PerfEvent()
}
14.2 批量上报与本地缓存
classBatchPerfReporter(
privateval uploadUrl: String,
privateval batchSize: Int = 50,
privateval flushIntervalMs: Long = 30_000L
) : IPerfReporter {
privateval queue = ConcurrentLinkedQueue<PerfEvent>()
privateval executor = Executors.newSingleThreadScheduledExecutor()
init {
executor.scheduleAtFixedRate(
::flush, flushIntervalMs, flushIntervalMs, TimeUnit.MILLISECONDS
)
}
overridefunonEvent(event: PerfEvent) {
queue.offer(event)
if (queue.size >= batchSize) flush()
}
privatefunflush() {
val batch = mutableListOf<PerfEvent>()
while (batch.size < batchSize) {
batch.add(queue.poll() ?: break)
}
if (batch.isNotEmpty()) {
uploadBatch(batch)
}
}
privatefunuploadBatch(batch: List<PerfEvent>) {
// 序列化为 JSON 上报到 APM 后端
}
}
十五、工业级实践参考:Matrix-TraceCanary
Matrix-TraceCanary 是腾讯开源的工业级方案,其核心设计:
15.1 关键设计决策
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15.2 方法 ID 分配
// 构建期生成的方法 ID 映射表
// 格式:methodId,accessFlag,className,methodName,descriptor
// 示例:1,public,com/example/MainActivity,onCreate,(Landroid/os/Bundle;)V
object MethodIdManager {
privateval methodToId = HashMap<String, Int>()
privatevar nextId = AtomicInteger(1)
fungetOrAssign(signature: String): Int =
methodToId.getOrPut(signature) { nextId.getAndIncrement() }
// 构建完成后输出 mapping 文件
funwriteMappingFile(outputFile: File) {
outputFile.bufferedWriter().use { writer ->
methodToId.entries
.sortedBy { it.value }
.forEach { (sig, id) ->
writer.appendLine("$id,$sig")
}
}
}
}
十六、总结
16.1 技术选型决策树
需要性能监控?
│
├── 只需线上崩溃 ──────────────────> Firebase Crashlytics(零成本)
│
├── 需要方法级耗时 ─────────────────> Gradle 插件 + ASM 插桩
│ │
│ ├── 全量追踪(开销较大)──────> 方法耗时插桩 + 环形缓冲 + 卡顿触发上报
│ └── 关键路径追踪(推荐)────> 选择性包名插桩 + 实时上报
│
├── 需要启动优化 ──────────────────> 生命周期插桩 + Choreographer 首帧检测
│
├── 需要全埋点 ────────────────────> onClick 接口实现类插桩
│
└── 需要网络监控 ──────────────────> OkHttp Builder 调用点插桩 + EventListener
16.2 核心要点回顾
-
插件架构:使用
AsmClassVisitorFactory新 API,天然支持增量构建,避免旧Transform的全量构建问题。 -
访问者模式:
ClassVisitor负责识别目标类,MethodVisitor(通常继承AdviceAdapter)负责在方法入口/出口注入代码。 -
try-finally 模式:通过
visitTryCatchBlock保证异常路径也能触发监控结束逻辑。 -
性能开销控制:
-
用方法 ID(int)代替方法名字符串写入缓冲区 -
使用 isInstrumentable精确过滤,减少插桩范围 -
环形缓冲区避免内存增长 -
工程健壮性:
-
排除监控 SDK 本身防止递归 -
设置 COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS避免 VerifyError -
混淆时保护监控类,并自动上传 mapping 文件 -
可扩展性:通过
IPerfReporter接口解耦数据采集与上报,支持对接不同 APM 平台。
夜雨聆风