什么是64K限制?
Dalvik 规范将可在单个 DEX 文件内可引用的方法总数限制在 65536。
其中包括 Android 框架方法、内容库方法以及您自己代码中的方法。如果Android应用中的方法数达到64K的上限,打包过程中就会出现错误。
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536或者
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using –multi-dex option.为了解决64K限制的问题,所以有了以下方法
对于Android 5.0及更高的版本
使用ART运行时,原生支持从Apk文件加载多个 dex 文件
对于Android 5.0之前的版本
因为使用的是Dalvik运行时,限制Apk只能使用单个classes.dex文件
所以可以使用Dalvik可执行文件(Dex)分包支持库com.android.support:multidex:1.0.0
这个支持库MultiDex可能存在一些局限性
MultiDex实现原理
首先Dex分包必须使用到MultilDex支持库,否则只会生成一个主Dex(classes.dex)
如果Android sdk 版本支持分包(> 5.0),则MultilDex支持库将被禁用MultiDex拆分步骤
- 自动扫描整个工程代码得到 main-dex-list(即需要写入主dex的文件清单)
- 根据main-dex-list对整个工程编译后的所有class文件按照需要写入主或辅dex进行拆分
- 使用dx工具将主、辅dex的class文件分别打包成 .dex 文件(这是Apk打包流程的第四步)
如何自动生成main-dex-list?
Android SDK 从 build tools 21 开始提供了 mainDexClasses 脚本
该脚本就是在打包的时候用作与生成主、辅dex文件。mainDexClasses脚本执行步骤
调用 Proguard 的 shrink功能,生成一个临时的jar包 temp.jar
shrink功能是Proguard根据keep规则保留需要的类和类成员,并丢弃不需要Keep的类和类成员。
这些被保留的类生成了临时jar包,并且都是需要放在主dex文件中的入口类。入口类(即rules中的Instrumentation、application、Activity、Annotation等等)。
将生成的临时jar包和输入的文件集合(编译后的目录)作为参数,调用com.android.multidex.MainDexListBuilder 来生成 main-dex-list(主dex文件清单)
MultiDex加载
启动应用时,系统只加载了主dex(classes.dex),MultiDex需要我们在应用启动之后进行动态加载安装。
这个动态加载的jar包是 android-support-multidex.jar
在Android sdk 5.0之前,需要手动调用MultiDex支持库的方法
MultiDex.install(this);
如:
public class HelloMultiDexApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } }
当然MultiDex支持库提供了MutiDexApplication这个API,可以直接继承。
在Android sdk 5.0及之后的版本,原生支持MultiDex,并且build tools版本> 21.1
动态加载的流程图:
其主要是
从APK中提取所有的辅dex(classes2.dex,classes3.dex,…),
依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip…等zip文件的classes.dex中然后通过反射调用makeDexElements()方法这些zip文件封装为Elements数组
- 最后将Elements合并到BaseDexClassloader的DexPathList实例pathList的dexElements数组。
MultiDex.install(this);源码解析,见这里。
Dex文件结构解析
Dex文件,即Dalvik Executable format file(Dalvik可执行格式文件)
Dex文件中包含了应用程序中运行时数据(一个工程需要的类、方法、字段等信息)
Dex文件结构图
通过这张图,可以知道Dex总体分为三部分:
Header Section
Table Section
Data SectionHeader Section
上面这张图表是Header Section所有的内容,大致分为三个部分:
文件校验相关 (magic,checksum和signature)
这部分数据可以用来检查这个dex文件是否损坏。
文件内容相关(file_size,header_size和endian_tag)
其中endian_tag(字节序)就是大小端。
map_offset字段
这个字段表示MapList数据在Data Section中的偏移量。
MapList即Data Section中的Map Section,包含了Dex文件中可能出现的所有类型,用于校验。size 和 offset
这部分内容可以拿到Table Section中的各项数据。
Table Section
Table Section包含了一个dex文件中所有的数据(字符串、类、字段、方法等等)
这部分内容又共分为6个Table
String 、 Type 、Proto 、 Field 、 Method在Header Section中的size&offset部分正好对应这个6个Table。
所以Header Section中的 size&offset 以及其所占的长度可以精确定位每一个Table在Dex文件中的位置,大致过程如下:
BufferByte buffer = file2buffer(dexFile); buffer.position(table_offset); ByteBuffer b = buffer.slice(); b.limit(table_size * item_length);
当定位到了一个Table,那么每一个Table的具体结构又是什么样呢?
Reading the fucking source code..
路径:dalvik/libdex/DexFile.h这个头文件中定义了一系列的struct,其一一对应Dex文件结构图中的内容。
其中对应Header Section的struct DexHeader
对应Table Section部分struct,分别一一对应6个Table
每个struct / Table的内容释义:
struct DexStringId / String Table
struct DexStringId 对应Header Section的string_ids_size/off。 其中内部有一个4字节的stringDataOff,指向Dex文件结构图的Data Section。 通过这个stringDataOff,我们就可以定位到该dex文件中所有的字符串数据, 包括类名,方法名,输入输出的字符串等等。 String Table: 一个stringDataOff表示StringData在Data Section中的偏移量, StringData中包含了一个dex文件中的所有字符串资源,比如类名,方法名等等。 这个table也将被后面几个table反复的用到。
struct DexTypeId / Type Table
struct DexTypeId: 对应Header Section的type_ids_size/off。 其中内部有一个4字节的descriptor_idx,是string_ids数组中的一个索引, 通过该索引我们就可以得到一个用来描述类型的字符串,包括方法的返回类型,方法的参数类型等等。 Type Table: 一个descriptor_idx,用来索引String Table,得到一个字符串资源用来表示一个类型, 比如方法的返回类型,类类型等等。
struct DexFieldId / Field Table
struct DexFieldId: 对应Header Section中的field_ids_size/off。 其中内部是 一个2字节(ushort类型)的classIdx,用来表示该field所属的类,它是type_ids数组中的一个索引; 一个2字节(ushort类型)的typeIdx,用来表示该field的类型,它是type_ids数组中的一个索引; 一个4字节的nameIdx,用来表示该field的名字,它是string_ids数组中的一个索引。 Field Table: 一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field所属的类; 一个typeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该field的类型; 一个nameIdx用来索引String Table,得到一个字符串资源用来表示该field的名称。
struct DexMethodId / Method Table
struct DexMethodId: 对应Header Section的method_ids_size/off。 其中内部是 一个2字节(ushort类型)的classIdx,和DexTypeId一样用来表示该field所属的类,它是type_ids数组中的一个索引; 一个2字节(ushort类型)的protoIdx,用来表示该method的原型(返回类型,入参类型),它是proto_ids数组中的一个索引; 一个4字节的nameIdx,用来表示该method的名字,它是string_ids数组中的一个索引。 Method Table: 一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method所属的类; 一个protoIdx用来索引Proto Table,进而得到一个Proto Item,其中包含了一个method的原型,返回类型和入参类型; 一个nameIdx用来索引String Table,得到一个字符串资源用来表示该method的名称。
struct DexProtoId / Proto Table
struct DexProtoId 对应Header Section的proto_ids_size/off。 其中内部是 一个4字节的shortyIdx,用来表示一个method的原型,它是string_ids数组中的一个索引; 一个4字节的returnTypeIdx,用来表示一个method的返回类型,它是type_ids数组中的一个索引; 一个4字节的parametersOff,用来表示TypeList在Data Section中的偏移量,TypeList是一个method的参数列表。 Proto Table: 一个shortyIdx用来索引String Table,得到一个字符串资源用来表示该method的原型; 一个returnTypeIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该method的返回类型; 一个parametersOff用来表示TypeList(Type Item的集合)在Data Section中的偏移量,TypeList中包含了一个方法的所有入参类型。
struct DexClassDef / ClassDefs Table
struct DexClassDef: 对应Header Section的class_defs_size/off。 其中是 内部一个4字节的classIdx,用来表示一个类,它是type_ids数组的一个索引; 一个4字节的accessFlags,用来表示该类的权限,如public,private等等,用一个int值表示(就是该4字节); 一个4字节的superclassIdx,用来表示该类的父类(类名),它是type_ids数组中的一个索引; 一个4字节的interfacesOff,用来表示该类所实现的接口,它用来表示TypeList在Data Section中的偏移量(和DexProtoId中的parametersOff类似); 一个4字节的sourceFileIdx,用来表示该类的文件名(xxxx.java),它用来表示string_ids数组中的一个索引; 一个4字节的annotationsOff,用来表示该类的所有注解,它是annotations_directory_item在Data Section中的偏移量; 一个4字节的classDataOff,它表示该类的所有有关数据(方法,字段等等),它用来表示class_data_item在Data Section中的偏移量; 一个4字节的staticValuesOff,用来表示该类中的静态数据名,它用来表示DexEncodedArray在Data Section中的偏移量。 需要注意的是:annotationsOff,classDataOff两个字段所指向的数据并不是简单的字符串资源,而是一个结构体 struct, 其中annotations_directory_item中包含一个类中所有的类注解数据,方法注解数据和字段注解数据; 而class_data_item中则包含一个类中的类数据,方法数据和字段数据。有兴趣的同学可以自行研究下去。 ClassDefs Table: 一个classIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该类; 一个accessFlags用来表示该类的访问权限,如public,private等; 一个superclassIdx用来索引Type Table,进而得到一个Type Item来索引String Table,得到一个字符串资源用来表示该类的父类; 一个interfacesOff用来表示TypeList(Type Item的集合)在Data Section中的偏移量,TypeList中包含了一个类所实现的所有接口; 一个sourceFileIdx用来索引String Table,得到一个字符串资源用来表示该类的文件名; 一个annotationsOff用来表示annotations_directory_item在Data Section中的偏移量,annotations_directory_item中包含了该类所有的注解; 一个classDataOff用来表示class_data_item在Data Section中的偏移量,class_data_item用来表示该类的所有类数据; 一个staticValuesOff用来表示DexEncodedArray在Data Section中的偏移量,DexEncodedArray用来表示该类的所有静态数据。
总结
Table Section中各个Table之间的关系
String Table中的item可以获取到StringData在Data Section中的偏移量,从而获取String Data。 而其他table中的item的结构中的值分成两类: 第一类直接或者间接的索引String Table从而获取String Data; 第二类直接获取到Data Section中的偏移量,从而获取Data。
总结
首先Header Section中的size和off可以用来得到Table Section中每一个Table的位置。 然后Table Section中每一个table都有它自己的内部结构,通过id索引的方法,获取到Data Section中的数据。 这样一个dex文件中的三个Section就有机的结合在了一起。
那么,Android虚拟机在查找class,method和field的过程中,充分的利用了dex文件的结构。
例如:查找一个类,查找一个类是通过ClassLinker的resolveType方法来做的
const char* descriptor = dex_file.StringByTypeIdx(type_idx);
通过 type_ idx 去获取这个类的描述(descriptor_idx):
const char* StringByTypeIdx(uint32_t idx) const { const TypeId& type_id = GetTypeId(idx); return StringDataByIdx(type_id.descriptor_idx_); } const TypeId& GetTypeId(uint32_t idx) const { DCHECK_LT(idx, NumTypeIds()) << GetLocation(); return type_ids_[idx]; } const char* StringDataByIdx(uint32_t idx) const { uint32_t unicode_length; return StringDataAndUtf16LengthByIdx(idx, &unicode_length); } const char* StringDataAndUtf16LengthByIdx(uint32_t idx, uint32_t* utf16_length) const { if (idx == kDexNoIndex) { *utf16_length = 0; return nullptr; } const StringId& string_id = GetStringId(idx); return GetStringDataAndUtf16Length(string_id, utf16_length); } const StringId& GetStringId(uint32_t idx) const { DCHECK_LT(idx, NumStringIds()) << GetLocation(); return string_ids_[idx]; }
可以看到就是通过type_ idx去索引type_ ids数组得到Type_ Item,
然后用descriptor_ idx索引string_ ids得到类型的字符串描述。- 利用Dex文件,我们可以做的事
DexDiff,即2个Dex文件的差异计算。
比如微信的热修复框架Tinker,就用到了dexdiff算法。微信的DexDiff算法,就是利用Dex文件的结构,对每一个细分的Section(不是三大Section)进行差量计算,得到一个diff文件。
Dex文件加密和解密,APK安全问题。
技术文章
Android MultiDex实现原理以及Dex文件结构
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/weixiao1999/article/details/54579634
猜你喜欢
转载自blog.csdn.net/weixiao1999/article/details/54579634
今日推荐
周排行