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 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指令中,对应的返回值和返回操作码都是不一样的。比如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