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
相关推荐
TO_ZRG2 小时前
Android Content Provider 基础
android·jvm·oracle
studyForMokey2 小时前
【Android面试】数据库
android·数据库·面试
胡利光2 小时前
Harness Engineering 03|Eval & Trace Harness:验证和追溯的工程组织
android·开发语言·kotlin
jvvz afqh2 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql
aaajj2 小时前
【Android】防骚扰电话自动接听助手方案
android·人工智能
QCzblack2 小时前
php-ser-libs
android·开发语言·php
苏坡余2 小时前
Android Pixel7 13.0源码编译记录
android
灵魂学者3 小时前
使用 Android Studio 进行 HbuilderX H5+App 离线打包
android·ide·android studio·hbuilderx·apk build
scan7243 小时前
将记忆存储到数据库中
android