05-DALVIK加载和解析DEX过程
5.1 dex和odex文件结构和关系
上图是Dex文件和Odex文件的结构和关系图。
应用程序在第一次启动app的时候,会在/dalvik/dalvik-cache目录下生成odex文件结构,其实就是在原app的dex文件结构基础上,增加odex文件头和并在原dex文件末尾增加依赖库信息和辅助信息
依赖库信息知名该dex文件所需要的本地函数库,通过下图我们可以看到该本地函数库其实就是/system/framework下的jar包。
辅助信息记录了dex中类的索引表,dalvik为每一个dex文件创建一个对应DexClassLookup对象用于记dex文件所有的类索引信息,DexClassLookup对象为的每个类都生成了table结构体对象,该对象记录了类的描述符的哈希值、类描述符在dex文件中的偏移和类在dex文件中的偏移。其table结构体对象定义如下。也就是说通过该table结构体对象就可以对dex文件中某一个类快速定位。
5.2 Dex、DexFile、RawDexFile、DvmDex和ClassObject之间的关系
在开始分析学习Dalvik加载解析dex过程的源码之前,我们先来了解几个重要的数据结构之间的关系,Dalvik加载解析dex的过程就是紧紧围绕这些数据结构进行,其本质就是把加载的dex解析之后,最终以ClassObject的数据结构的形式保存在内存,方便在读取执行具体类方法指令的时候,可以快速查找执行。最终传递给解释器执行的是字节码指令,保存这个指令信息的正是Method结构体对象中的insns成员,改结构体对象是解释器的重要输入。而该结构体对象正是ClassObject结构体中的成员之一。ClassObject结构体对象几乎包含类目标在运行时所需的所有资源,包括当前类的超类、当前类使用的类加载器、Method类型指针等。
在第二章的2.3.4节,我们知道app在启动过程中创建了PathClassLoader加载dex文件。Dalvik加载和解析dex文件的操作就发生在PathClassLoader类中。我们从该类的构造函数开始分析。
位置:Android源码/android/app/ApplicationLoaders.java
class ApplicationLoaders{ public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent){ //父类加载器为BootClassLoader ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
synchronized (mLoaders) { if (parent == null) { parent = baseParent; }
if (parent == baseParent) { ClassLoader loader = mLoaders.get(zip); if (loader != null) { return loader; } … PathClassLoader pathClassloader = new PathClassLoader(zip, libPath, parent); … mLoaders.put(zip, pathClassloader); return pathClassloader; } … PathClassLoader pathClassloader = new PathClassLoader(zip, parent); … return pathClassloader; } } } |
getClassLoader方法这里创建了PathClassLoader类对象,我们跳转到该类的构造函数。
位置:libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); }
public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } } |
PathClassLoader类继承BaseDexClassLoader父类,PathClassLoader构造函数最终调用的是父类的构造函数。下面我们跳转到BaseDexClassLoader类的构造函数。
位置:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathLis
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } … } |
位置:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; private static final String APK_SUFFIX = ".apk";
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext,String dexPath,String libraryPath, File optimizedDirectory) { … this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); … this.nativeLibraryDirectories = splitLibraryPath(libraryPath); } … static class Element { private final File file; private final boolean isDirectory; private final File zip; private final DexFile dexFile; private ZipFile zipFile; private boolean initialized; … } } |
DexPathList有一个很关键的成员:Element数组dexElements变量。我们看下Element类的结构,可以知道,Element主要是用来保存dex或资源文件路径的类。我们知道一些app方法超过65535个的时候,就需要使用多dex的方案进行处理,如下图所示,所以这里是Element数组而不是一个Element。
spliteDexPath方法是传递进来的dex路径字符串分割转换成File对象数组,然后调用makeDexElements方法加载dex、jar、apk或zip结尾的文件。
下面,我们看下makeDexElements方法是如何创建Element数组对象的。
位置:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { ArrayList<Element> elements = new ArrayList<Element>(); /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) { // dex结尾的文件 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)) { zip = file;
try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if the * zip file turns out to be resource-only (that is, no classes.dex file in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ 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); }
if ((zip != null) || (dex != null)) { elements.add(new Element(file, false, zip, dex)); } }
return elements.toArray(new Element[elements.size()]); } |
makeDexElements遍历所有文件,我们看到加载dex文件的关键函数loadDexFile,它最终返回一个DexFile结构对象。我们接着分析loadDexFile函数:
位置:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
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); } } |
我们可以看到loadDexFile加载dex有两种情况,一种是没有设置保存优化dex目录的时候,则直接创建一个DexFile对象,否则就直接调用DexFile类的loadDex方法完成家在解析dex工作。
位置:libcore/dalvik/src/main/java/dalvik/system/DexFile.java
public final class DexFile { private int mCookie; private final String mFileName; … public DexFile(File file) throws IOException { this(file.getPath()); }
public DexFile(String fileName) throws IOException { mCookie = openDexFile(fileName, null, 0); mFileName = fileName; guard.open("close"); //System.out.println("DEX FILE cookie is " + mCookie); }
private DexFile(String sourceName, String outputName, int flags) throws IOException { … mCookie = openDexFile(sourceName, outputName, flags); mFileName = sourceName; guard.open("close"); //System.out.println("DEX FILE cookie is " + mCookie); }
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException { return new DexFile(sourcePathName, outputPathName, flags); } … private static int openDexFile(String sourceName, String outputName,int flags) throws IOException { return openDexFileNative(new File(sourceName).getCanonicalPath(), (outputName == null) ? null : new File(outputName).getCanonicalPath(), flags); }
native private static int openDexFileNative(String sourceName, String outputName, int flags) throws IOException; …
} |
我们可以看到,上面两种情况函数调用流程如下:
(1)new DexFile(File file)è DexFile(String fileName)è openDexFileè openDexFileNative
(2)loadDexè DexFile(String sourceName, String outputName, int flags)è openDexFileè openDexFileNative
可以看到最终两种情况都调用了一个native方法openDexFilenative,只是传递的参数不同而已。
openDexFileNative方法最终返回一个cookie标识,这个标识跟加载到内存中的dex数据结构相关联。下面我们进入native层分析openDexFileNative。
位置:Android源码/vm/native/dalvik_system_DexFile.cpp
static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) { StringObject* sourceNameObj = (StringObject*) args[0]; StringObject* outputNameObj = (StringObject*) args[1]; DexOrJar* pDexOrJar = NULL; JarFile* pJarFile; RawDexFile* pRawDexFile; char* sourceName; char* outputName; … if (hasDexExtension(sourceName) && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { ALOGV("Opening DEX file '%s' (DEX)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = NULL; } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { ALOGV("Opening DEX file '%s' (Jar)", sourceName);
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile; pDexOrJar->pDexMemory = NULL; } else { ALOGV("Unable to open DEX file '%s'", sourceName); dvmThrowIOException("unable to open DEX file"); } if (pDexOrJar != NULL) { pDexOrJar->fileName = sourceName; addToDexFileTable(pDexOrJar); } else { free(sourceName); }
free(outputName); RETURN_PTR(pDexOrJar); } |
可以看到Dalvik_dalvik_system_DexFile_openDexFileNative方法的核心部分是dvmRawDexFileOpen和dvmJarFileOpen。当传递进来的是dex文件类型的时候,调用dvmRawDexFileOpen加载解析,并将解析后的文件信息保存到RawDexFile结构体中;如果不是dex文件类型,例如dex文件包含在压缩包中,则调用dvmJarFileOpen加载解析,并将解析后的文件信息保存到JarFile中。接着,将两种情况解析后的信息都保存到DexOrJar结构体对象中,最后调用addToDexFileTable方法将当前解析后的DexOrJar对象保存到全局变量gDvm.userDexFiles哈希表数组中。
这里,我们主要分析dvmRawDexFileOpen的实现,dvmJarFileOpen方法过车类似,区别就是多了一个提取压缩包中dex文件的过程。
位置:Android源码/dalvik/vm/RawDexFile.cpp
int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName, RawDexFile** ppRawDexFile, bool isBootstrap) { DvmDex* pDvmDex = NULL; char* cachedName = NULL; int result = -1; int dexFd = -1; int optFd = -1; u4 modTime = 0; u4 adler32 = 0; size_t fileSize = 0; bool newFile = false; bool locked = false;
dexFd = open(fileName, O_RDONLY); … //获取优化后的dex文件名称 if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, NULL); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } ... optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime, adler32, isBootstrap, &newFile, /*createIfMissing=*/true); … //优化dex文件 if (newFile) { u8 startWhen, copyWhen, endWhen; bool result; off_t dexOffset;
dexOffset = lseek(optFd, 0, SEEK_CUR); result = (dexOffset > 0);
if (result) { startWhen = dvmGetRelativeTimeUsec(); result = copyFileToFile(optFd, dexFd, fileSize) == 0; copyWhen = dvmGetRelativeTimeUsec(); }
if (result) { result = dvmOptimizeDexFile(optFd, dexOffset, fileSize, fileName, modTime, adler32, isBootstrap); } } //加载解析dex文件 if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) { ALOGI("Unable to map cached %s", fileName); goto bail; } } |
5.3 Dex文件优化过程
位置:Android源码/dalvik/vm/analysis/DexPrepare.cpp
bool dvmOptimizeDexFile(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap) { … pid_t pid; … pid = fork(); if (pid == 0) { … static const char* kDexOptBin = "/bin/dexopt"; … androidRoot = getenv("ANDROID_ROOT"); if (androidRoot == NULL) { ALOGW("ANDROID_ROOT not set, defaulting to /system"); androidRoot = "/system"; } execFile = (char*)alloca(strlen(androidRoot) + strlen(kDexOptBin) + 1); strcpy(execFile, androidRoot); strcat(execFile, kDexOptBin); // /system/bin/dexopt … argv[curArg++] = execFile; argv[curArg++] = "--dex"; … const char* argv[argc+1]; … if (kUseValgrind) execv(kValgrinder, const_cast<char**>(argv)); else execv(execFile, const_cast<char**>(argv)); } … } |
这里我们看到dvmOptimizeDexFile最终是创建了一个子进程,并调用/system/bin/dexopt程序进行优化工作。
dexopt的主程序代码位于Android源码/dalvik/dexopt/OptMain.cpp文件中。其入口函数为main方法。
int main(int argc, char* const argv[]) { set_process_name("dexopt");
setvbuf(stdout, NULL, _IONBF, 0);
if (argc > 1) { if (strcmp(argv[1], "--zip") == 0) return fromZip(argc, argv); else if (strcmp(argv[1], "--dex") == 0) return fromDex(argc, argv); else if (strcmp(argv[1], "--preopt") == 0) return preopt(argc, argv); }
… } |
由于上面传递过来的是”--dex”参数,这里我们是调用fromDex方法进行优化。
位置:dexopt的主程序代码位于Android源码/dalvik/dexopt/OptMain.cpp
static int fromDex(int argc, char* const argv[]) { … //启动一个虚拟机进程 if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) { … } … //进行优化 if (!dvmContinueOptimization(fd, offset, length, debugFileName, modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0)) { … } } |
fromDex方法里调用了2个关键函数,dvmPrepForDexOpt用于启动一个虚拟机进程,然后调用了dvmContinueOptimization方法进行实际的优化工作。
位置:Android源码/dalvik/vm/analysis/DexPrepare.cpp
bool dvmContinueOptimization(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap) { … void* mapAddr; mapAddr = mmap(NULL, dexOffset + dexLength, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); … success = rewriteDex(((u1*) mapAddr) + dexOffset, dexLength, doVerify, doOpt, &pClassLookup, NULL); if (success) { DvmDex* pDvmDex = NULL; u1* dexAddr = ((u1*) mapAddr) + dexOffset;
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) { … }else{ … DexHeader* pHeader = (DexHeader*)pDvmDex->pHeader; updateChecksum(dexAddr, dexLength, pHeader); … } } } |
5.4 Dex文件加载解析过程
回到dvmRawDexFileOpen方法,在调用dvmOptimizeDexFile方法完成dex文件的优化后,接着调用dvmDexFileOpenFromFd方法将上一步优化后的dex文件映射到内存中并进行加载和解析。
位置:Android源码/dalvik/vm/DvmDex.cpp
int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) {
} |
位置:Android源码/dalvik/libdex/DexFile.cpp
DexFile* dexFileParse(const u1* data, size_t length, int flags) {
} |
位置:Android源码/dalvik/libdex/DexFile.cpp
DexFile* dexFileParse(const u1* data, size_t length, int flags) {
} |
总结
Dalvik在加载dex文件并执行某个类的方法,其整个流程可以用下图表示。类加载机制最终的目标就是为目标类生成一个ClassObject的数据结构的实例对象,并将加载了的这个类的ClassObject对象添加到全局变量gDvm的loadedClasses成员中,该成员主要是保存加载到内存中的类对象。
当Dalvik要运行某一个类方法的时候,是通过运行在内存中的ClassObject对象中的资源去执行,最后调用dvmInterpret方法初始化解释器并执行字节码指令。具体的细节会在下一章中详细讲解。
参考:
[1] Dalvik加载dex过程分析 http://zke1ev3n.me/2016/04/12/Dalvik%E5%8A%A0%E8%BD%BDdex%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/
[2] Dalvik and ART http://202.197.96.98/cache/10/03/newandroidbook.com/75c0878af2978850a35ea842e2acf90e/Andevcon-DEX.pdf
[3] Android中dex文件的加载与优化流程 http://blog.csdn.net/jsqfengbao/article/details/52103439