这里也可以对 Hello.class 文件执行 javap 命令,进行反汇编。
javap -c Hello
执行结果如下:
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.“”😦)V
4: aload_0
5: ldc #2 // String hello! youzan
7: putfield #3 // Field helloString:Ljava/lang/String;
10: return
public static void main(java.lang.String[]);
Code:
0: new #4 // class Hello
3: dup
4: invokespecial #5 // Method “”😦)V
7: astore_1
8: aload_1
9: aload_1
10: getfield #3 // Field helloString:Ljava/lang/String;
13: invokevirtual #6 // Method fun:(Ljava/lang/String;)V
16: return
public void fun(java.lang.String);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
}
其中 Code 之后都是具体的指令,供 JVM 虚拟机执行。指令的具体含义可以参考 JAVA 官方文档。
从 .class 到 .dex
上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。
dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的SDK根目录/build-tools/任意版本 里面。使用 dx 工具处理上面生成的Hello.class 文件,在 Hello.class 的目录下使用下面的命令:
dx --dex --output=Hello.dex Hello.class
执行完成后,会在当前目录下生成一个 Hello.dex 文件。这个 .dex 文件就可以直接在 Android 运行时环境执行,一般可以通过 PathClassLoader 去加载 dex 文件。现在在当前目录下执行 dexdump 命名来反编译:
dexdump -d Hello.dex
执行结果如下(部分区域的含义已经在下面描述):
Processing ‘Hello.dex’…
Opened ‘Hello.dex’, DEX version ‘035’
------ 这里是编写的 Hello.java 的类的信息 ------
Class #0 -
Class descriptor : ‘LHello;’
Access flags : 0x0001 (PUBLIC)
Superclass : ‘Ljava/lang/Object;’
Interfaces -
Static fields -
Instance fields -
#0 : (in LHello;)
name : ‘helloString’
type : ‘Ljava/lang/String;’
access : 0x0002 (PRIVATE)
------ 下面区域描述的是构造方法的信息。7010 0400 0100 1a00 0b00 之类的数字就是方法中的代码翻译成的指令。Dalvik 使用的是16位代码单元,所以这里就是4个数字为一组,每个数字是16进制。invoke-direct 这些是前面指令对应的助记符,也代表着这些指令的真正操作。如果对这些指令转化感兴趣可以去https://source.android.com/devices/tech/dalvik/instruction-formats 查看 ------
Direct methods -
#0 : (in LHello;)
name : ‘’ — 方法名称:这个很明显就是构造方法 —
type : ‘()V’ — 方法原型,()里面表示入参,()后面表示返回值,V代表void—
access : 0x10001 (PUBLIC CONSTRUCTOR) — 方法访问类型 —
code -
registers : 2 — 方法使用的寄存器数量 —
ins : 1 — 方法入参,方法除了我们定义的参数以外,系统还会默认带一个特殊参数 —
outs : 1
insns size : 8 16-bit code units — 指令大小 —
000148: |[000148] Hello.😦)V
000158: 7010 0400 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.😦)V // method@0004
00015e: 1a00 0b00 |0003: const-string v0, “hello! youzan” // string@000b
000162: 5b10 0000 |0005: iput-object v0, v1, LHello;.helloString:Ljava/lang/String; // field@0000
000166: 0e00 |0007: return-void
catches : (none)
positions :
0x0000 line=1
0x0003 line=2
locals :
0x0000 - 0x0008 reg=1 this LHello;
#1 : (in LHello;)
name : ‘main’
type : ‘([Ljava/lang/String;)V’
access : 0x0009 (PUBLIC STATIC)
code -
registers : 3
ins : 1
outs : 2
insns size : 11 16-bit code units
000168: |[000168] Hello.main:([Ljava/lang/String;)V
000178: 2200 0000 |0000: new-instance v0, LHello; // type@0000
00017c: 7010 0000 0000 |0002: invoke-direct {v0}, LHello;.😦)V // method@0000
000182: 5401 0000 |0005: iget-object v1, v0, LHello;.helloString:Ljava/lang/String; // field@0000
000186: 6e20 0100 1000 |0007: invoke-virtual {v0, v1}, LHello;.fun:(Ljava/lang/String;)V // method@0001
00018c: 0e00 |000a: return-void
catches : (none)
positions :
0x0000 line=5
0x0005 line=6
0x000a line=7
locals :
Virtual methods -
#0 : (in LHello;)
name : ‘fun’
type : ‘(Ljava/lang/String;)V’
access : 0x0001 (PUBLIC)
code -
registers : 3
ins : 2
outs : 2
insns size : 6 16-bit code units
000190: |[000190] Hello.fun:(Ljava/lang/String;)V
0001a0: 6200 0100 |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0001
0001a4: 6e20 0300 2000 |0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0003
0001aa: 0e00 |0005: return-void
catches : (none)
positions :
0x0000 line=10
0x0005 line=11
locals :
0x0000 - 0x0006 reg=1 this LHello;
source_file_idx : 1 (Hello.java)
到此为止,已经完成了将 Java 代码转变成 Dalvik 可执行的文件,即 dex。
Dex 文件的具体格式
现在来分析一下 Dex 文件的具体格式,就像 MP3,MP4,JPG,PNG 文件一样,Dex 文件也有它自己的格式,只有遵守了这些格式,才能被 Android 运行时环境正确识别。
Dex 文件整体布局如下图所示:
这些区域的数据互相关联,互相引用。由于篇幅原因,这里只是显示部分区域的关联,完整的请去官网自行查看相关数据整理。下图中的各字段都在后面的各区域的详细介绍中有具体介绍。
下面将分别对文件头、索引区、类定义区域进行简单的介绍。其它区域可以去 Android 官网了解。
文件头
文件头区域决定了该怎样来读取这个文件。具体的格式如下表(在文件中排列的顺序就是下面表格中的顺序):
id 区
id 区存储着字符串,type,prototype,field, method 资源的真正数据在文件中的偏移量,我们可以根据 id 区的偏移量去找到该 id 对应的真实数据。
字符串 id 区域
这个区块是一个偏移量列表,每个偏移量对应了一个真正的字符串资源,每个偏移量占32位。我们可以通过偏移量找到对应的实际字符串数据。具体格式如下:
最终这个偏移的位置应该是落在数据区的。找到这个偏移量的位置后,根据下面的格式就可以读取出这个字符串资源的具体数据:
类型 id 区
这个区块是一个索引列表,索引的值对应字符串id区域偏移量列表中的某一项。数据格式如下:
如果我们要找到某个类型的值,需要先根据类型id列表中的索引值去字符串id列表中找到对应的项,这一项存储的偏移量对应的字符串资源就是这个类型的字符串描述。
方法原型 id 区
这个区块是一个方法原型 id 列表,数据格式为:
成员 id 区
这个区块存储着原型 id 列表,数据格式为:
方法 id 区
这个区块存储着方法 id 列表,数据格式为: 这个区块存储着原型 id 列表,数据格式为:
类定义区
这个区域存储的是类定义的列表,具体的数据结构如下:
解析 dex 文件的工具
这里推荐一个可以解析 dex 文件的工具 010 Editor。它可以通过预置的模板让我们更清晰的了解 dex 文件的格式。
Dex 文件在 Android Tinker 热修复中的应用
在目前的主流的 Android 热修复方案中,Tinker有免费、开源、用户量大等优点,因此在有赞也是基于 Tinker 搭建 Android 热修复服务。Tinker 热修复的主要原理就是通过对比旧 APK 的 dex 文件与新 APK 的 dex 文件,生成补丁包,然后在 APP 中通过补丁包与旧 APK 的 dex 文件合成新的 dex 文件。流程如下图所示:
注:图片来源于 Tinker 官网
补丁包的生成
Tinker 官方使用自研一套合成方案,就是 DexDiff。它基于 Dex文件格式的特性,具有补丁包小,消耗内存小等优点。在 DexDiff 算法中,会根据 Dex文件的格式,将 Dex 文件划分为不同的区块类,如下图:
这些区块有一个统一的数据结构,主要的数据有区块对应的实际数据类型及在文件中的偏移量。如下图:
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。
【Android思维脑图(技能树)】
知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。
【Android高级架构视频学习资源】
Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!