我们都知道,Android 是用 Java 开发的,那么为什么 Android 不直接用 Java 的 JVM 虚拟机呢?
这是因为 JVM 把 Java 编译成的 class 字节码里面具有很多冗余信息,而且每次读取字节码之前都要进行 class 文件的读取,这样就会多次读写,而使用 Android 虚拟机则会把所有 class 文件打包成一个 dex 文件,这个 dex 文件就是压缩后去除冗余信息之后的字节码,Android 读取字节码只要读取整个 dex 文件即可,这样加速了运行速度。而且 JVM 是基于栈的虚拟机,而 Android 是基于寄存器的虚拟机上面提到栈和寄存器虚拟机,那么它俩有什么区别呢?
基于栈虚拟机读取指令是在内存上工作的,而寄存器读取指令则是在高速缓存区(寄存器)上工作,CPU 直接操作,执行更快。栈虚拟机更不依赖于硬件,而寄存器虚拟机会根据硬件来进行优化,所以不适合多平台移植。并且如果做同一个操作,栈的指令比寄存器的指令更多。总的来说就是以空间换时间知道 Android 为什么要使用自己的虚拟机后,那么为什么 Android 会在 4.4 版本之后抛弃 Dalvik 虚拟机,而反而使用 ART 虚拟机了呢,它两又有什么区别呢?
Dalvik 虚拟机的原理就是每次运行 app 的时候都需要通过 JIT 编译器将 dex 编译为 odex 文件,这样就会导致每次运行应用都很慢, 所以在 4.4 之后 Android 引入了 ART 虚拟机,它是在安装应用的时候或者重启手机的时候通过 dex2oat 把应用的 dex 先编译成 elf 文件(oat),等执行的时候就直接运行了,优化了 app 的运行速度。不过这样就会导致 app 安装或重启的时候很慢,所以 Android 7.0 之后安装或重启手机的时候又恢复了原先的 JIT 运行机制,不过这次会比 4.4 版本之前的多一个 profile 文件,用于记录 JIT 编译后的缓存信息,把 dex2oat 的时机放在了第一次运行 app 的时候或者手机空闲的时候读取安装应用进行 dex2oat 编译。下面来个例子
Hello.java
public class Hello{ public void main(String[] args){ add(); } public int add(){ return 1 + 2; }}
通过 javac Hello.java 之后转换为 Hello.class ,接下来通过 dx 工具转换为 dex 文件
dx --dex --output Hello.dex Hello.class
其中 Hello.dex 可以随便命名。最终可以得到 Hello.dex 文件,接着生成 odex 文件,odex 文件需要手机上面生成,所以需要先电脑连接手机
adb push Hello.dex /data/local/tmp
dex2oat --dex-file=/data/local/tmp/Hello.dex --oat-file=/data/local/tmp/Hello.odex --instruction-set=arm64 --app-image-file=/system/framework/oat/arm64/Hello.art --runtime-arg -Xms64m --runtime-arg -Xmx128m
--dex-file 指定要编译的dex文件
--oat-file 指定要输出的odex文件
--instruction-set 指定cpu架构
--app-image-file 指定odex相对应的art文件
--runtime-arg 指定dex2oat运行时的参数,如果编译时发生内存不足,可以把Xms和Xmx调大
生成 oat 文件命令
dex2oat --dex-file=/data/local/tmp/Hello.dex --runtime-arg -Xms64m --runtime-arg -Xmx64m --oat-file=/data/local/tmp/Hello.oat
oat 和 odex 都是给虚拟机执行的文件,而我们看的一般都是 dex 文件。通过 010 Editor 可以查看到 Hello.dex 文件结构为
如果要查看 class 字节码,则是通过 javap 命令
javap -v Hello.class
Classfile /D:/Hello.class Last modified 2020-11-4; size 326 bytes MD5 checksum fcfe915ce13b7c5e91106e2d57bf8629 Compiled from "Hello.java"public class Hello minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Methodref #4.#15 // java/lang/Object."":()V #2 = Methodref #3.#16 // Hello.add:()I #3 = Class #17 // Hello #4 = Class #18 // java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 main #10 = Utf8 ([Ljava/lang/String;)V #11 = Utf8 add #12 = Utf8 ()I #13 = Utf8 SourceFile #14 = Utf8 Hello.java #15 = NameAndType #5:#6 // "":()V #16 = NameAndType #11:#12 // add:()I #17 = Utf8 Hello #18 = Utf8 java/lang/Object{ public Hello(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_0 1: invokevirtual #2 // Method add:()I 4: pop 5: return LineNumberTable: line 3: 0 line 4: 5 public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: iconst_3 1: ireturn LineNumberTable: line 6: 0}SourceFile: "Hello.java"
字节码学习
Constant pool 常量池
它包括字面量(字符串,final 常量)和符号引用(类和接口的名称,字段的名称和描述符号,方法的名称和描述符)
#1 = Methodref #4.#15 // java/lang/Object."":()V
Methodref 表示方法的申明,#4,#15 指的是常量的位置,结果就是
// java/lang/Object."":()V
对应的是 Object 的 init 方法,最后 V 的意思是方法的返回值类型为 Void。剩下的其他常量也类似的申明。
stack
最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1
locals
局部变量所需的存储空间,单位为Slot, Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小。
args_size
方法参数的个数,这里是1 指的是实例方法的隐藏参数 this
LineNumberTable
该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。
方法调用命令
invokevirtual 调用实例方法,指的是虚方法,如上面的 add 方法
invokestatic 调用类方法,指的是 static 方法
invokeinterface 调用接口方法,运行时搜索合适方法调用
invokespecial 调用调用特殊实例方法,包括实例初始化方法、父类方法
invokedynamic 由用户引导方法决定,运行时动态解析出调用点限定符所引用方法
了解完 class 的相关知识,接下来应该就是了解 AspectJ 和 ASM 框架是如何修改 class 字节码的,具体如何操作,将在下一篇学习?