Android Transform+ASM添加try catch并返回默认值之明白讲义

Transform这个部分这里不作讲解,直接使用hunter库就行,里面对Transform遍历处理class文件都做好了封装,我们继承实现对应的方法就行。在build.gradle的dependencies里面添加依赖:

   // 使用hunter框架
    implementation('com.quinn.hunter:hunter-transform:0.9.3') {
        // 排除hunter带来的gradle传递依赖,以便自定义应用的gradle版本
        exclude group: 'com.android.tools.build'
    }

本文源代码:DxKit一个基于ASM的开发工具集:https://github.com/Dawish/DxKit

1. 查看try catch的生成原理

我们使用ASM Bytecode Viewer插件查看一个简单的try catch方法对应的ASM代码。在使用ASM Bytecode Viewer之前,在设置中记得把skip debug和skip frames勾选上,不勾选上,可能对产生一堆我们写ASM代码用不上的frame相关操作,我们自己在写ASM代码时,不建议直接操作frame。

ASM_Bytecode_Viewer设置.png

java源码:

       public int tryTest() {
        try {
            //1 try里面的方法执行
            int i = 3 / 0;
            //2 try里面正常,返回结果值
            return i;
        } catch (Exception e) {
            //3 发生异常
            ExceptionHandler.handleException(e);
            //4 异常处理完成返回默认值0
            return 0;
        }
    }

对应的 ASM代码

{
    // tryTest方法信息,方法签名为"()I"
    methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "tryTest", "()I", null, null);
    methodVisitor.visitCode();
    
    Label labelStart = new Label();     // try开始
    Label labelEnd = new Label();       // try结束
    Label labelHandler = new Label();   // catch开始处理异常
    
    // 添加TryCatch固定操作,传入上面几个label
    methodVisitor.visitTryCatchBlock(labelStart, labelEnd, labelHandler, "java/lang/Exception");
    //1 try里面的方法执行
    methodVisitor.visitLabel(labelStart);
    methodVisitor.visitInsn(ICONST_3);
    methodVisitor.visitInsn(ICONST_0);
    methodVisitor.visitInsn(IDIV);
    methodVisitor.visitVarInsn(ISTORE, 1);
    methodVisitor.visitVarInsn(ILOAD, 1);
    //2 try里面正常,返回结果值
    methodVisitor.visitLabel(labelEnd);
    methodVisitor.visitInsn(IRETURN);
    
    //3 发生异常,进入labelHandler
    methodVisitor.visitLabel(labelHandler);
    // 存储和加载Exception实例变量
    methodVisitor.visitVarInsn(ASTORE, 1);
    methodVisitor.visitVarInsn(ALOAD, 1);
    // 调用自定义异常处理方法
    methodVisitor.visitMethodInsn(INVOKESTATIC, "com/dxkit/library/utils/ExceptionHandler", "handleException", "(Ljava/lang/Exception;)V", false);
    //4 异常处理完成返回默认值0
    methodVisitor.visitInsn(ICONST_0);
    methodVisitor.visitInsn(IRETURN);
    
    // 方法结束固定操作
    methodVisitor.visitMaxs(2, 2);
    methodVisitor.visitEnd();
}

这简单的示例代码分四个步骤,java对应的ASM操作码,我都有注释。

methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "tryTest", "()I", null, null);

这句我们就能看出来,tryTest方法是public的,方法描述符是"()I" 说明方法没得参数,方法返回类型为int。

方法签名格式如下:

(参数类型描述符)返回值类型描述符

类型描述符:

类型描述符.png

举例方法签名:
方法描述符.png

这个跟我们写JNI开发的描述符是一样的。
在我们继承实现ClassVisitor的时候,在visitMethod方法中,会把方法签名descriptor回调给我们,通过descriptor我们就能判断任何方法的默认返回值是是啥。

2. 添加try catch把整个方法保护住

上面,我们知道了方法的ASM码与java代码的对应关系,也知道了方法描述符是怎么得来的,接下来就是找到切入点,把整个方法用try catch保护住。大概思路就是:

在方法开始之前,把try加上,在方法结束之前把catch以及异常处理添加上。

我们还是看上面的ASM代码:


ASM分段说明.png

methodVisitor.visitCode()标志方法开始进入。
methodVisitor.visitMaxs(2, 2)这里标志方法已经结束。

在AdviceAdapter对应的位置就是onMethodEntervisitMaxs,我们分别在这两个方法里面分别加上try和catch的代码就行。或者你想添加在visitCodevisitEnd方法里也行,最终代码都是一样的。
这样我们最终的代码为:

/**
 * @author danxingxi
 */
public class TryCatchClassVisitor extends ClassVisitor {

    private List methodList;
    TryCatchClassVisitor(ClassVisitor classVisitor, List methodList) {
        super(Opcodes.ASM6, classVisitor);
        this.methodList = methodList;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        LogUtil.log("TryCatchClassVisitor:" + "name : " + name);
        if (isNeedTryCatch(name)) {
            return new TryCatchMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor);
        } else {
            return methodVisitor;
        }
    }

    /**
     * 方法是否需要加try catch
     */
    private boolean isNeedTryCatch(String methodName) {
        if (methodList != null && methodList.contains(methodName)) {
            return true;
        }
        return false;
    }

    /**
     * 方法执行顺序
     * onMethodEnter
     * visitCode
     * onMethodExit
     * visitMaxs
     * visitEnd
     *
     * @author danxingxi
     */
    public class TryCatchMethodVisitor extends AdviceAdapter {

        // 方法返回值类型描述符
        private String methodDesc;

        private String exceptionHandleClass;

        private String exceptionHandleMethod;

        private Label startLabel = new Label(),   // 开头
                endLabel = new Label(),           // 结尾
                handlerLabel = new Label(),       // 处理
                returnLabel = new Label();        // 返回

        TryCatchMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);

            LogUtil.log("TryCatchMethodVisitor:" + "descriptor : " + descriptor);
            methodDesc = descriptor;

            Map exceptionHandler = TryCatchExtension.exceptionHandler;
            if (exceptionHandler != null && !exceptionHandler.isEmpty()) {
                exceptionHandler.entrySet().forEach(entry -> {
                    exceptionHandleClass = entry.getKey();
                    exceptionHandleMethod = entry.getValue();
                });
            }

        }

        // 开始执行方法,插入的代码会onMethodEnter插入的代码之后,在本来执行代码之前。
        @Override
        public void visitCode() {
            super.visitCode();
        }

        // 方法进入
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            // 1标志:try块开始位置
            mv.visitTryCatchBlock(startLabel,
                    endLabel,
                    handlerLabel,
                    "java/lang/Exception");
            mv.visitLabel(startLabel);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            // 2标志:try块结束
            mv.visitLabel(endLabel);

            // 3标志:catch块开始位置
            mv.visitLabel(handlerLabel);
            mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
            // 0代表this, 1 第一个参数,异常信息保存到局部变量
            mv.visitVarInsn(ASTORE, 1);
            // 从local variables取出局部变量到operand stack
            mv.visitVarInsn(ALOAD, 1);
            // 自定义异常处理
            if (exceptionHandleClass != null && exceptionHandleMethod != null) {
                mv.visitMethodInsn(INVOKESTATIC, exceptionHandleClass,
                        exceptionHandleMethod, "(Ljava/lang/Exception;)V", false);

            } else {
                // 没提供处理类就直接抛出异常
                mv.visitInsn(ATHROW);
            }

            // 顺序向下执行,可以不要GOTO
            //mv.visitJumpInsn(Opcodes.GOTO, returnLabel);
            // 返回label
            // mv.visitLabel(returnLabel);

            // catch结束,方法返回默认值收工
            Pair defaultVo = ASMUtil.getDefaultByDesc(methodDesc);
            int value = defaultVo.getKey();
            int opcode = defaultVo.getValue();
            if (value >= 0) {
                mv.visitInsn(value);
            }
            mv.visitInsn(opcode);
            super.visitMaxs(maxStack, maxLocals);

        }

        @Override
        public void visitEnd() {
            super.visitEnd();
        }
    }

}

3. 发生异常时返回默认值

首先我们看看java中各种类型对应的默认值

数据类型 默认值
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
boolean false
String null

其实只要是引用类型,就是对象,默认返回值就是null,不用管时数组还是多维数组,默认值也是 null。知道这个我们就可以根据方法描述符的返回类型来确认了。
具体操作的方法如下:

  /**
     * 根据方法描述符获取返回类型和默认值
     *
     * @param methodDesc
     * @return
     */
    public static Pair getDefaultByDesc(String methodDesc) {
        Pair pair = null;
        int value = -1;
        int opcode = -1;

        if (methodDesc.endsWith("[Z") ||
                methodDesc.endsWith("[I") ||
                methodDesc.endsWith("[S") ||
                methodDesc.endsWith("[B") ||
                methodDesc.endsWith("[C")) {
            value = Opcodes.ACONST_NULL;
            opcode = Opcodes.ARETURN;

        } else if (methodDesc.endsWith("Z") ||
                methodDesc.endsWith("I") ||
                methodDesc.endsWith("S") ||
                methodDesc.endsWith("B") ||
                methodDesc.endsWith("C")) {
            value = Opcodes.ICONST_0;
            opcode = Opcodes.IRETURN;

        } else if (methodDesc.endsWith("J")) {
            value = Opcodes.LCONST_0;
            opcode = Opcodes.LRETURN;

        } else if (methodDesc.endsWith("F")) {
            value = Opcodes.FCONST_0;
            opcode = Opcodes.FRETURN;

        } else if (methodDesc.endsWith("D")) {
            value = Opcodes.DCONST_0;
            opcode = Opcodes.DRETURN;

        } else if (methodDesc.endsWith("V")) {
            opcode = Opcodes.RETURN;

        } else {
            value = Opcodes.ACONST_NULL;
            opcode = Opcodes.ARETURN;
        }

        pair = new Pair<>(value, opcode);
        return pair;
    }

对于ASM来说,就是分为三大类,第一个就是为0,只不过不同类型的0对于的ASM中Opcodes码不一样,做一下区分就行,第二个就是为null,第三个就是无返回值的。
我们获取到的是一个 Pair,第一个是默认值,第二个是操作码,当默认值 >=0时,说明有默认值,当为-1时说明是无返回值的方法。

  Pair defaultVo = ASMUtil.getDefaultByDesc(methodDesc);
  int value = defaultVo.getKey();
  int opcode = defaultVo.getValue();
  if (value >= 0) {
      // 有默认值,加载
      mv.visitInsn(value);
  }
  // 针对不同的默认值执行不同操作码
  mv.visitInsn(opcode);

其中你要知道,就算是默认值都是0或者是 0.0, 但是在ASM中,或者是jvm指令中,对应的返回值和返回操作码都是不一样的。比如floatdouble,对应的默认返回值为FCONST_0DCONST_0,返回操作码为: FRETURNDRETURN,剩下的我就不一一说明了。
下面是方法验证:

源代码
    public Object getObj() {
        Object object = new Object();
        return object;
    }
    
    public double getDouble() {
        double a = 2.3d;
        return a;
    }

    public long getLong() {
        long a = 1234567L;
        return a;
    }

    public float getFloat() {
        float a = 231234213.45f;
        return a;
    }

    public short getShort() {
        short a = 32767;
        return a;
    }

    public byte getByte() {
        byte a = 127;
        return a;
    }
ASM添加try catch处理后
    public Object getObj() {
        try {
            Object object = new Object();
            return object;
        } catch (Exception var2) {
            ExceptionHandler.handleException(var2);
            return null;
        }
    }
    public double getDouble() {
    try {
        double a = 2.3D;
        return a;
    } catch (Exception var3) {
        ExceptionHandler.handleException(var3);
        return 0.0D;
    }

    public long getLong() {
        try {
            long a = 1234567L;
            return a;
        } catch (Exception var3) {
            ExceptionHandler.handleException(var3);
            return 0L;
        }
    }

    public float getFloat() {
        try {
            float a = 2.31234208E8F;
            return a;
        } catch (Exception var2) {
            ExceptionHandler.handleException(var2);
            return 0.0F;
        }
    }

    public short getShort() {
        try {
            short a = 32767;
            return a;
        } catch (Exception var2) {
            ExceptionHandler.handleException(var2);
            return 0;
        }
    }

    public byte getByte() {
        try {
            byte a = 127;
            return a;
        } catch (Exception var2) {
            ExceptionHandler.handleException(var2);
            return 0;
        }
    }

想了解更多请查看:本文源代码:DxKit一个基于ASM的开发工具集:https://github.com/Dawish/DxKit

你可能感兴趣的:(Android Transform+ASM添加try catch并返回默认值之明白讲义)