ASM 插桩采集方法入参,出参及耗时信息

前言

ASM字节码插桩技术在Android开发中有着广泛的应用,但相信很多人会不知道怎么上手,不知道该拿ASM来做点什么。

学习一门技术最好的方法就是动手实践,本文主要通过ASM插桩采集方法的入参,出参及耗时信息并打印,通过一个不大不小的例子快速上手ASM插桩开发。

技术目标

我们先看下最终的效果

插桩前代码

首先来看下插桩前代码,就是一个sum方法

    private fun sum(i: Int, j: Int): Int {
        return i + j
    }

插桩后代码

接下来看下插桩后的代码

    private final int sum(int i, int j) {
        ArrayList arrayList = new ArrayList();
        arrayList.add(Integer.valueOf(i));
        arrayList.add(Integer.valueOf(j));
        MethodRecorder.onMethodEnter("com.zj.android_asm.MainActivity", "sum", arrayList);
        int i2 = i + j;
        MethodRecorder.onMethodExit(Integer.valueOf(i2), "com.zj.android_asm.MainActivity", "sum", "I,I", "I");
        return i2;
    }

可以看出,方法所有参数都被添加到了一个arrayList中,并且调用了MethodRecorder.onMethodEnter方法
而在结果返回之前,则会调用MethodRecorder.onMethodExit方法,并将返回值,参数类型,返回值类型等作为参数传递过支。

日志输出

在调用了onMethodExit之后,会计算出方法耗时并输出日志,如下所示

类名:com.zj.android_asm.MainActivity 
方法名:sum 
参数类型:[I,I] 
入参:[1,2] 
返回类型:I 
返回值:3 
耗时:0 ms 

技术实现

上面我们介绍了最后要实现的效果,下面就来看下怎么一步一步实现,主要分为以下3步:

  1. 在方法开始时采集方法参数
  2. 在方法结束时采集返回值
  3. 调用帮助类计算耗时及打印结果

ASM采集方法参数

采集方法参数的方法也很简单,主要就是读取出所有参数的值并存储在一个List中,主要问题在于我们需要用字节码来实现这些逻辑.

override fun onMethodEnter() {
    // 方法开始
    if (isNeedVisiMethod() && descriptor != null) {
        val parametersIdentifier = MethodRecordUtil.newParameterArrayList(mv, this)   //1. new一个List
        MethodRecordUtil.fillParameterArray(methodDesc, mv, parametersIdentifier, access) //2. 填充列表
		MethodRecordUtil.onMethodEnter(mv, className, name, parametersIdentifier) //3. 调用帮助类
    }
    super.onMethodEnter()
}

如上所示,采集方法参数也分为3步,接下来我来一步步看下代码

ASM创建列表
    fun newParameterArrayList(mv: MethodVisitor, localVariablesSorter: LocalVariablesSorter): Int {
    	// new一个ArrayList
        mv.visitTypeInsn(AdviceAdapter.NEW, "java/util/ArrayList")
        mv.visitInsn(AdviceAdapter.DUP)
        mv.visitMethodInsn(
            AdviceAdapter.INVOKESPECIAL,
            "java/util/ArrayList",
            "<init>",
            "()V",
            false
        )
        // 存储new出来的List
        val parametersIdentifier = localVariablesSorter.newLocal(Type.getType(List::class.java))
        mv.visitVarInsn(AdviceAdapter.ASTORE, parametersIdentifier)
        // 返回parametersIdentifier,方便后续访问这个列表
        return parametersIdentifier
    }

逻辑其实很简单,主要问题在于需要用ASM代码写,需要掌握一些字节码指令相关知识。不过我们也可以用asm-bytecode-outline来自动生成这段代码,这样难度可以降低不少。关于代码中各个指令的具体含义,可查阅Java虚拟机(JVM)字节码指令表

ASM填充列表

接下来要做的就是读出所有的参数并填充到上面创建的列表中

    fun fillParameterArray(
        methodDesc: String,
        mv: MethodVisitor,
        parametersIdentifier: Int,
        access: Int
    ) {
    	// 判断是不是静态函数
        val isStatic = (access and Opcodes.ACC_STATIC) != 0
        // 静态函数与普通函数的cursor不同
        var cursor = if (isStatic) 0 else 1
        val methodType = Type.getMethodType(methodDesc)
        // 获取参数列表
        methodType.argumentTypes.forEach {
        	// 读取列表
            mv.visitVarInsn(AdviceAdapter.ALOAD, parametersIdentifier)
            // 根据不同类型获取不同的指令,比如int是iload, long是lload
            val opcode = it.getOpcode(Opcodes.ILOAD)
            // 通过指令与cursor读取参数的值
            mv.visitVarInsn(opcode, cursor)
            if (it.sort >= Type.BOOLEAN && it.sort <= Type.DOUBLE) {
            	// 基本类型转换为包装类型
                typeCastToObject(mv, it)
            }
            // 更新cursor
            cursor += it.size
            // 添加到列表中
            mv.visitMethodInsn(
                AdviceAdapter.INVOKEINTERFACE,
                "java/util/List",
                "add",
                "(Ljava/lang/Object;)Z",
                true
            )
            mv.visitInsn(AdviceAdapter.POP)
        }
    }

主要代码如上所示,代码中都有注释,主要需要注意以下几点:

  1. 静态函数与普通函数的初始cursor不同,因此需要区分开来
  2. 不同类型的参数加载的指令也不同,因此需要通过Type.getOpcode获取具体指令
  3. 为了将参数放在一个列表中,需要将基本类型转换为包装类型,比如int转换为Integer
ASM调用帮助类
    fun onMethodEnter(
        mv: MethodVisitor,
        className: String,
        name: String?,
        parametersIdentifier: Int
    ) {
        mv.visitLdcInsn(className)
        mv.visitLdcInsn(name)
        mv.visitVarInsn(AdviceAdapter.ALOAD, parametersIdentifier)
        mv.visitMethodInsn(
            AdviceAdapter.INVOKESTATIC, "com/zj/android_asm/MethodRecorder", "onMethodEnter",
            "(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V", false
        )
    }

这个比较简单,主要就是通过ASM调用MethodRecorder.onMethodEnter方法

ASM采集返回值

override fun onMethodExit(opcode: Int) {
    // 方法结束
    if (isNeedVisiMethod()) {
        if ((opcode in IRETURN..RETURN) || opcode == ATHROW) {
            when (opcode) {
            	// 基本类型返回
                in IRETURN..DRETURN -> {
                	// 读取返回值
                    MethodRecordUtil.loadReturnData(mv, methodDesc)
                    MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
                }
                // 对象返回
                ARETURN -> {
                	// 读取返回值
                    mv.visitInsn(DUP)
                    MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
                }
                // 空返回
                RETURN -> {
                    mv.visitLdcInsn("void")
                    MethodRecordUtil.onMethodExit(mv, className, name, methodDesc)
                }
            }
        }
    }
    super.onMethodExit(opcode);
}

采集返回值的逻辑也很简单,主要分为以下几步

  1. 判断当前指令,并且根据不同类型的返回添加不同的逻辑
  2. 通过DUP指令复制栈顶数值并将复制值压入栈顶,以读取返回值
  3. 读取方法参数类型与返回值类型,并调用MethodRecorder.onMexthodExit方法

帮助类实现

由于ASM需要直接操作字节码,写起来终究不太方便,因此我们尽可能把代码转移到帮助类中,然后通过在ASM中调用帮助类来简化开发,帮助类的代码如下所示:

object MethodRecorder {
    private val mMethodRecordMap = HashMap<String, MethodRecordItem>()

    @JvmStatic
    fun onMethodEnter(className: String, methodName: String, parameterList: List<Any?>?) {
        val key = "${className},${methodName}"
        val startTime = System.currentTimeMillis()
        val list = parameterList?.filterNotNull() ?: emptyList()
        mMethodRecordMap[key] = MethodRecordItem(startTime, list)
    }

    @JvmStatic
    fun onMethodExit(
        response: Any? = null,
        className: String,
        methodName: String,
        parameterTypes: String,
        returnType: String
    ) {
        val key = "${className},${methodName}"
        mMethodRecordMap[key]?.let {
            val parameters = it.parameterList.joinToString(",")
            val duration = System.currentTimeMillis() - it.startTime
            val result = "类名:$className \n方法名:$methodName \n参数类型:[$parameterTypes] \n入参:[$parameters] \n返回类型:$returnType \n返回值:$response \n耗时:$duration ms \n"
            Log.i("methodRecord", result)
        }
    }
}

代码其实也很简单,主要逻辑如下:

  1. 方法开始时调用onMethodEnter方法,传入参数列表,并记录下方法开始时间
  2. 方法结束时调用onMethodExit方法,传入返回值,计算方法耗时并打印结果

总结

通过上述步骤,我们就把ASM插桩实现记录方法入参,返回值以及方法耗时的功能完成了,通过插桩可以在方法执行的时候输出我们需要的信息。而这些信息的价值就是可以很好的让我们做一些程序的全链路监控以及工程质量验证。

总得来说,逻辑上其实并不复杂,主要问题可能在于需要熟悉如何直接操作字节码,我们可以通过asm-bytecode-outline等工具自动生成代码来简化开发,同时也可以通过尽量把逻辑迁移到帮助类中的方式来减少直接操作字节码的工作。

示例代码

本文所有源码可见:

GitHub - RicardoJiang/Android-ASM: Asm插桩应用实例

参考资料

Java虚拟机(JVM)字节码指令表
ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时
Java ASM系列:(025)修改已有的方法(添加-进入和退出-打印方法参数和返回值)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android利用ASM(字节码操作框架)监控方法耗时的步骤如下: 1. 添加ASM依赖库 在build.gradle文件中添加ASM依赖库: ```gradle dependencies { implementation 'org.ow2.asm:asm:9.1' } ``` 2. 编写ASM代码 使用ASM编写一个ClassVisitor,用于修改字节码,在方法的开头和结尾插入计时代码: ```java public class TimeClassVisitor extends ClassVisitor { public TimeClassVisitor(int api, ClassVisitor cv) { super(api, cv); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); if (!name.equals("<init>") && !name.equals("<clinit>")) { mv = new TimeMethodVisitor(api, mv, access, name, descriptor); } return mv; } static class TimeMethodVisitor extends MethodVisitor { private final String mName; private final String mDesc; private final Label mStartLabel = new Label(); private final Label mEndLabel = new Label(); public TimeMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) { super(api, mv); mName = name; mDesc = desc; } @Override public void visitCode() { super.visitCode(); mv.visitLabel(mStartLabel); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitFieldInsn(PUTSTATIC, "com/example/MyClass", "sStartTime", "J"); } @Override public void visitInsn(int opcode) { if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) { mv.visitLabel(mEndLabel); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitFieldInsn(GETSTATIC, "com/example/MyClass", "sStartTime", "J"); mv.visitInsn(LSUB); mv.visitFieldInsn(PUTSTATIC, "com/example/MyClass", "sStartTime", "J"); } super.visitInsn(opcode); } @Override public void visitMaxs(int maxStack, int maxLocals) { mv.visitMaxs(maxStack + 4, maxLocals); } } } ``` 3. 在应用中使用ASM代码 在应用中调用ASM代码,在Activity的onCreate方法中插入以下代码: ```java try { ClassReader cr = new ClassReader(getClass().getName()); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); TimeClassVisitor tcv = new TimeClassVisitor(Opcodes.ASM7, cw); cr.accept(tcv, ClassReader.EXPAND_FRAMES); byte[] code = cw.toByteArray(); Class<?> clazz = defineClass(getClass().getName(), code, 0, code.length); clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } ``` 以上代码将读取当前Activity的字节码,使用TimeClassVisitor修改字节码,然后重新定义类并实例化,以便在方法中添加计时代码。 4. 打印方法耗时 在Activity的onDestroy方法中打印方法耗时: ```java Log.i("Time", "onCreate: " + (sStartTime / 1000000f) + "ms"); ``` 以上代码将打印onCreate方法耗时,单位为毫秒。 通过以上步骤,就可以使用ASM监控方法耗时了。需要注意的是,ASM会修改字节码,可能会导致应用崩溃或现其他问题,所以需要谨慎使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值