dex学习和探究
一. Dex是什么
Dex (Dalvik Executable Format)是java中存储类定义和相关联数据的一种数据格式,主要用于保存类定义及其关联的辅助数据
.dex结构分成三部分:
文件头:表明了是dex文件,已经文件的大小等等数据
索引头:如下图所示
数据区:数据区,就像是jvm中的堆保存方法+变量。
下面是dex的数据格式:
odex简介
odex是OptimizedDEX的缩写,表示对dex的优化。系统预置应用经常会采用odex的方式加载,一般存放在/data/dalvik-cache目录下。由于Android程序的apk文件为zip压缩包格式,Dalvik虚拟机每次加载它们时需要从apk中读取classes.dex文件,这样会耗费很多cpu时间,而采用odex方式优化的dex文件,已经包含了加载dex必须的依赖库文件列表,Dalvik虚拟机只需检测并加载所需的依赖库即可执行相应的dex文件,这大大缩短了读取dex文件所需的时间。
不过,这个优化过程会根据不同设备上Dalvik虚拟机的版本、Framework库的不同等因素而不同。在一台设备上被优化过的ODEX文件,拷贝到另一台设备上不一定能够运行。
二. Dex的加载流程
Android提供了一个专门验证与优化dex文件的工具dexopt。其源码位于Android系统源码的dalvik/dexopt目录下,Dalvik虚拟机在加载一个dex文件时,通过指定的验证与优化选项来调用dexopt进行相应的验证与优化操作。具体加载流程参考
简要总结下整个的加载流程,首先是对文件名的修正,后缀名置为”.dex”作为输出文件,然后生个一个DexPathList对象函数直接返回一个DexPathList对象,
在DexPathList的构造函数中调用makeDexElements()函数,在makeDexElement()函数中调用loadDexFile()开始对.dex或者是.jar .zip .apk文件进行处理,跟入loadDexFile()函数中,会发现里面做的工作很简单,调用optimizedPathFor()函数对optimizedDiretcory路径进行修正。
之后才真正通过DexFile.loadDex()开始加载文件中的数据,其中的加载也只是返回一个DexFile对象。
在DexFile类的构造函数中,重点便放在了其调用的openDexFile()函数,在openDexFile()中调用了openDexFileNative()真正进入native层,在openDexFileNative()的真正实现中,对于后缀名为.dex的文件或者其他文件(.jar .apk .zip)分开进行处理:
.dex文件调用dvmRawDexFileOpen();
其他文件调用dvmJarFileOpen()。
在dvmRawDexFileOpen()函数中,检验dex文件的标志,检验odex文件的缓存名称,之后将dex文件拷贝到odex文件中,并对odex进行优化。
调用dvmDexFileOpenFromFd()对优化后的odex文件进行映射,通过mprotect置为”只读”属性并将映射的内存结构保存在DvmDex*结构中。
dvmJarFileOpen()先对文件进行映射,结构保存在ZipArchive中,然后再尝试以文件名作为dex文件名来“打开”文件,如果失败,则调用dexZipFindEntry在ZipArchive的名称hash表中找名为”class.dex”的文件,然后创建odex文件,下面就和dvmRawDexFileOpen()一样了,就是对dex文件进行优化和映射。
最关键的loadDexFile和optimizedPathFor方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
}
//生成odex的目录
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
optimizedPathFor 方法:就是将dex 优化形成optimizedDex文件。然后
通过DexFile的静态方法将.dex文件。或者apk,.jar加载成DexFile文件。DexFile 是C语言中的结构体,里面保存的都是一些指针。经过不断的校检,最后通过dvm里面的c代码处理,得到DexFile。
三. Dex的应用场景
结合dex的支持动态加载的特性,dex通常被用于以下场合:
MultiDex分包: 拆分dex,可以解决方法数超过65535导致的编译问题。也支持分包后再Application的onCreate()方法中调用 MultiDex.install(this)来加载不同的dex包。
可以用于解决2.3上无法安装问题
INSTALL_FAILED_DEXOPT可以解决方法数过多问题:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536应用热更新: 参考腾讯的tinker方案。tinker的核心思想是利用DexDiff算法对比差异生成Patch补丁包,全量合成新Dex进行整体替换。这里面就涉及到dex的具体加载过程中,很重要的一个特点,在DexPathList加载类时,会遍历dexElements数组,ClassLoader在加载到正确的类之后就会停止加载此类,基于这个原理,可以用来做应用的局部热更新。tinker是在这个基础上增加了差量包的定义,引入了自定义的 dexdiff算法。
插件化:动态加载模块,参见于广点通sdk方案。将包含dex的jar放到asset目录下,动态加载函数类,而不用安装独立的应用。该方案可以用于广告sdk,应用插件等开发,采用动态加载dex,通常会将dex文件下载放置于data目录而不是sd卡目录(sd卡不安全),并且采用加密算法进行dex的内部加密,支持动态下载更新dex文件,提升了代码破解难度,增加了apk的运维可能。
- 应用加固和脱壳: 这方面没有过多涉猎,有空可以学习下mergerly整理的加固脱壳方法
四. Dex方案的优劣
可能带来的问题:
MultiDex分包可能增加进入耗时,dex文件不能过大,否则容易在进入加载dex时出现anr或异常。
可能引入一些加载异常:
ClassNotFoundException: Didn’t find class “com.squareup.picasso.Picasso” on path: DexPathList[[zip file “/data/app/com.groupon.dexlazyload-1/base.apk”]
增加了代码注入和应用劫持的风险
优点:
支持热更新和热修复,避免整体apk更新带来的流量问题和迭代问题。
支持dex加固,一定程度上增加了apk被反编译破解带来的风险。
允许动态加载指定的dex内的函数。
五. tqy dex加载分析
tqy sdk的初始化中会调用initSdk,
校验文件:
加载dex
具体的dex文件被放置到: /data/user/0/com.tqy.sdk.ad/ 目录上:
在通过dex文件的md5校验,之后加载类实例,覆盖原有的register方法,jar包中清晰可见在原有的基础上增加了adServiceLogic的实现。
也意味着原有的方法只提供了基础的方法名称,在dex中才是方法的真正实现。