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<String> methodList;
    TryCatchClassVisitor(ClassVisitor classVisitor, List<String> 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<String, String> 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<Integer, Integer> 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<Integer, Integer> getDefaultByDesc(String methodDesc) {
        Pair<Integer, Integer> 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<Integer, Integer> 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、ASM 的優勢和逆勢 使用 ASM 操作字節碼的優勢與逆勢都 比較明顯,其分別如下所示。 1、ASM 的優勢 ...
    waiwaaa閱讀 364評論 0 1
  • AspectJ 非常強大,但是它也只能實現 50% 的字節碼操作場景,如果想要實現 100% 的字節碼操作場景,那...
    凱玲之戀閱讀 917評論 0 4
  • 成為一名優秀的Android開發,需要一份完備的知識體系[https://github.com/Android-A...
    字節跳不動閱讀 594評論 0 0
  • 前言 前面一篇文章 ASM 簡介[http://www.lxweimin.com/p/a85e8f83fa14] ...
    Whyn閱讀 11,728評論 1 22
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,098評論 0 4