Android字节码处理-三种常见的使用场景

279 阅读6分钟

升级Android Studio Ladybug Feature Drop

Androider(研发工程师)离不开使用Android Studio,一般情况下我自己私人电脑一般会追Android Studio的最新版本。

image.png 升级到Ladybug,同时Gradle升级到了8.3,捣鼓这个还花了点时间。

字节码ASM插桩插件

image.png Android Studio plugins市场中有两个ASM相关的插件,我比较喜欢ASM Bytecode Viewer。安装完ASM Bytecode Viewer插件之后,需要重启Android Studio才能生效。网上有网友经常说ASM Bytecode Viewer不能用了,应该是Android Studio版本升级之后.class文件存放的位置有变化,导致右键点击相应的java文件时候,选择ASM Bytecode Viewer相应的菜单选项的时候出现ASMPlugin内容为空。相应的方法是对.class文件进行相应的操作。

ASM统计插桩统计方法耗时

之前提供的插桩耗时统计的版本中没有打印类名和方法名,今天提供出来。Talk is cheap. Show me the code.今天我们按照倒叙的方式来描述整个字节码的过程。首先,先来分析MethodVisitor子类的代码,代码如下所示:

public class TraceMethodAdapter extends AdviceAdapter {

        private final String methodName;
        private final String className;
        private boolean find = false;


        protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
            super(api, mv, access, name, desc);
            this.className = className;
            this.methodName = name;
        }

        @Override
        public void visitTypeInsn(int opcode, String s) {
            if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
                find = true;
                mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");
                return;
            }
            super.visitTypeInsn(opcode, s);

        }

        @Override
        public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
            //需要排查CustomThread自己
//
            if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && find) {
                find = false;
                mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);
//                Log.e("asmcode", "className:%s, method:%s, name:%s", className, methodName, name);
                return;
            }
            super.visitMethodInsn(opcode, owner, name, desc, itf);

//
//            if (owner.equals("android/telephony/TelephonyManager") && name.equals("getDeviceId") && desc.equals("()Ljava/lang/String;")) {
//                Log.e("asmcode", "get imei className:%s, method:%s, name:%s", className, methodName, name);
//            }
        }

        private int timeLocalIndex = 0;

        @Override
        protected void onMethodEnter() {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            timeLocalIndex = newLocal(Type.LONG_TYPE); //这个是LocalVariablesSorter 提供的功能,可以尽量复用以前的局部变量
            mv.visitVarInsn(LSTORE, timeLocalIndex);
        }

        @Override
        protected void onMethodExit(int opcode) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, timeLocalIndex);
            mv.visitInsn(LSUB);//此处的值在栈顶
            mv.visitVarInsn(LSTORE, timeLocalIndex);//因为后面要用到这个值所以先将其保存到本地变量表中


            int stringBuilderIndex = newLocal(Type.getType("Ljava/lang/StringBuilder"));
            mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);//需要将栈顶的 stringbuilder 保存起来否则后面找不到了
            mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
            mv.visitLdcInsn(className + "." + methodName + " time:");
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitInsn(Opcodes.POP);
            mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
            mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitInsn(Opcodes.POP);
            mv.visitLdcInsn("Geek");
            mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);//注意: Log.d 方法是有返回值的,需要 pop 出去
            mv.visitInsn(Opcodes.POP);//插入字节码后要保证栈的清洁,不影响原来的逻辑,否则就会产生异常,也会对其他框架处理字节码造成影响

        }
    }

代码中有相应的注释,希望对你有所帮助。这里还需要对以下几点做解释说明:

  1. 类名className和方法名methodName是创建TraceMethodAdapter时候作为参数传递进来的。
  2. newLocal(Type.LONG_TYPE)是这个是LocalVariablesSorter提供的功能,可以尽量复用以前的临时变量。
  3. visitInsn(LSUB)就是将操作数栈中的两个数出栈。栈有先进后出和后进先出的特性。出栈的第一个数作为减数,出栈的第二个数作为被减数,并将结果重新压入操作数栈。代码中有很多都是操作数栈和局部变量之间的交互作用。
  4. 方法调用的时候,使用斜线作为间隔的类全名,类型限定符的时候前面加L。比如mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)这句代码java/lang/StringBuilder是类名,()Ljava/lang/String;方法名描述符中String使用Ljava/lang/String;。int stringBuilderIndex = newLocal(Type.getType("Ljava/lang/StringBuilder"))声明临时变量的使用就使用Ljava/lang/StringBuilder。

然后来分析raceClassAdapter类,代码如下:

public class TraceClassAdapter extends ClassVisitor {
    private String className;

    TraceClassAdapter(int i, ClassVisitor classVisitor) {
        super(i, 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);
        this.className = name;

    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {

        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className);
    }
   } 

TraceClassAdapter就是把类名className作为参数传递进来。

其次再看AddTimerClassVisitorFactory类,代码如下所示:

public abstract class AddTimerClassVisitorFactory implements AsmClassVisitorFactory<InstrumentationParameters.None> {
    @Override
    public ClassVisitor createClassVisitor(ClassContext classContext, ClassVisitor nextVisitor) {
        return new TraceClassAdapter(Opcodes.ASM9, nextVisitor);
    }

    @Override
    public boolean isInstrumentable(ClassData classData) {
        return classData.getClassName().contains("com.zj.android_asm");
    }
}

isInstrumentable方法,如果返回true代表此类需要处理。

最后来看MethodRecordPlugin类,代码如下所示:

class MethodRecordPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        val androidComponents = target.extensions.getByType(AndroidComponentsExtension::class.java)

        androidComponents.onVariants { variant ->
            variant.instrumentation.transformClassesWith(
                AddTimerClassVisitorFactory::class.java,
                InstrumentationScope.ALL
            ) {}
            variant.instrumentation.setAsmFramesComputationMode(
                FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
            )
        }
    }
}

整个流程也就是这样的,我们在build.gradle使用apply语法实际上是调用的Plugin,Plugin代码逻辑中instrumentation会注册AsmClassVisitorFactory,AsmClassVisitorFactory判断哪些类需要处理,createClassVisitor方法返回ClassVisitor, 根据相应的逻辑重写ClassVisitor相应的方法(比如修改相应的方法,增删改相应的数据等)。

运行结果如下图所示:

image.png

ASM方法调用替换

如果调用第三方框架的方法有问题,可能是逻辑错误,也可能是隐私相关的问题,同时我们也没有第三方框架的源码,没有办法通过直接修改的方式来实现,就可以用ASM把有问题的方法替换成没有问题的方法。今天就以简单的打印日志替换为例,代码如下面所示:

mport static org.objectweb.asm.Opcodes.ASM9;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class ReplaceMethodClassVisitor extends ClassVisitor {

     ReplaceMethodClassVisitor(ClassVisitor classVisitor) {
         super(ASM9, classVisitor);
    }


    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MethodVisitor(ASM9, mv) {
            @Override
            public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
                System.out.println("owner is " + owner + " name is " + name);
                if (owner.equals("java/io/PrintStream") && name.equals("println")) {
                    // 替换 System.out.println() 为 Log.d()
                    super.visitLdcInsn("Hello ASM"); // Log Message
//                    super.visitLdcInsn("Hello world"); // Log Message
                    super.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
                } else {
                    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
                }
            }
        };
    }

}

运行结果如下图所示:

image.png

ASM异常监控

防止抛出的NullPointerException、ArrayIndexOutOfBoundsException等崩溃,从而防止崩溃的发生,从而保证软件的稳定性。自动插入 try-catch,防止崩溃。 出现崩溃的代码如下所示:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)
        sayHello()
        test(500)
        testMethod()
    }

    fun sayHello() {
        println("Hello, World!")
    }
    fun testMethod() {
        val result: Int = 10 / 0 // 出现异常
    }

    private fun test(time: Long) {
        Thread.sleep(time)
    }

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

MainActivity调用了testMethod()方法,已经”Caused by: java.lang.ArithmeticException: divide by zero“异常,导致应用崩溃。 如何把整个方法体trycatch呢,示例代码如下所示:

public class TryCatchMethodVisitor extends MethodVisitor {

    private Label startLabel = new Label();
    private Label endLabel = new Label();
    private Label catchLabel = new Label();

    public TryCatchMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM9, mv);
    }


    @Override
    public void visitCode() {
        super.visitLabel(startLabel); // ✅ 标记 try 开始
        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
            super.visitLabel(endLabel); // ✅ 标记 try 结束
        }
        super.visitInsn(opcode);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitTryCatchBlock(startLabel, endLabel, catchLabel, "java/lang/Exception");

        super.visitLabel(catchLabel);
        super.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
        super.visitVarInsn(Opcodes.ASTORE, 1);

        super.visitLdcInsn("ASM");
        super.visitLdcInsn("Exception caught!");
        super.visitVarInsn(Opcodes.ALOAD, 1);
        super.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e",
                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)I", false);
        super.visitInsn(Opcodes.POP);

        super.visitInsn(Opcodes.RETURN); // ✅ 确保 return 存在

        super.visitMaxs(maxStack + 3, maxLocals);
    }

}

总结

ASM可以做的事情很多,统计函数耗时是一个基本功能,方法替换,自动插入 try-catch,防止崩溃。希望文章对您有所帮助。

参考资料

Android Studio Ladybug Feature Drop 稳定版已推出