Android .dex文件简介

1.引言

上节我问过自己问题dvm,将Class转成成.dex文件,然后再将.dex文件转换成Class文件。那么为什么要这样做。这样做不是多此一举吗,为什么不直接用Class文件,这样做岂不是很方面吗?解决这些问题之前我首要的是要明白什么是.dex,内部结构,有什么用。接下来我将谈谈.dex。主要参考着几篇优秀的文章。Android中dex文件的加载与优化流程 。
分析的参考博客.dex浅析在此感谢他们的付出。

2.正题

2.1 .dex的结构

.dex结构分成三部分:
文件头:表明了是dex文件,已经文件的大小等等数据
索引头:如下图所示
数据区:数据区,就像是jvm中的堆保存方法+变量。(写在这对jvm的常量池,堆栈,寄存器不是很清楚,准备专门在写一篇文章记录下加强自己的记忆)

Android .dex文件简介_第1张图片
Paste_Image.png

2.2 odex文件

odex是OptimizedDEX的缩写,表示对dex的优化。写到这让我想起上一篇DexClassLoad的构造方法.里面需要传入一个OptimizedPath。这个OptimizedPath大概就是这个odex的路径。

2.3 DexFile文件

DexFile是dex文件被映射到内存中的结构,出了基本的dex文件结构外,还包含了DexOptHead和尾部附加的数据,这些数据是Android系统为了结合当前平台特性对dex文件的结构进行了优化和扩充,是运行效率更高。

Android .dex文件简介_第2张图片
Paste_Image.png

从上面的图可以看出。DexFile 保存了一个Class对象的属性+方法的索引。也就是说 我们可以根据一个DexFile 得到一个Class对象。

2.4 .dex的加载过程

**加载过程略微的繁琐,了解下过程就像。我直接复制过来的 **
Android提供了一个专门验证与优化dex文件的工具dexopt。其源码位于Android系统源码的dalvik/dexopt目录下,Dalvik虚拟机在加载一个dex文件时,通过指定的验证与优化选项来调用dexopt进行相应的验证与优化操作。
dexopt的主程序为OptMain.cpp,其中处理apk/jar/zip文件中的classes.dex的函数为extractAndProcessZip(),extractAndProcessZip()首先通过dexZipFindEntry()函数检查目标文件中是否拥有class.dex,如果没有就失败返回,成功的话就调用dexZipGetEntryInfo()函数来读取classes.dex的时间戳与crc校验值,如果这一步没有问题,接着调用dexZipExtractEntryTo-File()函数释放classes.dex为缓存文件,然后开始解析传递过来的验证与优化选项,验证选项使用“v=”指出,优化选项使用“o=”指出。所有的预备工作都做完后,调用dvmPrepForDexOpt()函数启动一个虚拟机进程,在这个函数中,优化选项dexOptMode与验证选项varifyMode被传递到了全局DvmGlobals结构gDvm的dexOptMode与classVerifyMode字段中。这时候所有的初始化工作已经完成,dexopt调用dvmContinueOptimization()函数开始真正的验证和优化工作。
dvmContinueOptimization()函数的调用链比较长。首先从OptMain.cpp转移到、dalvik/vm/analysis/DexPrepare.cpp,因为这里有dvmContinueOptimization()函数的实现。函数首先对dex文件做简单的检查,确保传递进来的目标文件属于dex或odex,接着调用mmap()函数将整个文件映射到内存中,然后根据gDvm的dexOptMode与classVerifyMode字段来设置doVarify与doOpt两个布尔值,接着调用rewriteDex()函数来重写dex文件,这里的重写内容包括字符调整、结构重新对齐、类验证信息以及辅助数据。rewriteDex()函数调用dexSwapAndVerify()调整字节序,接着调用dvmDexFileOpenPartial()创建DexFile结构,dvmDexFileOpenPartial()函数的实现在Android系统源码dalvik/vm/DvmDex.cpp文件中,该函数调用dexFileParse()函数解析dex文件,dexFileParse()函数读取dex文件的头部,并根据需要调用验证dexComputeChecksum()函数或调用dexComputeOptChecksum()函数来验证dex或odex文件爱你头的checksum与signature字段。
接着回到DvmDex.cpp文件继续看代码,当验证成功后,dvmDexFileOpenPartial()函数调用allocateAuxStructures()函数设置DexFile结构辅助数据的相关字段,最后执行完后返回到rewriteDex()函数。rewriteDex()接下来调用loadAllClasses()加载dex文件中所有的类,如果这一步失败了,程序等不到后面的优化与验证就退出了,如果没有错误发生,会调用verifyAndOptimizeClasses()函数进行真正的验证工作,这个函数会调用verifyAndOptimizeClass()函数来优化与验证具体的类,而verifyAndOptimizeClass()函数会细分这些工作,调用dvmVerifyClass()函数进行验证,再调用dvmOptimizeClass()函数进行优化。
dvmVerifyClass()函数的实现代码位于Android系统源码的dalvik/vm/analysis/DexVerify.cpp文件中。这个函数调用verifyMethod()函数对类的所有直接方法与虚方法进行验证,verifyMethod()函数具体的工作是先调用verifyInstructions()函数来验证方法中的指令及其数据的正确性,再调用dvmVerifyCodeFlow()函数来验证代码流的正确性。
dvmOptimizeClass()函数的实现代码位于Android系统源码的dalvik/vm/analysis/Optimize.cpp文件爱你中。这个函数调用optimizeMethod()函数对类的所有直接方法与虚方法进行优化,优化的主要工作是进行“指令替换”,替换原则的优先级为“volatile”替换-正确性替换-高性能替换。比如指令iget-wide会根据优先级替换为“volatile”形式的iget-wide-volatile,而不是高性能的iget-wide-quick.
rewriteDex函数返回后,会再次调用dvmDexFileOpenPartial()来验证odex文件,接着调用dvmGenerateRegisterMaps()函数来填充辅助数据区结构,填充结构完成后,接下来调用updateChecksum()函数重写dex文件爱你的checksum值,再往下就是writeDependencies()与writeOptData()了。

流程图:

Android .dex文件简介_第3张图片
Paste_Image.png

Android .dex文件简介_第4张图片
Paste_Image.png
Android .dex文件简介_第5张图片
Paste_Image.png

3.0 源码分析

结合上面的概念。对DexFile等有所了解。现在我结合一个大神的博客,将源码分析也贴出来吧。个人觉得他的源码分析很贴切,很有条理。

DexClassLoader的源码

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

**BaseDexClassLoader **

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

接下来看DexPathList:

public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
    //省略参数校验以及异常处理的代码
        this.definingContext = definingContext;
        ……
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        ……
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

我们继续阅读DexPathList.java文件中makeDexElements 的关键代码:

private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
                                             ArrayList suppressedExceptions) {
     // ……
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();
            if (name.endsWith(DEX_SUFFIX)) {   //.dex文件
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                    //.apk  .jar  .zip文件
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            } else if (file.isDirectory()) {
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
            } else {
                System.logW("Unknown file type for: " + file);
            }
        }
                    //……
        return elements.toArray(new Element[elements.size()]);
    }

从上面的代码可以看出ArrayList files。就是我们所有的dex文件。根绝后缀判断文件的类型。然后调用loadDexFile。得到一个DexFile。前面说到了DexFile 是包含了一个Class类里面属性+方法的索引。然后根据new Element(file, true, null, null) 得到一个Element,添加到了elements中。

接下来看下loadDexFile:

private 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”作为输出文件,然后生个一个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文件进行优化和映射。

也只是分析了一个大概流程,还有很多有待之后进行深入。而这里对于阅读Android源码,有了新的体会,首先是工具上,我之前一直是用Source InSight 但是对于一些函数的实现,找起来却是不太方便,因为必须要将函数实现的文件导入到工程中,而用VS来阅读源码,利用Ctrl+Shift+F的功能,在Android源码目录下搜索更为方便,然后可以在Source InSight中进行导入,阅读。其次不得不说阅读源码真的是一个比较痛苦的过程,但真的学习下来,收获还是很大的。

你可能感兴趣的:(Android .dex文件简介)