Android方法耗时监控插件开发
需求:自定义一个Gradle插件,这个Gradle插件可以统计方法的耗时,并当方法耗时超过阈值时,可以通过打印Log日志在控制台,然后可以通过Log定位到耗时方法的位置,帮助我们找出耗时方法和当前线程名,并且可以过滤指定包名的类进行插桩和不插桩。
技术栈:ASM + Transform Action
ASM
ASM是一个通用的Java字节码操作和分析框架,它提供了一个简单易用的字节码操作方法,可以直接以二进制的形式修改现有类或动态生成类。简单地说,ASM就是一个字节码操作框架,通过ASM,我们可以凭空生成一个类或者修改现有的类,Asm相比其他的字节码操作框架如Javasist、AspectJ等的优点就是体积小、性能好、效率高。但它的缺点就是学习成本高,不过现在已经有Android Studio插件ASM Bytecode Outline可以替我们自动的生成Asm代码。
ASM中有两类api,一种是基于树模型的tree api,一种是基于访问者模式的visitor api,这里主要使用的visitor api,在visitor api中有三个主要的类用于读取、访问和生成class字节码:
- classVisitor:它是用于访问class字节码,它里面有很多visitXX方法,每调用一个visitXX方法,就表示你在访问class文件的某个结构,如Method、Field、Annotation等
- classReader:用于读取以字节数组形式给出的class字节码,它有一个accept方法,用于接收一个classVisitor实例,accept方法内部会调用ClassVisitor的visitXX方法来访问已读取的class文件
- classWriter:它继承自ClassVisitor,可以以二进制形式生成class字节码,它有一个toByteArray方法,可以把已生成的二进制形式的class字节码转换成字节数组形式返回
classVisitor,classReader,classWriter这三个之间是可以组合使用的。
需要在build.gradle中引入ASM
dependencies {implementation 'com.android.tools.build:gradle:8.6.1'//核心api,提供visitor apiimplementation 'org.ow2.asm:asm:9.6'//可选,提供了一些基于核心api的预定义类转换器implementation 'org.ow2.asm:asm-commons:9.6'//可选,提供了一些基于核心api的工具类implementation 'org.ow2.asm:asm-util:9.6'
}
读取、访问一个类
ClassVisitor的主要结构如下:
public abstract class ClassVisitor {//ASM的版本, 版本数值定义在Opcodes接口中,最低为ASM4,目前最新为ASM7protected final int api;//委托的ClassVisitor,可传空protected ClassVisitor cv;public ClassVisitor(final int api) {this(api, null);}public ClassVisitor(final int api, final ClassVisitor cv) {//...this.api = api;this.cv = cv;}//表示开始访问这个类public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {if (cv != null) {cv.visit(version, access, name, signature, superName, interfaces);}}//表示访问这个类的源文件名(如果有的话)public void visitSource(String source, String debug) {if (cv != null) {cv.visitSource(source, debug);}}//表示访问这个类的外部类(如果有的话)public void visitOuterClass(String owner, String name, String desc) {if (cv != null) {cv.visitOuterClass(owner, name, desc);}}//表示访问这个类的注解(如果有的话)public AnnotationVisitor visitAnnotation(String desc, boolean visible) {if (cv != null) {return cv.visitAnnotation(desc, visible);}return null;}//表示访问这个类的内部类(如果有的话)public void visitInnerClass(String name, String outerName,String innerName, int access) {if (cv != null) {cv.visitInnerClass(name, outerName, innerName, access);}}//表示访问这个类的字段(如果有的话)public FieldVisitor visitField(int access, String name, String desc,String signature, Object value) {if (cv != null) {return cv.visitField(access, name, desc, signature, value);}return null;}//表示访问这个类的方法(如果有的话)public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {if (cv != null) {return cv.visitMethod(access, name, desc, signature, exceptions);}return null;}//表示结束对这个类的访问public void visitEnd() {if (cv != null) {cv.visitEnd();}}//...省略了一些其他visitXX方法
}
可以发现,ClassVisitor的所有visitXX方法都把逻辑委托给另外一个ClassVisitor的visitorXX方法。
我们知道了ClassVisitor中方法的作用后,我们自定义一个类,
class PrintClassVisitor : ClassVisitor(ASM9),Opcodes{override fun visit(version: Int,access: Int,name: String?,signature: String?,superName: String?,interfaces: Array<out String>?) {println("$name extends $superName {")}override fun visitSource(source: String?, debug: String?) {println("source name = $source")}override fun visitOuterClass(owner: String?, name: String?, descriptor: String?) {println("Outer Class = $name")}override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {println(" annotation = $descriptor")return null}override fun visitInnerClass(name: String?,outerName: String?,innerName: String?,access: Int) {println(" inner class = $name")}override fun visitField(access: Int,name: String?,descriptor: String?,signature: String?,value: Any?): FieldVisitor? {println("field = $name")return null}override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor? {println("method = $name")return null}override fun visitEnd() {println("}")}}
在每个visitXX方法把类的相关信息打印出来,最后使用ClassReader读取OuterClass的class字节码,在accept方法中传入classVisitor实例,完成对OuterClass的访问
fun main(){//创建ClassVisitor实例val printClassVisitor: PrintClassVisitor = PrintClassVisitor()//从构造传入OuterClass的全权限定名,ClassReader会读取OuterClass字节码为字节数组val classReader:ClassReader = ClassReader(OuterClass::class.java.name)//在ClassReader的accept传入ClassVisitor实例,开启访问,第二个参数表示访问模式classReader.accept(printClassVisitor,0)
}
输出结果
生成一个类
ClassWriter可以凭空生成一个类,举个例子
定义一个接口
interface Author {val name: Stringget() = "YiRan"fun getAge():Int
}
使用ClassWriter生成Author接口的代码
val classWriter = ClassWriter(0)//生成类的头部classWriter.visit(V1_7, ACC_PUBLIC+ ACC_ABSTRACT+ ACC_INTERFACE,"com/example/gradlestudy/Author",null,"java/lang/Object",null)//生成文件名classWriter.visitSource("Author.java",null)//生成名为Name,值为YiRan的字段val fileVisitor = classWriter.visitField(ACC_PUBLIC+ ACC_FINAL+ ACC_STATIC,"NAME","LJava/lang/String",null,"YiRan")fileVisitor.visitEnd()//生成名为getAge,返回值为int方法val methodVisitor = classWriter.visitMethod(ACC_PUBLIC+ ACC_ABSTRACT,"getAge","()I",null,null)methodVisitor.visitEnd()//生成类完毕classWriter.visitEnd()val bytes = classWriter.toByteArray()// 将字节码写入文件val outputPath = "E:\\GradleStudy\\app\\src\\main\\java\\com\\example\\gradlestudy\\Author.class"FileOutputStream(outputPath).use { fos ->fos.write(bytes)}
输出相应的class文件
可以发现使用ClassWriter生成一个简单的接口代码量就很多,如果这是一个类,并且类中的方法有方法体,那么会更复杂,因此可以使用ASM Bytecode Outline插件来完成这繁琐的过程,,然后在你想要查看的Asm代码的类右键 -> Show Bytecode outline,就会在侧边窗口中显示这个类的字节码(Bytecode)和Asm代码(ASMified),点击ASMified栏目就会显示这个类的Asm码,例如下图就是Person接口的通过插件生成的Asm代码:
转换一个类
ClassReader可以用来读取一个类,ClassVisitor可以用来访问一个类,ClassWirter可以生成一个类,所以当把它们三个组合在一起时,我们可以把class字节码通过ClassReader读取,把读取到的class字节码通过扩展的ClassVisitor转换,转换后,再通过ClassWirter重新生成这个类,就可以达到转换一个类的目的。
class RemoveAnnotationClassVisitor(visitor: ClassVisitor) : ClassVisitor(ASM9,visitor),Opcodes {override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {//返回nullreturn null;}}
@Deprecated
public class OuterClass {private int mData = 1;public OuterClass(int data){mData = data;}public int getmData(){return mData;}class InnerClass {}}
fun main(){val classReader = ClassReader(OuterClass::class.java.name)val classWriter = ClassWriter(0)val removeAnnotationClassVisitor = RemoveAnnotationClassVisitor(classWriter)classReader.accept(removeAnnotationClassVisitor,0)val bytes = classWriter.toByteArray()val outputPath = "E:\\GradleStudy\\app\\src\\main\\java\\com\\example\\gradlestudy\\Outer.class"FileOutputStream(outputPath).use { fos ->fos.write(bytes)}
}
RemoveAnnotationClassVisitor代理了ClassWriter,RemoveAnnotationClassVisitor把OuterClass转换完成后就交给了ClassWriter,最终我们可以通过ClassWriter的toByteArray方法返回转换后的OuterClass类的字节数组,最后输出对应的类到指定目录。
可以看到转换后的类把前面的OuterClass类的注解移除掉了
通过想象把ClassVisitor串成一条转换链,把ClassReader为头,ClassWriter为尾,中间是一系列的ClassVisitor,ClassReader把读取到的class字节码经过一系列的ClassVisitor转换后到达ClassWriter,最终被ClassWriter生成新的class。
Transform Action
Transform API是AGP1.5就引入的特性,主要用于在Android构建过程中,在Class转Dex的过程中修改Class字节码。利用Transform API,我们可以拿到所有参与构建的class文件,然后使用ASM或其他字节码插桩工具进行修改,插入自定义逻辑。
但Transform已经被标记为废弃,在AGP9.0中移除
Transform Action介绍
Transform API是由Android Gradle插件提供,而Transform Action则是由Gradle提供,由Gradle提供统一的Transform API。
Gradle官方提供了详细的文档文档链接跳转,具体可参考文档
AsmClassVisitorFactory介绍
直接使用Transform Action和Transform API一样,需要手动处理增量编译的逻辑。AGP做了一层封装,提供了AsmClassVisitorFactory来方便我们使用Transform Action进行ASM操作。并且使用AsmClassVisitorFactory能够提升性能和减少代码量。
代码编写
实现AsmClassVisitorFactory
abstract class TimeCostTransform: AsmClassVisitorFactory<InstrumentationParameters.None> {companion object{const val TAG = "TimeCostTransform"}override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor {return TimeCostClassVisitor(nextClassVisitor,classContext.currentClassData.className,TimeCostPlugin.includePackages,TimeCostPlugin.excludePackages)}//对所有class文件都进行插桩处理override fun isInstrumentable(classData: ClassData): Boolean {return true}//自动增量编译,就是第一次会全部编译,后面只有更改了才编译,可通过观察打印信息
}
- AsmClassVisitorFactory即创建ClassVisitor对象的工厂。此接口的实现必须是一个抽象类
- createClassVisitor返回我们自定义的classVisitor,在自定义visitor处理完成后,需要传内容给下一个visitor,因此放其在构造函数中传入
- isInstrumentable用于控制我们自定义Visitor是否需要处理这个类,通过这个方法可以过滤我们不需要的类,从而提高编译速度
自定义ClassVisitor
class TimeCostClassVisitor(nextVisitor: ClassVisitor,private val className: String,private val includePackages: Set<String>,private val excludePackages: Set<String>) : ClassVisitor(Opcodes.ASM9, nextVisitor) {companion object{const val TAG = "TimeCostClassVisitor"}private var startTime:Int = 0private var endTime:Int = 0private var costTime:Int = 0private var thisMethodStack:Int = 0override fun visitMethod(access: Int,name: String?,descriptor: String?,signature: String?,exceptions: Array<out String>?): MethodVisitor {if(!shouldInstrumentClass()){return super.visitMethod(access, name, descriptor, signature, exceptions)}val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)val newMethodVisitor =object : AdviceAdapter(Opcodes.ASM9, methodVisitor, access, name, descriptor) {@Overrideoverride fun onMethodEnter() {//println("$TAG onMethodEnter-----> name $name className $className")// 方法开始if (isNeedVisiMethod(name)) {//long startTime = System.currentTimeMillis()mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);startTime = newLocal(Type.LONG_TYPE)mv.visitVarInsn(Opcodes.LSTORE,startTime)}super.onMethodEnter();}@Overrideoverride fun onMethodExit(opcode: Int) {//println("$TAG onMethodExit-----> name $name className $className opcode $opcode")// 方法结束if (isNeedVisiMethod(name)) {//long endTime = System.currentTimeMillis();//表示调用静态方法System.currentTimeMillis() 的返回值(一个 long 类型的时间戳)会被压入操作数栈顶mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);//创建了一个新的局部变量槽,用于存储 long 类型的值,newLocal 方法会返回这个局部变量的索引(index),并将其赋值给 endTimeendTime = newLocal(Type.LONG_TYPE)//System.currentTimeMillis() 的返回值从操作数栈被存储到局部变量 endTimemv.visitVarInsn(Opcodes.LSTORE, endTime)//long costTime = endTime - startTime;//从局部变量表中加载 endTime 的值到操作数栈mv.visitVarInsn(LLOAD,endTime)//从局部变量表中加载 startTime 的值到操作数栈mv.visitVarInsn(LLOAD,startTime)//操作数栈顶的两个 long 值会被弹出并相减(栈顶的值减去次栈顶的值),结果再被压入操作数栈mv.visitInsn(LSUB)costTime = newLocal(Type.LONG_TYPE)//将操作数栈顶的 long 值存储到局部变量表中mv.visitVarInsn(LSTORE,costTime)//判断costTime是否大于阈值mv.visitVarInsn(LLOAD,costTime)mv.visitLdcInsn((TimeCostPlugin.mThreshold).toLong())mv.visitInsn(LCMP)//if costTime <= sThreshold,就跳到end标记处,否则继续往下执行val end:Label = Label()mv.visitJumpInsn(IFLE,end)//StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0]mv.visitTypeInsn(Opcodes.NEW, "java/lang/Exception")mv.visitInsn(Opcodes.DUP)mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Exception", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);mv.visitInsn(Opcodes.ICONST_0);mv.visitInsn(Opcodes.AALOAD);thisMethodStack = newLocal(Type.getType(StackTraceElement::class.java))mv.visitVarInsn(ASTORE, thisMethodStack);/*** Log.e("TimeCost", String.format(* "===> %s.%s(%s:%s)方法耗时 %d ms",* thisMethodStack.getClassName(), //类的全限定名称* thisMethodStack.getMethodName(),//方法名* thisMethodStack.getFileName(), //类文件名称* thisMethodStack.getLineNumber(),//行号* costTime //方法耗时* )* );*/mv.visitLdcInsn("MethodTimeCost")mv.visitLdcInsn("===> %s.%s(%s:%s)\u65b9\u6cd5\u8017\u65f6 %d ms,线程名称:%s")mv.visitLdcInsn(6)mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_0)mv.visitVarInsn(ALOAD,thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getClassName", "()Ljava/lang/String;", false);mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_1)mv.visitVarInsn(ALOAD, thisMethodStack);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(Opcodes.DUP)mv.visitInsn(Opcodes.ICONST_2)mv.visitVarInsn(ALOAD,thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getFileName", "()Ljava/lang/String;", false);mv.visitInsn(AASTORE);mv.visitInsn(DUP);mv.visitInsn(ICONST_3);mv.visitVarInsn(ALOAD, thisMethodStack)mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StackTraceElement", "getLineNumber", "()I", false);mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);mv.visitInsn(AASTORE);mv.visitInsn(DUP);mv.visitInsn(ICONST_4)mv.visitVarInsn(Opcodes.LLOAD,costTime)mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false)mv.visitInsn(Opcodes.AASTORE)mv.visitInsn(DUP)mv.visitInsn(ICONST_5)//获取当前线程名称mv.visitMethodInsn(INVOKESTATIC,"java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false)mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getName", "()Ljava/lang/String;", false)mv.visitInsn(Opcodes.AASTORE)mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false);mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);mv.visitInsn(Opcodes.POP);//end标记处,即方法的末尾mv.visitLabel(end)}super.onMethodExit(opcode);}}return newMethodVisitor}//排除特定方法名 <clinit>类的静态初始化方法 <init>类的实例初始化方法private fun isNeedVisiMethod(name: String?):Boolean {return name != "<clinit>" && name != "<init>"}private fun shouldInstrumentClass(): Boolean {// 排除指定的包for (excludePackage in excludePackages) {if (className.startsWith(excludePackage)) {return false}}// 包含指定的包if (includePackages.isNotEmpty()) {for (includePackage in includePackages) {if (className.startsWith(includePackage)) {return true}}return false}// 默认插桩return true}
}
主要通过ASM字节码插桩,在方法的前后插入如下代码,通过计算两者的时间差得到方法耗时