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

posted @ 2023-01-30 18:48  Domefy  阅读(135)  评论(1编辑  收藏  举报