Android字节码处理-函数耗时统计揭秘

函数的耗时统计

函数的耗时统计是应用性能管理(Application Performance Management)中的一个重要方面,可能会影响到用户体验,比如说页面的卡顿,操作的不流畅,甚至可能会发生ANR。统计方法的方案大体上可以分为两种:

  1. 代码耗时统计法。
  2. 字节码插桩耗时统计法。

代码耗时统计法侵入性强,需要修改每个需要统计的方法,在相应的方法体中增加相应的代码逻辑;字节码插桩耗时统计法无侵入性,需要配置相应的参数,编写相应的ASM逻辑。字节码插桩适合数量繁重,以及步骤统一的任务。函数做耗时统计就是这类任务。

ASM耗时统计的几种方式

  1. 增加属性字段,在插桩方法的开始时候,相应的属性字段设置为当前时刻的值,在插桩方法的结束之前重新获取当前时刻的值,减去属性的值,计算结果就是函数耗时的值。
  2. 不需要增加属性字段,在插桩方法的开始的时候,分配一个临时变量来存储当前时刻的值,在插桩方法结束之前重新获取当前时刻的值,减去之前分配的临时变量的值,计算结果就是函数耗时的值。 方法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();
    }

事例代码中需要讲解,或者说需要注意的地方。

  1. AddTimerClassVisitor继承ClassVisitor。
  2. 需要增加属性,需要重写AddTimerClassVisitor的visitEnd方法。在visitEnd方法调用之前执行新增相应的属性,调用visitField。visitField的参数包括属性访问控制修饰符,属性名称,描述符,签名(泛型),默认值。
  3. 重写visitMethod方法,需要通过判断如果不是构造方法才进行相应的方法插桩。visitMehod方法的返回值是MethodVisitor,如果返回null代表方法删除;如果返回新建的其他MethodVisitor,代表方法的重写(尽量取一个不常用的变量名称,避免和类的其他变量名称冲突)。
  4. 新建的MethodVisitor需要重新visitCode方法,在方法开始之前取得当前时刻赋值给timer。
  5. 新建的MethodVisitor需要重新visitInsn方法,在方法结束之前取得当前时刻,并减去timer的值计算方法耗时。
  6. 新建的MethodVisitor需要重新visitMaxs方法,因为 long占2个槽值,计算两个long的减法最多可能多占用4个槽值,所以比最大值可能多站4个槽值。
  7. 如果只是计算耗时这种逻辑,建议使用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需要注意的地方:

  1. 重写visitCode方法,需要记录时间戳,放在startTimeVarIndex索引位置上。本地变量用LocalVariablesSorter来申请。
  2. 重写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子类的注意事项:

  1. 重写visitMethod方法。构造方法不需要统计方法耗时,所以需要过滤。
  2. LocalVariablesSorter的作用是分配本地变量,所以它应该作为TimingMethodVisitor属性,而不应该作为visitMethod的返回值,返回值应该是TimingMethodVisitor。

运行结果如下图所示:

耗时统计基本功能已经做完了。

函数耗时还有需要做的地方

  1. 函数的类名和方法名需要展示出来。
  2. 耗时的阈值需要设定,可以从本地设定,也外部配置平台获取。
  3. 超过阈值的耗时函数的相关信息需要上报到APM系统,这样方便研发人员进行分析优化。
  4. 统计函数耗时需要制定规则,哪类需要过滤,哪类不需要过滤需要制定规则。

总结

ASM可以做的事情很多,统计函数耗时是一个基本功能,我今天实现一个简单的耗时统计的功能。希望文章对您有所帮助。

相关推荐
sjsjs114 分钟前
【数据结构-并查集】力扣1722. 执行交换操作后的最小汉明距离
数据结构·算法·leetcode
CoderIsArt16 分钟前
生成一个立方体贴图(Cube Map)
算法·sharpgl
且听风吟ayan22 分钟前
leetcode day20 滑动窗口209+904
算法·leetcode·c#
m0_6759882323 分钟前
Leetcode350:两个数组的交集 II
算法·leetcode·数组·哈希表·python3
_Itachi__23 分钟前
LeetCode 热题 100 160. 相交链表
算法·leetcode·链表
m0_6759882326 分钟前
Leetcode1206:设计跳表
算法·leetcode·跳表·python3
冠位观测者28 分钟前
【Leetcode 每日一题 - 扩展】1512. 好数对的数目
数据结构·算法·leetcode
Joyner201828 分钟前
python-leetcode-路径总和 III
算法·leetcode·职场和发展
南宫生28 分钟前
力扣每日一题【算法学习day.133】
java·学习·算法·leetcode
_Itachi__29 分钟前
LeetCode 热题 100 560. 和为 K 的子数组
数据结构·算法·leetcode