ASM开源库实现函数耗时插桩

文章目录

    • 一、ASM简介
      • [1. 设计框架](#1. 设计框架)
      • [2. 设计模式:访问者模式和责任链模式](#2. 设计模式:访问者模式和责任链模式)
      • [3. visitor访问顺序](#3. visitor访问顺序)
    • 二、ASM插桩常见用途
      • [1. 性能监控优化](#1. 性能监控优化)
      • [2. 自动化埋点与数据采集(无痕埋点)](#2. 自动化埋点与数据采集(无痕埋点))
      • [3. 热修复与功能动态化](#3. 热修复与功能动态化)
      • [4. 隐私合规与安全改造](#4. 隐私合规与安全改造)
    • 三、ASM实现函数耗时统计
      • [1. AGP环境](#1. AGP环境)
      • [2. 插件类](#2. 插件类)
      • [3. 生成ClassVisitor的工厂类](#3. 生成ClassVisitor的工厂类)
      • [4. 函数插桩实现。](#4. 函数插桩实现。)
      • [5. 插桩实现的效果](#5. 插桩实现的效果)
    • 四、常用的工具类
    • 五、ASM插桩经典架构
      • [1. 经典架构](#1. 经典架构)
      • [2. 优化ClassReader读取效率](#2. 优化ClassReader读取效率)
      • [3. 优化原理](#3. 优化原理)
    • 六、类结构
    • 七、参考资料

一、ASM简介

1. 设计框架

说明 功能定位 核心职责 关键函数
ClassReader 数据解析器 负责解析原始类的字节数组,并将其结构化的事件流传递给访问者对象,驱动整个流程。 构造函数:ClassReader( byte[] classFile) 接收Visitor:void accept(ClassVisitor classVisitor, int parsingOptions)
ClassVisitor 访问者接口/抽象类 定义了类各个结构(如字段、方法、注解)的访问方法,是修改字节码的入口。 构造函数:ClassVisitor(int api, ClassVisitor classVisitor) 方法:visitvisitOuterClassvisitAnnotationvisitFieldvisitMethod
ClassWriter 字节码生成器 继承自ClassVisitor,负责接收访问事件并生成修改后的二进制字节数组。 构造函数:ClassWriter(ClassReader classReader, int flags) 生成类:byte[] toByteArray()

2. 设计模式:访问者模式和责任链模式

  1. 访问者接口 (Visitor): 定义了访问每一个具体元素的方法 visit(Element)
  2. 具体访问者 (Concrete Visitor): 实现访问者接口,负责定义具体的算法/操作逻辑。
  3. 元素接口 (Element): 定义一个 accept(Visitor) 方法,允许访问者访问。
  4. 具体元素 (Concrete Element): 实现 accept 方法,并在该方法内部回调访问者的 visit 方法。
  5. 对象结构 (Object Structure): 用于存储和遍历元素对象集合(如列表或树)。

3. visitor访问顺序

  • 类:visit visitSource? visitOuterClass? (visitAnnotation|visitAttribute)* (visitInnerClass|visitField|visitMethod)* visitEnd
  • 函数方法:visitAnnotationDefault? (visitAnnotation|visitParameterAnnotation|visitAttribute)* (visitCode (visitTryCatchBlock|visitLabel|visitFrame|visitXxxInsn|visitLocalVariable|visitLineNumber)* visitMaxs)? visitEnd

二、ASM插桩常见用途

1. 性能监控优化

  • 批量统计函数执行耗时,自动在方法开头插入 System.currentTimeMillis(),结尾插入计算与上报逻辑,用于定位启动慢、卡顿的方法。比如BlockCanary
  • 批量trace插桩。启动速度优化 :在 ApplicationActivity 关键生命周期方法中插入 Trace 开关,精准统计冷启动、温启动各阶段耗时。

2. 自动化埋点与数据采集(无痕埋点)

  • 全量页面访问统计 :拦截 Activity.onCreate / onResumeFragment.onResume,自动上报页面名称、停留时长。
  • 点击事件埋点 :在 View.OnClickListener.onClick 执行前插入代码,获取控件 ID、文本、位置等信息进行上报。
  • 列表曝光统计 :结合 RecyclerViewonBindViewHolder 或滚动监听,插入曝光标记代码。

3. 热修复与功能动态化

热修复框架的核心机制之一就是通过字节码插桩为每个方法预留"补丁"入口。

  • 方法替换(Method Hook) :在每个方法开头插入一个静态方法调用,检查是否有需要执行的补丁代码,如有则跳转执行补丁,实现不重启修复线上 bug。代表框架:TinkerSophix
  • 资源修复/So 修复:同样可在初始化阶段插入代码,实现资源路径或 So 加载路径的替换。

4. 隐私合规与安全改造

  • 敏感 API 统一拦截/替换 :扫描所有调用 TelephonyManager.getDeviceId()Settings.Secure.getString()(获取 Android ID)、MAC 地址获取等代码行,替换为返回"合规空值"或统一管理,以适应监管要求。
  • 增加try catch安全防护:对一些通用逻辑增加catch保护,减少线上崩溃。

三、ASM实现函数耗时统计

1. AGP环境

2. 插件类

kotlin 复制代码
package com.example.asm.test

import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationScope
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class AsmPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    project.logger.lifecycle("=========== ASM Method Time Cost Plugin Applied ===============")
    LogUtil.init(project.logger)

    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)

    androidComponents.onVariants { variant ->
      project.logger.quiet("注册 ASM 变换到 variant: ${variant.name}")

      // 注册 AsmClassVisitorFactory
      variant.instrumentation.transformClassesWith(
        AsmClassVisitorFactoryImpl::class.java,
        InstrumentationScope.PROJECT
      ) {
        // 配置参数(如果需要)
      }

      // 设置 ASM frames 计算模式,对应ASM中的ClassWriter.COMPUTE_FRAMES
      variant.instrumentation.setAsmFramesComputationMode(
        FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
      )
    }
  }
}

3. 生成ClassVisitor的工厂类

kotlin 复制代码
package com.example.asm.test

import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.InstrumentationParameters
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.util.TraceClassVisitor
import org.objectweb.asm.util.CheckClassAdapter
import java.io.PrintWriter

abstract class AsmClassVisitorFactoryImpl : AsmClassVisitorFactory<InstrumentationParameters.None> {
    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        // 责任链模式
        val checkClassVisitor = CheckClassAdapter(nextClassVisitor) // 检查asm修改后的代码是否符合规范,如果不符合规范,会抛出异常
        val traceClassVisitor = TraceClassVisitor(checkClassVisitor, PrintWriter(System.out)) // 打印asm修改后的代码
        val cv =  MethodTimeCostClassVisitor(traceClassVisitor)
        return cv
    }

    // 判断是否需要对该类进行插桩,对不需要插桩的类进行过滤
    override fun isInstrumentable(classData: ClassData): Boolean {
        return classData.className.contains("com.example.myapplication2.ui")
    }
}

4. 函数插桩实现。

  • MethodTimeCostMethodVisitor继承自AdviceAdapter类,重写onMethodEnteronMethodExit放在在函数进入退出时插桩。
kotlin 复制代码
package com.example.asm.test

import org.gradle.api.logging.Logger
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.commons.AdviceAdapter

class MethodTimeCostClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM9, classVisitor) {
  private val logger: Logger? = LogUtil.getLogger()
  init {
    if (classVisitor is ClassWriter) {
      logger?.quiet("classVisitor is ClassWriter instance")
    }
    logger?.lifecycle("MethodTimeCostClassVisitor 初始化")
  }

  override fun visit(
    version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<out String?>?
  ) {
    logger?.lifecycle("MethodTimeCostClassVisitor visit method: $name")
    super.visit(version, access, name, signature, superName, interfaces)
  }

  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 if (mv != null) MethodTimeCostMethodVisitor(mv, access, name, descriptor) else mv
  }

  private class MethodTimeCostMethodVisitor(
    mv: MethodVisitor,
    access: Int,
    name: String?,
    descriptor: String?,
    private val logger: Logger? = null
  ) : AdviceAdapter(Opcodes.ASM9, mv, access, name, descriptor) {

    // 用于存储开始时间的局部变量索引(long 类型需要 2 个 slot)
    private var timeVarIndex = -1

    override fun onMethodEnter() {
      // 调用 System.currentTimeMillis() 记录开始时间
      mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)

      // 将返回的 long 时间存储到局部变量表中
      timeVarIndex = newLocal(Type.LONG_TYPE)
      storeLocal(timeVarIndex)

      logger?.debug("方法 $name 进入时已插入时间记录代码")
    }

    override fun onMethodExit(opcode: Int) {
      if (timeVarIndex == -1) return

      // 1. 获取结束时间并计算耗时
      mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
      loadLocal(timeVarIndex)
      mv.visitInsn(LSUB)

      // 2. 使用 String.valueOf() 将 long 转为 String
      mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(J)Ljava/lang/String;", false)

      // 3. 拼接字符串:"methodName cost: " + duration + " ms"
      mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
      mv.visitInsn(DUP)
      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)

      mv.visitLdcInsn("method $name cost: ")
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)

      mv.visitInsn(SWAP)  // 交换 StringBuilder 和 duration 字符串
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false) // 拼接耗时

      mv.visitLdcInsn(" ms") // 拼接ms
      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)

      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)

      // 4. 调用 Log.i()
      mv.visitLdcInsn("$name")
      mv.visitInsn(SWAP)
      mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false)
      mv.visitInsn(POP)

      logger?.debug("方法 $name 退出时已插入耗时计算代码")
    }
  }
}

5. 插桩实现的效果

四、常用的工具类

类名 作用
TraceClassVisitor 打印转换完成后的字节码
CheckClassAdapter 校验字节码文件是否合法,字节码文件不合法时抛出编译异常。
AdviceAdapter 有函数进入和退出的回调,用于实现ASM插桩。不用考虑帧结构问题
kotlin 复制代码
val checkClassVisitor = CheckClassAdapter(nextClassVisitor) // 检查asm修改后的代码是否符合规范,如果不符合规范,会抛出异常
val traceClassVisitor = TraceClassVisitor(checkClassVisitor, PrintWriter(System.out)) // 打印asm修改后的代码
val cv =  MethodTimeCostClassVisitor(traceClassVisitor) //责任链模式,最外层的classVisitor先执行。插桩类在最外层

五、ASM插桩经典架构

1. 经典架构

java 复制代码
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv 将所有事件转发给 cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { // 修改类内容 };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 与 b1 表示同一个类

2. 优化ClassReader读取效率

  • 在构造ClassWriter时,传入ClassReader对象,如果方法没有修改过,classReader遍历方法时,就会直接将原方法拷贝,而不详细解析方法体。
  • 实际上ClassReader是将symbolTable传入ClassWriter。用于直接解析方法相关的位置信息。
java 复制代码
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0); // 优化点
ClassVisitor cv = new ClassVisitor(ASM4, cw) { // 修改类内容 };
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();

3. 优化原理

  • 优化源码参考:org.objectweb.asm.ClassReader#readMethod,参考文档:https://www.yuque.com/mikaelzero/asm/bwbaz7
  • 在ClassReader组件的accept方法参数中传送了ClassVisitor,如果ClassReader检测到这个ClassVisitor返回的MethodVisitor来自一个ClassWriter,这意味着这个方法的内容将不会被转换,事实上,应用程序甚至不会 看到其内容。 在这种情况下,ClassReader组件不会分析这个方法的内容,不会生成相应事件,只是复制ClassWriter中表示这个方法的字节数组。
java 复制代码
// If the returned MethodVisitor is in fact a MethodWriter, it means there is no method
// adapter between the reader and the writer. In this case, it might be possible to copy
// the method attributes directly into the writer. If so, return early without visiting
// the content of these attributes.
if (methodVisitor instanceof MethodWriter) {
  MethodWriter methodWriter = (MethodWriter) methodVisitor;
  if (methodWriter.canCopyMethodAttributes(
      this,
      synthetic,
      (context.currentMethodAccessFlags & Opcodes.ACC_DEPRECATED) != 0,
      readUnsignedShort(methodInfoOffset + 4),
      signatureIndex,
      exceptionsOffset)) {
    methodWriter.setMethodAttributesSource(methodInfoOffset, currentOffset - methodInfoOffset);
    return currentOffset;
  }
}

六、类结构

类结构 详细结构
类信息 修饰符、类名字、超类、接口
常量池 数值、字符串、类型常量
其他 源文件名、封装的类引用、注释*、属性*
内部类* 名称
字段* 修饰符、名字、类型、注释*、属性*
方法* 修饰符、名字、返回类型与参数类型、注释*、属性*、编译后的代码
java 复制代码
final int access // 访问权限
final String name // 函数名、字段名
final String descriptor // 字段的类型,函数的描述符
final String signature // 泛型信息

七、参考资料

  1. ASM版本implementation "org.ow2.asm:asm:7.2"
  2. 文档:https://www.yuque.com/mikaelzero/asm
相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui