IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: 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文件,修改或者增强原有代码逻辑的操作。

编写好的代码经过编译后如下:

67649b728c5a411b94b084c231547789.png

?经过字节码插桩后如下:

349110f71c7f448c94ff04febc42766d.png

比如需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。

ASM框架就是操作java字节码的框架,按照class文件的格式,解析、修改、生成class,可以动态生成类或者增强现有类的功能。热修复、systrace都使用了字节码插桩。

我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。

10e89025cfb1483ea20cc779abefe636.jpg

?字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。

?

2.ASM的使用

①引入ASM依赖:

6669b21ba51d4879865095de3abe2be1.png

?使用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查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

093831f1632a4e4eb1ba93b5df2b3b8e.jpg

?安装完成之后,可以在需要插桩的类源码中点击右键:

d133b44836a14bc892836db7a15340c1.png

?点击ASM Bytecode Viewer之后会弹出

cebbad0afcd44ea3ad75f96ab46bc146.jpg

?所以第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中的格式如下:

afb93642df6649419ac5d3c5c0ef5e88.jpg

?对应的代码如下:

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
上一篇文章      下一篇文章      查看所有文章
加:2022-07-05 23:36:33  更:2022-07-05 23:37:56 
 
开发: 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-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码