函数的耗时统计
函数的耗时统计是应用性能管理(Application Performance Management)中的一个重要方面,可能会影响到用户体验,比如说页面的卡顿,操作的不流畅,甚至可能会发生ANR。统计方法的方案大体上可以分为两种:
- 代码耗时统计法。
 - 字节码插桩耗时统计法。
 
代码耗时统计法侵入性强,需要修改每个需要统计的方法,在相应的方法体中增加相应的代码逻辑;字节码插桩耗时统计法无侵入性,需要配置相应的参数,编写相应的ASM逻辑。字节码插桩适合数量繁重,以及步骤统一的任务。函数做耗时统计就是这类任务。
ASM耗时统计的几种方式
- 增加属性字段,在插桩方法的开始时候,相应的属性字段设置为当前时刻的值,在插桩方法的结束之前重新获取当前时刻的值,减去属性的值,计算结果就是函数耗时的值。
 - 不需要增加属性字段,在插桩方法的开始的时候,分配一个临时变量来存储当前时刻的值,在插桩方法结束之前重新获取当前时刻的值,减去之前分配的临时变量的值,计算结果就是函数耗时的值。 方法1和方法2,各自有各自的优缺点。方法1的优点就是简单,插桩方法都使用统一的变量进行计算;方法1的缺点就是插桩方法使用了统一的变量(类变量或者实力变量)可能因为多线程导致计算结果不准确。方法2的缺点是开发复杂,优点就是准确。
 
ASM使用属性字段统计耗时
事例代码如下所示:
            
            
              typescript
              
              
            
          
          import static org.objectweb.asm.Opcodes.*;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class AddTimerClassVisitor extends ClassVisitor {
    private String mOwner;
    public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        mOwner = name;
    }
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (methodVisitor != null && !name.equals("<init>") && !"<clinit>".equals(name)) {
            MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) {
                @Override
                public void visitCode() {
                    mv.visitCode();
                    mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                            "currentTimeMillis", "()J");
                    mv.visitInsn(LSUB);
                    mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
                }
                @Override
                public void visitInsn(int opcode) {
                    if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                        mv.visitFieldInsn(GETSTATIC, mOwner, "timer", "J");
                        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                                "currentTimeMillis", "()J");
                        mv.visitInsn(LADD);
                        mv.visitFieldInsn(PUTSTATIC, mOwner, "timer", "J");
//                        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//                        mv.visitLdcInsn("Entering ASM");
//                        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
                    }
                    mv.visitInsn(opcode);
                }
                @Override
                public void visitMaxs(int maxStack, int maxLocals) {
                    super.visitMaxs(maxStack + 4, maxLocals);
                }
            };
            return newMethodVisitor;
        }
        return methodVisitor;
    }
    @Override
    public void visitEnd() {
        FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "timer",
                "J", null, null);
        if (fv != null) {
            fv.visitEnd();
        }
        cv.visitEnd();
    }
        事例代码中需要讲解,或者说需要注意的地方。
- AddTimerClassVisitor继承ClassVisitor。
 - 需要增加属性,需要重写AddTimerClassVisitor的visitEnd方法。在visitEnd方法调用之前执行新增相应的属性,调用visitField。visitField的参数包括属性访问控制修饰符,属性名称,描述符,签名(泛型),默认值。
 - 重写visitMethod方法,需要通过判断如果不是构造方法才进行相应的方法插桩。visitMehod方法的返回值是MethodVisitor,如果返回null代表方法删除;如果返回新建的其他MethodVisitor,代表方法的重写(尽量取一个不常用的变量名称,避免和类的其他变量名称冲突)。
 - 新建的MethodVisitor需要重新visitCode方法,在方法开始之前取得当前时刻赋值给timer。
 - 新建的MethodVisitor需要重新visitInsn方法,在方法结束之前取得当前时刻,并减去timer的值计算方法耗时。
 - 新建的MethodVisitor需要重新visitMaxs方法,因为 long占2个槽值,计算两个long的减法最多可能多占用4个槽值,所以比最大值可能多站4个槽值。
 - 如果只是计算耗时这种逻辑,建议使用SystemClock获取自Android设备启动以来的时刻,而不是使用时间戳(手机手动被调整时间导致计算时间有差错)。
 
ASM使用临时变量统计耗时
MethodVisitor子类事例代码如下所示:
            
            
              java
              
              
            
          
          import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.LocalVariablesSorter;
import org.objectweb.asm.*;
public class TimingMethodVisitor extends MethodVisitor {
    private LocalVariablesSorter lvs;
    private int startTimeVarIndex;
    public TimingMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }
    public void setLVS(LocalVariablesSorter lvs) {
        this.lvs = lvs;
    }
    @Override
    public void visitCode() {
        super.visitCode();
        // Insert: long startTime = System.nanoTime();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
        startTimeVarIndex = lvs.newLocal(Type.LONG_TYPE); // Allocate local variable
        mv.visitVarInsn(Opcodes.LSTORE, startTimeVarIndex);
    }
    @Override
    public void visitInsn(int opcode) {
        // Check if the opcode is a return instruction
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
            // Insert: long elapsedTime = System.nanoTime() - startTime;
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
            mv.visitVarInsn(Opcodes.LLOAD, startTimeVarIndex);
            mv.visitInsn(Opcodes.LSUB);
            // Print elapsed time
            mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitInsn(Opcodes.SWAP); // Swap PrintStream and elapsedTime
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
        }
        super.visitInsn(opcode);
    }
}
        MethodVisitor需要注意的地方:
- 重写visitCode方法,需要记录时间戳,放在startTimeVarIndex索引位置上。本地变量用LocalVariablesSorter来申请。
 - 重写visitInsn方法,获取当前时间戳,减去startTimeVarIndex索引位置上的时间戳,差值就是方法耗时。
 
ClassVisitor子类的事例代码:
            
            
              java
              
              
            
          
          import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.LocalVariablesSorter;
public class TimingClassVisitor extends ClassVisitor {
    public TimingClassVisitor(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 (mv != null && !name.equals("<init>")) { // Avoid modifying constructors
            LocalVariablesSorter lvs = new LocalVariablesSorter(access, descriptor, mv);
            TimingMethodVisitor tmv = new TimingMethodVisitor(api, lvs);
            tmv.setLVS(lvs);
            return tmv;
        }
        return mv;
    }
}
        ClassVisitor子类的注意事项:
- 重写visitMethod方法。构造方法不需要统计方法耗时,所以需要过滤。
 - LocalVariablesSorter的作用是分配本地变量,所以它应该作为TimingMethodVisitor属性,而不应该作为visitMethod的返回值,返回值应该是TimingMethodVisitor。
 
运行结果如下图所示:
 耗时统计基本功能已经做完了。
函数耗时还有需要做的地方
- 函数的类名和方法名需要展示出来。
 - 耗时的阈值需要设定,可以从本地设定,也外部配置平台获取。
 - 超过阈值的耗时函数的相关信息需要上报到APM系统,这样方便研发人员进行分析优化。
 - 统计函数耗时需要制定规则,哪类需要过滤,哪类不需要过滤需要制定规则。
 
总结
ASM可以做的事情很多,统计函数耗时是一个基本功能,我今天实现一个简单的耗时统计的功能。希望文章对您有所帮助。