| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android ASM字节码插桩 -> 正文阅读 |
|
[移动开发]Android ASM字节码插桩 |
1.ASM ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。 插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。 编写好的代码经过编译后如下: ?经过字节码插桩后如下: 比如需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。 ASM框架就是操作java字节码的框架,按照class文件的格式,解析、修改、生成class,可以动态生成类或者增强现有类的功能。热修复、systrace都使用了字节码插桩。 我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。 ?字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。 ? 2.ASM的使用 ①引入ASM依赖: ?使用testImplementation引入,表示只能在Java的单元测试中使用这个框架,对Android中的依赖关系没有任何影响。 注:AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。 ②准备待插桩Class 在 test/java下面创建一个Java类: InjectTest.java: public class InjectTest { ? ? public static void main(String[] args) throws InterruptedException{ ? ? ? ? Thread.sleep(1000); ? ? } } 由于我们操作的是字节码插桩,也就是class文件,所以需要进入 test/java下面使用 javac对这个java类进行编译生成对应的class文件: javac com/demo/test/InjectTest.java 执行上面的命令编译后,就会在test/java下面生成对应的InjectTest.class文件,这个class文件就是待插桩的文件。 ③执行插桩 待插桩的class文件准备好了,接下来这写个单元测试来执行插桩吧。利用ASM向 main方法中插入一开始图中的记录函数执行时间的日志输出。 在test/java下新建ASMUnitTest.java文件: public class?ASMUnitTest { ? ? @Test ? ? public void test() { ? ? ? ? //1 准备待分析的class ? ? ? ? FileInputStream fis = new FileInputStream("src/test/java/com/demo/test/InjectTest.class"); ? ? ? ? //2 执行分析与插桩 ? ? ? ? ClassReader cr = new ClassReader(fis); // ClassReader是class字节码的读取与分析引擎 ? ? ? ? ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_FRAMES); // 写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小 ? ? ? ? cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);? //执行分析,处理结果写入cw, EXPAND_FRAMES表示栈图以扩展格式进行访问 ? ? ? ? //3、获得执行了插桩之后的字节码数据 ? ? ? ? byte[] newClassBytes = cw.toByteArray(); ? ? ? ? File file = new File("src/test/java2/"); ? ? ? ? file.mkdirs(); ? ? ? ? FileOutputStream fos = new FileOutputStream("src/test/java2/com/demo/test/InjectTest.class"); ? ? ? ? fos.write(newClassBytes); ? ? ? ? fos.close(); ? ? } } 这一步会首先获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java2目录下。其中关键点就在于第2步中,即如何进行插桩: 把class数据交给ClassReader进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor。 public class ClassAdapterVisitor extends ClassVisitor { ? ? public ClassAdapterVisitor(ClassVisitor cv) { ? ? ? ? super(Opcodes.ASM7, cv); ? ? } ? ? @Override ? ? public MethodVisitor visitMethod(int access, String name, String desc, String signature,String[] exceptions) { ? ? ? ?System.out.println("方法:" + name + " 签名:" + desc); ? ? ? ? MethodVisitor mv = super.visitMethod( access, name, desc, signature, exceptions); ? ? ? ? return new MethodAdapterVisitor(api,mv, access, name, desc); ? ? } } 分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader将会调用ClassAdapterVisitor中对应的visitMethod、 visitAnnotation、 visitField这些 visitXX方法。 我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。 //AdviceAdapter: 子类,对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析 public class MethodAdapterVisitor extends AdviceAdapter { ? ? protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { ? ? ? ? super(api, methodVisitor, access, name, descriptor); ? ? } ? ? private int start; ? ? @override ? ? protected void onMethodEnter() { ? ? ? ? super.onMethodEnter(); ? ? ? ? //进入方法时,插入 long l = System.currentTimeMillis();? ? ? ? ?? ?invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); //执行System.currentTimeMillis(); ? ? ? ??start = newLocal(Type.LONG_TYPE);?//创建本地LONG类型变量? ? ? ? ??storeLocal(start); //将上一步方法执行结果保存到创建的本地变量中 ? ? } ? ? @override ? ? ?protected void onMethodExit(int opcode) { ? ? ? ? super.onMethodExit(opcode); ? ? ? ? //退出方法时,插入 long e = System.currentTimeMillis();?? ? ? ? ? invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); ? ? ? ??int end = newLocal(Type.LONG_TYPE); ? ? ?? ?storeLocal(end); ? ? ? ??//退出方法时,插入System.out.println( "execute" + (e - l) + "ms."); ? ? ? ? getStatic(Type.getType( "Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;")); //执行System.out ? ? ? ? newInstance(Type.getType( "Ljava/lang/StringBuilder;")); // 执行new StringBuilder分配内存 ? ? ? ? dup(); //dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder ? ? ?? ?invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V")); //调用StringBuilder的构造方法 ? ?? ? ?visitLdcInsn("execute:");? ? ? ? ? invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;) Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法? ?? ? ?? ? ?loadLocal(end); // 加载方法结束的时间 ? ?? ? ?loadLocal(start); //加载方法开始的时间 ? ?? ? ?math(SUB,Type.LONG_TYPE); //减法 ? ? ? ??invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法? ?? ?? ? ? ?invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;")); // 调用StringBuilder的toString方法? ? ? ? ? invokeVirtual(Type.getType( "Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V")); // 调用StringBuilder的println方法 ? ??} ? ? @override ? ? public AnnotationVisitor visitAnnotation(String description, boolean visible) { //获取注解 ? ? ? ? System.out.println("方法名为:" + getName() + "对应的注解为:" + description); ? ? ? ? return super.visitAnnotation(description, visible); ? ? } } MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。 上述代码中onMethodEnter在进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。 onMethodEnter和onMethodExit方法里的代码怎么写?其实onMethodEnter方法里就是 long s = System.currentTimeMillis();这句代码的相对的指令。而onMethodExit方法里就是long e = System.currentTimeMillis();? ?System.out.println("execute" + (e - s) + "ms.");这两句代码相对应的指令。 我们可以先写一份代码: void test(){ ? ? ?//进入方法时插入的代码 ? ? ?long s = System.currentTimeMillis(); ? ? // 退出方法时插入的代码 ? ? long e = System.currentTimeMillis();? ? ? System.out.println("execute" + (e - s) + "ms."); } 然后使用javac编译成Class再使用javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。 ?安装完成之后,可以在需要插桩的类源码中点击右键: ?点击ASM Bytecode Viewer之后会弹出 ?所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATIC与 LSTORE。 再回到 onMethodEnter方法中: @override protected void onMethodEnter() { ? ? super.onMethodEnter(); ? ? ? ? //invokeStatic指令,调用静态方法 ? ? ? ??invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); // 相当于java中System.currentTimeMillis();这一句代码 ? ?? ?//用一个本地变量接收上一步的执行结果 ? ? ? int start = newLocal(Type.LONG_TYPE); //start表示当前long类型的本地变量的索引 ? ?? storeLocal(start);? //store指令,将方法执行结果从操作数栈存储到局部变量 } invokeStatic指令涉及到几个名词: ①类型描述符 Java代码中的类型,在字节码中有相应的表示协议: Java Type? ? ? ? Type description ? ?boolean? ? ? ? ? ? ? ? ? ? Z ? ? ?char? ? ? ? ? ? ? ? ? ? ? ? C ? ? ? byte? ? ? ? ? ? ? ? ? ? ? ? B ? ? ?short? ? ? ? ? ? ? ? ? ? ? ?S ? ? ? int? ? ? ? ? ? ? ? ? ? ? ? ? ?I ? ? ?float? ? ? ? ? ? ? ? ? ? ? ? F ? ? ? long? ? ? ? ? ? ? ? ? ? ? ?J ? ? ?double? ? ? ? ? ? ? ? ? ? D ? ? object? ? ? ? ? ?Ljava/lang/Object; ? ? ?int[]? ? ? ? ? ? ? ? ? ? ? ? ?[I ? Object[][]? ? ? ? ? [[Ljava/lang/Object; ? ? ?void? ? ? ? ? ? ? ? ? ? ? ??V ? 引用类型? ? ? ? ? ? ? ? ??L (1)Java基本类型的描述符是单个字符,例如Z表示boolean、C表示char (2)类的类型的描述符是这个类的全限定名,前面加上字符L , 后面跟上一个「;」,例如String的类型描述符为Ljava/lang/String; (3)数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符,多维数组则使用多个方括号。 借助上面的协议分析,想要看到字节码中参数的类型,就比较简单了。 ②方法描述符 方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。 方法描述符以左括号开头,然后是每个形参的类型描述符,然后是是右括号,接下来是返回类型的类型描述符,例如,该方法返回void,则是V,要注意的是,方法描述符中不包含方法的名字或参数名。 比如: void m(int i, float f)对应的方法描述符是(IF)V ,表明该方法会接收一个int和float型参数,且无返回值。 int m(Object o)对应的方法描述符是(Ljava/lang/Object;)I 表示接收Object型参数,返回int。 int[] m(int i, String s)对应的方法描述符是(ILjava/lang/String;)[I 表示接受int和String,返回一个int[]。 Object m(int[] i)对应的方法描述符是 ([I)Ljava/lang/Object; 表示接受一个int[],返回Object。 ? 同样,onMethodExit也根据指令去编写代码: onMethodExit中需要插入的代码在ASMByteCode中的格式如下: ?对应的代码如下: invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); int end = newLocal(Type.LONG_TYPE); storeLocal(end); getStatic(Type.getType( "Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;")); //执行System.out newInstance(Type.getType( "Ljava/lang/StringBuilder;")); // 执行new StringBuilder分配内存 dup(); //dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V")); //调用StringBuilder的构造方法 visitLdcInsn("execute:");? invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;) Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法? ?? loadLocal(end); // 加载方法结束的时间 loadLocal(start); //加载方法开始的时间 math(SUB,Type.LONG_TYPE); //减法 invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法? ?? invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;")); // 调用StringBuilder的toString方法? invokeVirtual(Type.getType( "Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V")); // 调用StringBuilder的println方法 最终执行完插桩之后,就可以获得修改后的class数据。 ? 3.有选择性的插桩 现在存在一个问题,就是待插桩的class里所有的方法都被加入了插桩的代码。 插桩后生成的的InjectTest.class如下: public class?InjectTest { ? ? public InjectTest() { ? ? ? ? long var1 = System.currentTimeMillis(); ? ? ? ? long var3 = System.currentTimeMillis(); ? ? ? ? System.out.println("execute:" + (var3 - var1) + "ms."; ? ? } ? ? public static void main(String[] var0) throws InterruptedException { ? ? ? ??long var1 = System.currentTimeMillis(); ? ? ? ? Thread.sleep(1000L); ? ? ? ? long var3 = System.currentTimeMillis(); ? ? ? ? System.out.println("execute:" + (var3 - var1) + "ms."; ? ? } } 我们只想在main方法里插桩,而不想在构造方法里插桩,这时候可以使用注解。 ①创建注解 新建ASMTest类: ASMTest.java: @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface ASMTest{ } ②在需要插桩的方法上面添加注解 InjectTest.java: public class?InjectTest { ? ? @ASMTest ? ? public static void main(String[] var0) throws InterruptedException { ? ? ? ? Thread.sleep(1000L); ? ? } ? ? public void aa() { //新增一个方法,没有添加注解,因此不会执行插桩代码 ? ? } } ? ? ? |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 3:47:52- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |