乐于分享
好东西不私藏

通过 Gradle 插件做全局性能监控

通过 Gradle 插件做全局性能监控

一、为什么需要 Gradle 插件级别的性能监控

1.1 传统监控方案的痛点

在移动端性能监控领域,常见的接入方式大致有以下几类:

方案
接入方式
问题
手动埋点
在每个方法首尾手写计时代码
侵入性强、覆盖不全、维护成本高
运行时 Hook
动态代理、反射
只能代理接口,兼容性差,Android 高版本受限
第三方 SDK
接入 Firebase Performance 等
数据不透明、定制性差、增大包体
Gradle 插件插桩
编译期自动注入
零侵入、全覆盖、可定制

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 历史版本对比

AGP 版本
API
状态
< 4.2
Transform

 抽象类 (com.android.build.api.transform)
已废弃
7.0+
AsmClassVisitorFactory

 + Instrumentation
推荐
8.0+
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 插桩逻辑设计

方法耗时插桩需要处理三种出口:

  1. 正常 return
  2. 异常 throw(未捕获)
  3. 有 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 方案选型对比

方案
原理
精度
开销
Looper 消息监控
替换 Printer
消息粒度
Choreographer 帧率监控
监听 vsync 回调
帧粒度
插桩 + 采样
主线程方法插桩 + 定时采样
方法粒度
WatchDog 线程
定时发 Message,超时告警
粗粒度

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
接口实现类
Lambda 点击
编译后的匿名类
同上
View.OnLongClickListener onLongClick(View)Z
接口实现类
Compose 点击
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 在编译后不存在对应的方法体,其代码被内联到调用处。

影响:高阶函数(如 applyletrun)内部耗时被归到调用方方法名下。

解决:记录这是已知限制,在数据分析时注意。或使用 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: Longval stack: String) : PerfEvent()
dataclassLaunch(val report: LaunchReport) : PerfEvent()
dataclassClick(val event: ClickEvent) : PerfEvent()
dataclassNetwork(val url: String, val method: String, 
val costMs: Longval 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 关键设计决策

设计点
Matrix 方案
原因
方法 ID 化
每个方法分配 int ID,运行时只记录 ID
极低内存/CPU 开销
环形缓冲区
固定大小,覆盖写入
避免 OOM
离线分析
卡顿触发后离线还原调用树
正常路径零上报开销
Mapping 文件
构建时生成 methodId→方法名 的映射
方便服务端还原
帧率监控
Choreographer doFrame 回调
精准感知丢帧

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 核心要点回顾

  1. 插件架构:使用 AsmClassVisitorFactory 新 API,天然支持增量构建,避免旧 Transform 的全量构建问题。

  2. 访问者模式ClassVisitor 负责识别目标类,MethodVisitor(通常继承 AdviceAdapter)负责在方法入口/出口注入代码。

  3. try-finally 模式:通过 visitTryCatchBlock 保证异常路径也能触发监控结束逻辑。

  4. 性能开销控制

    • 用方法 ID(int)代替方法名字符串写入缓冲区
    • 使用 isInstrumentable 精确过滤,减少插桩范围
    • 环形缓冲区避免内存增长
  5. 工程健壮性

    • 排除监控 SDK 本身防止递归
    • 设置 COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS 避免 VerifyError
    • 混淆时保护监控类,并自动上传 mapping 文件
  6. 可扩展性:通过 IPerfReporter 接口解耦数据采集与上报,支持对接不同 APM 平台。