常见的Android文件结构 - class.dex
class.dex文件
class.dex 是 Android 应用的中间字节码文件,包含所有 Java/Kotlin 代码经编译后的指令集合,供 Android Runtime(ART/Dalvik)执行
DEX的目录结构
- 主 DEX 文件:classes.dex(默认)。
- MultiDEX 生成:classes2.dex,classes3.dex 等。
- 原生库和其他文件路径
├── classes.dex ├── classes2.dex ├── lib/ # 原生库(armeabi-v7a、arm64-v8a 等) └── assets/ # 原始资源(未编译)
DEX文件结构解析
通过010editor编辑器打开class.dex文件,可以查看其结构
DEX 文件由 多个结构化的 Section(区段) 组成,各 Section 通过头部(Header)定义的偏移量进行定位
通过代码格式理解其文件格式
struct DexFile {
DexHeader header; // 文件头
DexStringId string_ids[]; // 字符串索引表
DexTypeId type_ids[]; // 类型索引表
DexProtoId proto_ids[]; // 方法原型索引表
DexFieldId field_ids[]; // 字段索引表
DexMethodId method_ids[]; // 方法索引表
DexClassDef class_defs[]; // 类定义表
DexData data; // 数据区(字节码、注解等)
DexLink link_data; // 静态链接数据(可选)
};
各 Section 的详细结构
文件头(DexHeader)
头部位于 DEX 文件起始位置,定义了所有 Section 的偏移量和数据规模,固定大小为 0x70 字节
- 关键字段示例:
- string_ids_size/string_ids_off:字符串表的数量和偏移。
- class_defs_size/class_defs_off:类定义表的数量和偏移
struct DexHeader {
u1 magic[8]; // Magic Number("dex\n", 后跟版本标识,如 "035\0")
u4 checksum; // 文件的 Adler-32 校验和(不含 magic 和 checksum 本身)
u1 signature[20]; // SHA-1 哈希,用于防篡改
u4 file_size; // 整个文件的大小(字节)
u4 header_size; // 头部大小(固定 0x70)
u4 endian_tag; // 字节序标记(0x12345678 小端)
u4 link_size; // link_data 区段的大小(静态链接使用)
u4 link_off; // link_data 区段的偏移量
u4 map_off; // map_list 的偏移量(DexMapList 结构,描述各 Section 的位置)
// ...其他字段定义各 Section 的偏移和数量
};
字符串索引表(DexStringId)
- 存储所有字符串在数据区的偏移量(去重处理)
struct DexStringId {
u4 string_data_off; // 字符串数据的偏移量(指向 DexStringItem)
};
- 字符串数据格式(DexStringItem):
- 字符串长度(LEB128 编码)+ UTF-8 数据(无终止符)
struct DexStringItem {
uleb128 utf16_size; // UTF-16 字符数(并非字节长度!)
u1 data[]; // UTF-8 字节流
};
类型索引表(DexTypeId)
- 存储所有 类型描述符字符串的索引(指向字符串表)
struct DexTypeId {
u4 descriptor_idx; // 对应字符串表中的索引(如 "Ljava/lang/String;")
};
方法原型索引表(DexProtoId)
- 定义方法原型(包含返回类型和参数类型列表)
struct DexProtoId {
u4 shorty_idx; // 短描述符索引(如 "VI" 表示 void func(int))
u4 return_type_idx; // 返回值类型索引(对应 DexTypeId)
u4 parameters_off; // 参数类型列表偏移(指向 DexTypeList)
};
- 参数列表(DexTypeList)
struct DexTypeList {
u4 size; // 参数数量
DexTypeItem list[size]; // 参数类型数组
};
struct DexTypeItem {
u2 type_idx; // 类型索引(DexTypeId)
};
字段/方法索引表(DexFieldId / DexMethodId)
-
定义字段和方法的元信息
-
DexFieldId 结构
struct DexFieldId { u2 class_idx; // 所属类索引(DexTypeId) u2 type_idx; // 字段类型索引(DexTypeId) u4 name_idx; // 字段名索引(DexStringId) };
-
DexMethodId 结构
struct DexMethodId { u2 class_idx; // 所属类索引(DexTypeId) u2 proto_idx; // 方法原型索引(DexProtoId) u4 name_idx; // 方法名索引(DexStringId) };
-
类定义表(DexClassDef)
- 描述类的完整元数据
struct DexClassDef { u4 class_idx; // 类类型索引(DexTypeId) u4 access_flags; // 访问标志(public, final, etc.) u4 superclass_idx; // 父类索引(DexTypeId) u4 interfaces_off; // 接口列表偏移(DexTypeList) u4 source_file_idx; // 源文件名索引(DexStringId,可选) u4 annotations_off; // 注解信息偏移(DexAnnotationsDirectoryItem,可选) u4 class_data_off; // 类数据偏移(DexClassData 结构) u4 static_values_off; // 静态字段初始值偏移(DexEncodedArray) };
类数据(DexClassData)
- 存储类的实际字段和方法数据,包含可变长度结构(LEB128 编码压缩)
struct DexClassData {
uleb128 static_fields_size; // 静态字段数量
uleb128 instance_fields_size; // 实例字段数量
uleb128 direct_methods_size; // 直接方法数量(static + private)
uleb128 virtual_methods_size; // 虚方法数量
DexField[static_fields_size]; // 静态字段数组
DexField[instance_fields_size]; // 实例字段数组
DexMethod[direct_methods_size]; // 直接方法数组
DexMethod[virtual_methods_size]; // 虚方法数组
};
// 字段定义(动态解析)
struct DexField {
uleb128 field_idx; // 字段索引(DexFieldId)
uleb128 access_flags; // 访问标志
};
// 方法定义(动态解析)
struct DexMethod {
uleb128 method_idx; // 方法索引(DexMethodId)
uleb128 access_flags; // 访问标志
uleb128 code_off; // 方法代码偏移(DexCode 结构)
};
方法代码(DexCode)
- 存储方法的字节码、寄存器信息、异常表等
struct DexCode { u2 registers_size; // 使用寄存器总数 u2 ins_size; // 输入参数占用的寄存器数 u2 outs_size; // 输出参数占用的寄存器数(用于调用其他方法) u2 tries_size; // try-catch 块数量 u4 debug_info_off; // 调试信息偏移(DexDebugInfoItem) u4 insns_size; // 指令数组的大小(以 2 字节为单位) u2 insns[insns_size]; // Dalvik 指令数组 // 可选的 try-catch 数据(DexTryItem)和 handlers 信息 };
关键数据结构特性
- (1) 索引化设计
- 所有字符串、类型、方法等均通过 索引引用(而非直接存储),极大减少冗余数据。
- 例如:方法名通过 name_idx 指向 DexStringId 列表中的一个条目。
- (2) LEB128 编码
- 在变长字段(如 uleb128 类型)中使用 LEB128 (Little-Endian Base 128) 压缩编码:
- 目的:优化空间效率,尤其是在类数据(DexClassData)中,字段/方法数量大但实际值较小。
- (3) 紧密排列
- 各 Section 的数据在文件内紧密排列,通过 偏移量快速跳转,避免了不必要的 IO 开销
DEX文件的验证与优化
- 验证过程
- 安装时验证(由 installd 触发):
- Magic Number 检查:确认文件头是否为有效 DEX 签名(64 65 78 0A → "dex\n")。
- Checksum/签名验证:比对 APK 签名中的 DEX 完整性。
- 结构校验:检查 DEX 偏移量是否正确,避免恶意篡改后的越界访问。
- Dalvik 字节码验证:
- 格式验证:方法指令是否符合规范。
- 类型验证:寄存器使用和类型匹配。
- 控制流验证:跳转指令是否合法。
- 优化过程
- 工具:dex2oat(取代旧版 dexopt)
- 模式:
- AOT(Ahead-of-Time):安装时将 DEX 编译为本地机器码(OAT 格式)。
- JIT(Just-in-Time):运行时动态优化高频代码
DEX文件的修改
-
反编译与修改工具
- Apktool:
- 解包 APK → 得到 classes.dex。
- 使用 d2j-dex2jar 转换为 JAR,或直接修改 smali 代码。
- Apktool:
-
baksmali/smali(汇编与反汇编):
baksmali classes.dex -o smali/ # DEX → smali 文本 smali smali/ -o modified_classes.dex # 重编译为 DEX
-
动态加载技术
// 使用 DexClassLoader 加载插件化 DEX DexClassLoader loader = new DexClassLoader( dexPath, optimizedDir, null, getClassLoader() ); Class<?> clazz = loader.loadClass("com.plugin.MyClass"); clazz.getMethod("invoke").invoke(null);
MultiDEX
- 问题背景
- 方法数限制:单个 DEX 文件最多支持 65536 (2^16) 个方法引用(源于 DEX 格式设计)。
- 触发条件:大型应用依赖库过多(如 Google Play Services、Facebook SDK 等)→ 生成多个 DEX 文件。
- 启用Multidex
<GRADLE> // build.gradle android { defaultConfig { multiDexEnabled true } } dependencies { implementation 'androidx.multidex:multidex:2.0.1' }