“J”, null, null);
if (fv != null) {
fv.visitEnd();
}
cv.visitEnd();
}
}
demo 代码,实际生产环境需要做更严格的条件校验。
我们给原类添加了一个 timer 字段,访问修饰符是 public static,并且其类型是 J 也就是 long 类型。
我们把代码组装到一起:
public class ClassWriterTest {
public static void main(String[] args) throws Exception {
Class clazz = C.class;
String clazzFilePath = Utils.getClassFilePath(clazz);
ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath));
ClassWriter classWriter = new ClassWriter(0);
AddTimerClassVisitor addTimerClassVisitor = new AddTimerClassVisitor(Opcodes.ASM5, classWriter);
classReader.accept(addTimerClassVisitor, 0);
// 写入文件
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream(“/Users/zhy/Desktop/copyed.class”);
fos.write(bytes);
fos.flush();
fos.close();
}
}
然后我们运行下。
运行生成的类,反编译看一下:
开心…
接下来我们要尝试修改字节码了,为方法添加耗时信息打印了。
修改方法
通过上文的学习,我们之前对于方法的遍历,会执行 ClassVisitor的 visitMethod 方法,修改方法肯定是离不开这个方法了,所以我们详细的看下这个方法:
ClassVisitor
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, descriptor, signature, exceptions);
}
return null;
}
可以看到,这个方法的参数中包含了方法所有声明相关的信息,但是没有包含实际运行的代码相关信息,即指令信息。
不过可以看到,这个方法的返回值并不是 null,而是一个 MethodVisitor,所以我们 ClassReader 遍历class 文件的思路肯定是:先给你方法声明相关信息,然后我们给它返回一个 MethodVisitor,它拿到这个 MethodVisitor,再通过 MethodVisitor开始遍历这个方法内部的所有信息。
所以…我们需要自定义一个 MethodVisitor 完成代码的插入。
先撸一点代码:
public class AddTimerClassVisitor extends ClassVisitor {
public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) {
};
return newMethodVisitor;
}
…
我们在刚才的AddTimerClassVisitor中复写了visitMethod,再其内部我们自定义了一个 MethodVisitor 代理一波本来的对象。
问题又来了?通过举一反三的思想,我们应该能够猜到 MethodVisitor 跟 ClassVisitor 设计应该是类似的,里面一堆 visitXXX 方法,我们这次修改字节码是在方法前后分别注入代码,那么到底该选择复写哪些方法呢?
这就要求我们知道 MethodVisitor 中各种 visitXXX 方法的执行顺序了:
visitAnnotationDefault?
(visitAnnotation |visitParameterAnnotation |visitAttribute )* ( visitCode
(visitTryCatchBlock |visitLabel |visitFrame |visitXxxInsn | visitLocalVariable |visitLineNumber )*
visitMaxs )? visitEnd
首先是遍历一些注解、参数相关信息;从 visitCode 开始遍历一整个方法。
我们的注入是:
-
方法开始:我们选择复写 visitCode 方法;
-
RETURN 之前:我们选择复写 visitXxxInsn,再其内部判断当前指令是否是 RETURN;
选择好了注入的时机,问题来了,我们好像还不知道注入的代码怎么写呢?
是的,这里其实要求大家对字节码是有足够的掌握,不然我怎么写估计都不太好理解,但是我尽量用推导的方式,引导大家去理解。
首先我们要了解我们添加的代码,会以字节码指令的方式注入进去,所以我们要先大概看下在字节码的层面上变化是怎样的。
所以修改之前,我们要看分别看一下修改前与修改后对应的方法字节码:
public void m() throws Exception {
Thread.sleep(100);
}
对应字节码:
public void m() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // long 100l
3: invokestatic #4 // Method java/lang/Thread.sleep:(J)V
6: return
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/imooc/blogdemo/blog03/C;
Exceptions:
throws java.lang.Exception
}
public void m() throws Exception {
timer -= System.currentTimeMillis();
Thread.sleep(100);
timer += System.currentTimeMillis();
}
对应字节码:
我框起来这两块代码,就是我们新增的字节码指令,那么我们实际要编写的代码和这两块新增的字节码是一一对应的。
先看我们方法最前面添加的指令:
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, mOwner, “timer”, “J”);
mv.visitMethodInsn(INVOKESTATIC, “java/lang/System”,
“currentTimeMillis”, “()J”);
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, mOwner, “timer”, “J”);
}
你仔细观察一下,其实和我们框起来的字节码对比:
0: getstatic #2 // Field timer:J
3: invokestatic #3 // Method java/lang/System.currentTimeMillis:()J
6: lsub
7: putstatic #2 // Field timer:J
基本上没有任何区别。
不过我们还是解释下这几行字节码:
-
拿当前类静态变量 timer,压到操作数栈
-
调用 System. System.currentTimeMillis,方法返回值压到操作数栈;
-
调用 “timer - System. System.currentTimeMillis”,结果压栈
-
将 3 得到的值,再次赋值给 timer 字段;
换成代码其实就是:
timer -= System.currentTimeMillis();
同样的,我们把方法 RETURN 前的代码也写了:
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, mOwner, “timer”, “J”);
mv.visitMethodInsn(INVOKESTATIC, “java/lang/System”,
“currentTimeMillis”, “()J”);
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, mOwner, “timer”, “J”);
}
mv.visitInsn(opcode);
}
你可以采用和上面相同的方式去对比字节码。
有一点不同的是,对于 RETURN 这个指令,我们判断了多个,因为我们并不知道当前方法的返回值情况,如果确定方法没有返回值,那么只要判断 RETURN 即可。
好了,我们贴下完整的代码:
package com.imooc.blogdemo.blog03;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import static org.objectweb.asm.Opcodes.*;
public class AddTimerClassVisitor extends ClassVisitor {
private String mOwner;
public AddTimerClassVisitor(int api, ClassVisitor classVisitor) {
super(api, 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);
mOwner = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (methodVisitor != null && !name.equals(“”)) {
MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) {
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, mOwner, “timer”, “J”);
mv.visitMethodInsn(INVOKESTATIC, “java/lang/System”,
“currentTimeMillis”, “()J”);
mv.visitInsn(LSUB);
mv.visitFieldInsn(PUTSTATIC, mOwner, “timer”, “J”);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitFieldInsn(GETSTATIC, mOwner, “timer”, “J”);
mv.visitMethodInsn(INVOKESTATIC, “java/lang/System”,
“currentTimeMillis”, “()J”);
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, mOwner, “timer”, “J”);
}
mv.visitInsn(opcode);
}
};
return newMethodVisitor;
}
return methodVisitor;
}
@Override
public void visitEnd() {
FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, “timer”,
“J”, null, null);
if (fv != null) {
fv.visitEnd();
}
cv.visitEnd();
}
}
注:这其实就是官方文档中的一个例子。
然后我们运行一下,在桌面生成 copyed.class 文件。
完美,反编译一下,正常。
这个时候,还不能开心的太早,可以反编译不代表你代码编写就没有错误!
我们来验证下文件的正确性:
简单修改了一下输出文件的路径:
byte[] bytes = classWriter.toByteArray();
// 修改,为了一会能 java 执行
FileOutputStream fos = new FileOutputStream(“/Users/zhy/Desktop/com/imooc/blogdemo/blog03/C.class”);
fos.write(bytes);
fos.flush();
fos.close();
然后我们切到桌面,执行下:
java com.imooc.blogdemo.blog03.C
输出:
192:Desktop zhy$ java com.imooc.blogdemo.blog03.C
Error: A JNI error has occurred, please check your installation and try again
Exception in thread “main” java.lang.VerifyError: Operand stack overflow
Exception Details:
Location:
com/imooc/blogdemo/blog03/C.m()V @3: invokestatic
Reason:
Exceeded max stack size.
Current Frame:
bci: @3
flags: { }
locals: { ‘com/imooc/blogdemo/blog03/C’ }
stack: { long, long_2nd }
Bytecode:
0x0000000: b200 12b8 0018 65b3 0012 1400 19b8 0020
0x0000010: b200 2412 26b6 002c b200 12b8 0018 61b3
0x0000020: 0012 b1
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
at java.lang.Class.getMethod0(Class.java:3018)
at java.lang.Class.getMethod(Class.java:1784)
at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
报错了!我们讲的头头世道,结果一运行,啪啪打脸。
为什么呢?
我们观察下报错原因:Exceeded max stack size.
看起来我们忘记了一个细节。
什么细节呢?
我们需要回归下,刚才代码修改前后的字节码,其中有个细节我们忽略了:
// 前
public void m() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
// 后
public void m() throws java.lang.Exception;
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
大家注意到没有,方法有个 stack值变化了!
这个值是什么意思呢?
他指的是我们操作数栈需要的深度,我们的 Java字节码指令,其中很多指令操作都会压栈操作,然后有些指令或者方法调用会弹出栈中的操作数去执行,例如 lsub就会弹出操作数栈里面的两个 long 类型的值去做减法,完成后再把结果压栈。
在这个方法中所有的指令执行完成,所需要的栈的深度在编译期就确定了,从反编译结果来看我们需要变成 4。
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
MethodVisitor newMethodVisitor = new MethodVisitor(api, methodVisitor) {
@Override
public void visitCode() {
// 省略
}
@Override
public void visitInsn(int opcode) {
// 省略
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(4, maxLocals);
}
};
return newMethodVisitor;
}
再次运行,生成 C.class,然后再次 java 一下:
Desktop zhy$ java com.imooc.blogdemo.blog03.C
错误: 在类 com.imooc.blogdemo.blog03.C 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
这次的错误终于正常了,因为我们没有写 main() 方法。
不过,事情并没有结束。
我们把 maxStack 写成 4 合理吗?
mv.visitMaxs(4, maxLocals);
明显不合理,我们只是针对我们这个单一的方法,有可能之前某个方法 maxStack是 10。
所以这个修改是极度不合理的,那么我们是不清楚经过我们的修改到底给多少合适的,所以我们可以定义一个增量值。
在我们刚才修改的字节码中:
getstatic timer // 压栈 2
invoke System.currentTimeMillis // 压栈 2
LSUB // 出栈4,压栈 2
put static timer // 出栈 2
你可以按照上述也分析下 RETURN 之前插入的代码,最大的压栈数字是 4,也就说我们最大会增加 4 个操作数栈的深度。
你可能会有疑问,为什么 getstatic timer,压栈是 2 不是 1 吗?
因为 long 类型占两个位置。
所以我们应该写成:
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs( maxStack + 4, maxLocals);
}
然后验证一下,也没问题。
不过stack 变成了 6,实际上 4 就足够了。
所以有没有更好的方式呢?
有的!
我们在构建 ClassWriter 的时候,代码是这么写的:
ClassWriter classWriter = new ClassWriter(0);
注意构造方法传入了一个 0,实际上接受的是一个 flag,其实有个 flag 是:
ClassWriter.COMPUTE_FRAMES
我们可以构建时传入:
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
这样就可以删除刚才的 visitMaxs相关代码了,它会自动帮我们重新计算 stackSize。
这个时候,你再重新运行,再次反编译,又可以看到 stack =4 啦。
到这里,初步的一个字节码修改案例就到这了。
可以看出来,如果你想通过 ASM修改class文件,最起码你得:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
**Android精讲视频学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!
**任何市场都是优胜略汰适者生存,只要你技术过硬,到哪里都不存在饱和不饱和的问题,所以重要的还是提升自己。懂得多是自己的加分项 而不是必须项。门槛高了只能证明这个市场在不断成熟化!**另外一千个读者就有一千个哈姆雷特,所以以上只是自己的关键,不喜勿喷!
如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。欢迎关注会持续更新和分享的。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
-1712608862451)]
[外链图片转存中…(img-SYpOh0Xp-1712608862451)]
[外链图片转存中…(img-xGgIfsuV-1712608862451)]
[外链图片转存中…(img-GVj1D8Fu-1712608862452)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-a4NO66AO-1712608862452)]
【延伸Android必备知识点】
[外链图片转存中…(img-21N6p4YF-1712608862452)]
【Android部分高级架构视频学习资源】
**Android精讲视频学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!
**任何市场都是优胜略汰适者生存,只要你技术过硬,到哪里都不存在饱和不饱和的问题,所以重要的还是提升自己。懂得多是自己的加分项 而不是必须项。门槛高了只能证明这个市场在不断成熟化!**另外一千个读者就有一千个哈姆雷特,所以以上只是自己的关键,不喜勿喷!
如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。欢迎关注会持续更新和分享的。
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-cR8v3aaF-1712608862453)]