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。
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。
方法簽名格式如下:
(參數類型描述符)返回值類型描述符
類型描述符:
舉例方法簽名:
這個跟我們寫JNI開發的描述符是一樣的。
在我們繼承實現
ClassVisitor
的時候,在visitMethod
方法中,會把方法簽名descriptor
回調給我們,通過descriptor
我們就能判斷任何方法的默認返回值是是啥。
2. 添加try catch把整個方法保護住
上面,我們知道了方法的ASM碼與java代碼的對應關系,也知道了方法描述符是怎么得來的,接下來就是找到切入點,把整個方法用try catch保護住。大概思路就是:
在方法開始之前,把try加上,在方法結束之前把catch以及異常處理添加上。
我們還是看上面的ASM代碼:
methodVisitor.visitCode()標志方法開始進入。
methodVisitor.visitMaxs(2, 2)這里標志方法已經結束。
在AdviceAdapter對應的位置就是onMethodEnter
和 visitMaxs
,我們分別在這兩個方法里面分別加上try和catch的代碼就行。或者你想添加在visitCode
和visitEnd
方法里也行,最終代碼都是一樣的。
這樣我們最終的代碼為:
/**
* @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指令中,對應的返回值和返回操作碼都是不一樣的。比如float
和 double
,對應的默認返回值為FCONST_0
和 DCONST_0
,返回操作碼為: FRETURN
和 DRETURN
,剩下的我就不一一說明了。
下面是方法驗證:
源代碼
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