文章目录
- 一、ASM简介
- 1. 设计框架
- 2. 设计模式:访问者模式和责任链模式
- 3. visitor访问顺序
- 二、ASM插桩常见用途
- 1. 性能监控优化
- 2. 自动化埋点与数据采集(无痕埋点)
- 3. 热修复与功能动态化
- 4. 隐私合规与安全改造
- 三、ASM实现函数耗时统计
- 1. AGP环境
- 2. 插件类
- 3. 生成ClassVisitor的工厂类
- 4. 函数插桩实现。
- 5. 插桩实现的效果
- 四、常用的工具类
- 五、ASM插桩经典架构
- 1. 经典架构
- 2. 优化ClassReader读取效率
- 3. 优化原理
- 六、类结构
- 七、参考资料
原文链接: https://blog.csdn.net/followYouself/article/details/160512010
一、ASM简介
1. 设计框架
| 说明 | 功能定位 | 核心职责 | 关键函数 |
|---|---|---|---|
| ClassReader | 数据解析器 | 负责解析原始类的字节数组,并将其结构化的事件流传递给访问者对象,驱动整个流程。 | 构造函数:ClassReader( byte[] classFile)接收Visitor: void accept(ClassVisitor classVisitor, int parsingOptions) |
| ClassVisitor | 访问者接口/抽象类 | 定义了类各个结构(如字段、方法、注解)的访问方法,是修改字节码的入口。 | 构造函数:ClassVisitor(int api, ClassVisitor classVisitor)方法: visit、visitOuterClass、visitAnnotation、visitField、visitMethod |
| ClassWriter | 字节码生成器 | 继承自ClassVisitor,负责接收访问事件并生成修改后的二进制字节数组。 | 构造函数:ClassWriter(ClassReader classReader, int flags)生成类: byte[] toByteArray() |
2. 设计模式:访问者模式和责任链模式
- 访问者接口 (Visitor):定义了访问每一个具体元素的方法
visit(Element)。 - 具体访问者 (Concrete Visitor):实现访问者接口,负责定义具体的算法/操作逻辑。
- 元素接口 (Element):定义一个
accept(Visitor)方法,允许访问者访问。 - 具体元素 (Concrete Element):实现
accept方法,并在该方法内部回调访问者的visit方法。 - 对象结构 (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插桩。启动速度优化:在
Application、Activity关键生命周期方法中插入 Trace 开关,精准统计冷启动、温启动各阶段耗时。
2. 自动化埋点与数据采集(无痕埋点)
- 全量页面访问统计:拦截
Activity.onCreate/onResume、Fragment.onResume,自动上报页面名称、停留时长。 - 点击事件埋点:在
View.OnClickListener.onClick执行前插入代码,获取控件 ID、文本、位置等信息进行上报。 - 列表曝光统计:结合
RecyclerView的onBindViewHolder或滚动监听,插入曝光标记代码。
3. 热修复与功能动态化
热修复框架的核心机制之一就是通过字节码插桩为每个方法预留“补丁”入口。
- 方法替换(Method Hook):在每个方法开头插入一个静态方法调用,检查是否有需要执行的补丁代码,如有则跳转执行补丁,实现不重启修复线上 bug。代表框架:
Tinker、Sophix。 - 资源修复/So 修复:同样可在初始化阶段插入代码,实现资源路径或 So 加载路径的替换。
4. 隐私合规与安全改造
- 敏感 API 统一拦截/替换:扫描所有调用
TelephonyManager.getDeviceId()、Settings.Secure.getString()(获取 Android ID)、MAC地址获取等代码行,替换为返回“合规空值”或统一管理,以适应监管要求。 - 增加try catch安全防护:对一些通用逻辑增加catch保护,减少线上崩溃。
三、ASM实现函数耗时统计
1. AGP环境
AGP 7.x以后,支持使用
AsmClassVisitorFactory实现ASM字节码插桩,废弃掉传统的transform API接口。Gradle插件实现参考:https://blog.csdn.net/followYouself/article/details/160449805
2. 插件类
packagecom.example.asm.testimportcom.android.build.api.instrumentation.FramesComputationModeimportcom.android.build.api.instrumentation.InstrumentationScopeimportcom.android.build.api.variant.AndroidComponentsExtensionimportorg.gradle.api.Pluginimportorg.gradle.api.ProjectclassAsmPlugin:Plugin<Project>{overridefunapply(project:Project){project.logger.lifecycle("=========== ASM Method Time Cost Plugin Applied ===============")LogUtil.init(project.logger)valandroidComponents=project.extensions.getByType(AndroidComponentsExtension::class.java)androidComponents.onVariants{variant->project.logger.quiet("注册 ASM 变换到 variant:${variant.name}")// 注册 AsmClassVisitorFactoryvariant.instrumentation.transformClassesWith(AsmClassVisitorFactoryImpl::class.java,InstrumentationScope.PROJECT){// 配置参数(如果需要)}// 设置 ASM frames 计算模式,对应ASM中的ClassWriter.COMPUTE_FRAMESvariant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)}}}3. 生成ClassVisitor的工厂类
packagecom.example.asm.testimportcom.android.build.api.instrumentation.AsmClassVisitorFactoryimportcom.android.build.api.instrumentation.ClassContextimportcom.android.build.api.instrumentation.ClassDataimportcom.android.build.api.instrumentation.InstrumentationParametersimportorg.objectweb.asm.ClassVisitorimportorg.objectweb.asm.util.TraceClassVisitorimportorg.objectweb.asm.util.CheckClassAdapterimportjava.io.PrintWriterabstractclassAsmClassVisitorFactoryImpl:AsmClassVisitorFactory<InstrumentationParameters.None>{overridefuncreateClassVisitor(classContext:ClassContext,nextClassVisitor:ClassVisitor):ClassVisitor{// 责任链模式valcheckClassVisitor=CheckClassAdapter(nextClassVisitor)// 检查asm修改后的代码是否符合规范,如果不符合规范,会抛出异常valtraceClassVisitor=TraceClassVisitor(checkClassVisitor,PrintWriter(System.out))// 打印asm修改后的代码valcv=MethodTimeCostClassVisitor(traceClassVisitor)returncv}// 判断是否需要对该类进行插桩,对不需要插桩的类进行过滤overridefunisInstrumentable(classData:ClassData):Boolean{returnclassData.className.contains("com.example.myapplication2.ui")}}4. 函数插桩实现。
MethodTimeCostMethodVisitor继承自AdviceAdapter类,重写onMethodEnter和onMethodExit放在在函数进入退出时插桩。
packagecom.example.asm.testimportorg.gradle.api.logging.Loggerimportorg.objectweb.asm.ClassVisitorimportorg.objectweb.asm.ClassWriterimportorg.objectweb.asm.MethodVisitorimportorg.objectweb.asm.Opcodesimportorg.objectweb.asm.Typeimportorg.objectweb.asm.commons.AdviceAdapterclassMethodTimeCostClassVisitor(classVisitor:ClassVisitor):ClassVisitor(Opcodes.ASM9,classVisitor){privatevallogger:Logger?=LogUtil.getLogger()init{if(classVisitorisClassWriter){logger?.quiet("classVisitor is ClassWriter instance")}logger?.lifecycle("MethodTimeCostClassVisitor 初始化")}overridefunvisit(version:Int,access:Int,name:String?,signature:String?,superName:String?,interfaces:Array<outString?>?){logger?.lifecycle("MethodTimeCostClassVisitor visit method:$name")super.visit(version,access,name,signature,superName,interfaces)}overridefunvisitMethod(access:Int,name:String?,descriptor:String?,signature:String?,exceptions:Array<outString?>?):MethodVisitor?{valmv=super.visitMethod(access,name,descriptor,signature,exceptions)returnif(mv!=null)MethodTimeCostMethodVisitor(mv,access,name,descriptor)elsemv}privateclassMethodTimeCostMethodVisitor(mv:MethodVisitor,access:Int,name:String?,descriptor:String?,privatevallogger:Logger?=null):AdviceAdapter(Opcodes.ASM9,mv,access,name,descriptor){// 用于存储开始时间的局部变量索引(long 类型需要 2 个 slot)privatevartimeVarIndex=-1overridefunonMethodEnter(){// 调用 System.currentTimeMillis() 记录开始时间mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentTimeMillis","()J",false)// 将返回的 long 时间存储到局部变量表中timeVarIndex=newLocal(Type.LONG_TYPE)storeLocal(timeVarIndex)logger?.debug("方法$name进入时已插入时间记录代码")}overridefunonMethodExit(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 转为 Stringmv.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$namecost: ")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")// 拼接msmv.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插桩。不用考虑帧结构问题 |
valcheckClassVisitor=CheckClassAdapter(nextClassVisitor)// 检查asm修改后的代码是否符合规范,如果不符合规范,会抛出异常valtraceClassVisitor=TraceClassVisitor(checkClassVisitor,PrintWriter(System.out))// 打印asm修改后的代码valcv=MethodTimeCostClassVisitor(traceClassVisitor)//责任链模式,最外层的classVisitor先执行。插桩类在最外层五、ASM插桩经典架构
1. 经典架构
byte[]b1=...;ClassWritercw=newClassWriter(0);// cv 将所有事件转发给 cwClassVisitorcv=newClassVisitor(ASM4,cw){// 修改类内容 };ClassReadercr=newClassReader(b1);cr.accept(cv,0);byte[]b2=cw.toByteArray();// b2 与 b1 表示同一个类2. 优化ClassReader读取效率
在构造ClassWriter时,传入ClassReader对象,如果方法没有修改过,classReader遍历方法时,就会直接将原方法拷贝,而不详细解析方法体。- 实际上ClassReader是将
symbolTable传入ClassWriter。用于直接解析方法相关的位置信息。
byte[]b1=...ClassReadercr=newClassReader(b1);ClassWritercw=newClassWriter(cr,0);// 优化点ClassVisitorcv=newClassVisitor(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中表示这个方法的字节数组。
// 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(methodVisitorinstanceofMethodWriter){MethodWritermethodWriter=(MethodWriter)methodVisitor;if(methodWriter.canCopyMethodAttributes(this,synthetic,(context.currentMethodAccessFlags&Opcodes.ACC_DEPRECATED)!=0,readUnsignedShort(methodInfoOffset+4),signatureIndex,exceptionsOffset)){methodWriter.setMethodAttributesSource(methodInfoOffset,currentOffset-methodInfoOffset);returncurrentOffset;}}六、类结构
| 类结构 | 详细结构 |
|---|---|
| 类信息 | 修饰符、类名字、超类、接口 |
| 常量池 | 数值、字符串、类型常量 |
| 其他 | 源文件名、封装的类引用、注释*、属性* |
| 内部类* | 名称 |
| 字段* | 修饰符、名字、类型、注释*、属性* |
| 方法* | 修饰符、名字、返回类型与参数类型、注释*、属性*、编译后的代码 |
finalintaccess// 访问权限finalStringname// 函数名、字段名finalStringdescriptor// 字段的类型,函数的描述符finalStringsignature// 泛型信息七、参考资料
- ASM版本
implementation "org.ow2.asm:asm:7.2" - 文档:https://www.yuque.com/mikaelzero/asm
- 原文链接,转载请附上原文出处链接和本声明:https://blog.csdn.net/followYouself/article/details/160512010