騰訊Android面試:系統如何加載一個dex文件,他的底層原理是怎麼實現的

本系列文章專注分享大型Bat面試知識,後續會持續更新,喜歡的話麻煩點擊一個關注

面試官: 系統如何加載一個dex文件,他的底層原理是怎麼實現的

心理分析:面試官想知道你是否有過對dex加載相關經驗。此題主要爲tinker熱修復做鋪墊。dex加載與熱修復是有關係的,求職者一定要注意 面試官後續會面試到tinker

**求職者:**應該從DexClassLoader 加載出發

DexClassLoader 是加載包含classes.dex文件的jar文件或者apk文件; 通過構造函數發現需要一個應用私有的,可寫的目錄去緩存優化的classes。可以用使用File dexoutputDir = context.getDir(“dex”,0);創建一個這樣的目錄,不要使用外部緩存,以保護你的應用被代碼注入。

其源碼如下:

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

再解釋下幾個構造函數參數的意義:

  • dexpath爲jar或apk文件目錄。

  • optimizedDirectory爲優化dex緩存目錄。

  • libraryPath包含native lib的目錄路徑。

  • 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);
}

第一句調用的還是父類的構造函數,也就是ClassLoader的構造函數:

protected ClassLoader(ClassLoader parentLoader) {
        this(parentLoader, false);
    }
 
    ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
       if (parentLoader == null && !nullAllowed) {
            throw new NullPointerException(“parentLoader == null && !nullAllowed”);
      }
      parent = parentLoader;
}

該構造函數把傳進來的父類加載器賦給了私有變量parent。

再來看第二句:

this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

pathList爲該類的私有成員變量,類型爲DexPathList,進入到DexPathList函數:

Constructs an instance.
79     *
80     * @param definingContext the context in which any as-yet unresolved
81     * classes should be defined
82     * @param dexPath list of dex/resource path elements, separated by
83     * {@code File.pathSeparator}
84     * @param libraryPath list of native library directory path elements,
85     * separated by {@code File.pathSeparator}
86     * @param optimizedDirectory directory where optimized {@code .dex} files
87     * should be found and written to, or {@code null} to use the default
88     * system directory for same
89     */
90    public DexPathList(ClassLoader definingContext, String dexPath,
91            String libraryPath, File optimizedDirectory) {
92
93        if (definingContext == null) {
94            throw new NullPointerException("definingContext == null");
95        }
96
97        if (dexPath == null) {
98            throw new NullPointerException("dexPath == null");
99        }
100
101        if (optimizedDirectory != null) {
102            if (!optimizedDirectory.exists())  {
103                throw new IllegalArgumentException(
104                        "optimizedDirectory doesn't exist: "
105                        + optimizedDirectory);
106            }
107
108            if (!(optimizedDirectory.canRead()
109                            && optimizedDirectory.canWrite())) {
110                throw new IllegalArgumentException(
111                        "optimizedDirectory not readable/writable: "
112                        + optimizedDirectory);
113            }
114        }
115
116        this.definingContext = definingContext;
117
118        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
119        // save dexPath for BaseDexClassLoader
120        this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
1        suppressedExceptions);
122
123        // Native libraries may exist in both the system and
124        // application library paths, and we use this search order:
125        //
126        //   1. This class loader's library path for application libraries (libraryPath):
127        //   1.1. Native library directories
128        //   1.2. Path to libraries in apk-files
129        //   2. The VM's library path from the system property for system libraries
130        //      also known as java.library.path
131        //
132        // This order was reversed prior to Gingerbread; see http://b/2933456.
133        this.nativeLibraryDirectories = splitPaths(libraryPath, false);
134        this.systemNativeLibraryDirectories =
135                splitPaths(System.getProperty("java.library.path"), true);
136        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
137        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
138
139        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
140                                                          suppressedExceptions);
141
142        if (suppressedExceptions.size() > 0) {
143            this.dexElementsSuppressedExceptions =
144                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
145        } else {
146            dexElementsSuppressedExceptions = null;
147        }
148    }

前面是一些對於傳入參數的驗證,然後調用了makeDexElements。

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
        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()) {
                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()]);
    }
}

不管是dex文件,還是apk文件最終加載的都是loadDexFile,跟進這個函數:

如果optimizedDirectory爲null就會調用openDexFile(fileName, null, 0);加載文件。
否則調用DexFile.loadDex(file.getPath(), optimizedPath, 0);
而這個函數也只是直接調用new DexFile(sourcePathName, outputPathName, flags);
裏面調用的也是openDexFile(sourceName, outputName, flags);
所以最後都是調用openDexFile,跟進這個函數:
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);
        }
}
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);

而這個函數調用的是so的openDexFileNative這個函數。打開成功則返回一個cookie。

接下來就是分析native函數的實現部分了。

-openDexFileNative函數

static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args,JValue* pResult)
{
    ……………
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”);
    }
    ……………
}

這裏會根據是否爲dex文件或者包含classes.dex文件的jar,分別調用函數dvmRawDexFileOpen和dvmJarFileOpen來處理,最終返回一個DexOrJar的結構。

首先來看dvmRawDexFileOpen函數的處理:

int dvmRawDexFileOpen(const char* fileName, const char* odexOutputName,
    RawDexFile** ppRawDexFile, bool isBootstrap)
{
    .................
    dexFd = open(fileName, O_RDONLY);
    if (dexFd < 0) goto bail;

    /* If we fork/exec into dexopt, don't let it inherit the open fd. */
    dvmSetCloseOnExec(dexFd);

    //校驗前8個字節的magic是否正確,然後把校驗和保存到adler32
    if (verifyMagicAndGetAdler32(dexFd, &adler32) < 0) {
        ALOGE("Error with header for %s", fileName);
        goto bail;
    }
    //得到文件修改時間以及文件大小
   if (getModTimeAndSize(dexFd, &modTime, &fileSize) < 0) {
        ALOGE("Error with stat for %s", fileName);
        goto bail;
    }
    .................
    //調用函數dexOptCreateEmptyHeader,構造了一個DexOptHeader結構體,寫入fd並返回
    optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime,
        adler32, isBootstrap, &newFile, /*createIfMissing=*/true);

    if (optFd < 0) {
        ALOGI("Unable to open or create cache for %s (%s)",
                fileName, cachedName);
        goto bail;
    }
    locked = true;

       //如果成功生了opt頭
    if (newFile) {
        u8 startWhen, copyWhen, endWhen;
        bool result;
       off_t dexOffset;

        dexOffset = lseek(optFd, 0, SEEK_CUR);
        result = (dexOffset > 0);

        if (result) {
            startWhen = dvmGetRelativeTimeUsec();
            // 將dex文件中的內容寫入文件的當前位置,也就是從dexOffset的偏移處開始寫
            result = copyFileToFile(optFd, dexFd, fileSize) == 0;
            copyWhen = dvmGetRelativeTimeUsec();
        }

        if (result) {
            //對dex文件進行優化
            result = dvmOptimizeDexFile(optFd, dexOffset, fileSize,
                fileName, modTime, adler32, isBootstrap);
        }

        if (!result) {
            ALOGE("Unable to extract+optimize DEX from '%s'", fileName);
            goto bail;
        }

        endWhen = dvmGetRelativeTimeUsec();
        ALOGD("DEX prep '%s': copy in %dms, rewrite %dms",
            fileName,
            (int) (copyWhen - startWhen) / 1000,
            (int) (endWhen - copyWhen) / 1000);
    }

     //dvmDexFileOpenFromFd這個函數最主要在這裏幹了兩件事情
     // 1.將優化後得dex文件(也就是odex文件)通過mmap映射到內存中,並通過mprotect修改它的映射內存爲只讀權限
     // 2.將映射爲只讀的這塊dex數據中的內容全部提取到DexFile這個數據結構中去
    if (dvmDexFileOpenFromFd(optFd, &pDvmDex) != 0) {
        ALOGI("Unable to map cached %s", fileName);
        goto bail;
    }

    if (locked) {
        /* unlock the fd */
       if (!dvmUnlockCachedDexFile(optFd)) {
            /* uh oh -- this process needs to exit or we'll wedge the system */
            ALOGE("Unable to unlock DEX file");
            goto bail;
        }
        locked = false;
    }

    ALOGV("Successfully opened '%s'", fileName);
    //填充結構體 RawDexFile
    *ppRawDexFile = (RawDexFile*) calloc(1, sizeof(RawDexFile));
    (*ppRawDexFile)->cacheFileName = cachedName;
   (*ppRawDexFile)->pDvmDex = pDvmDex;
    cachedName = NULL;      // don't free it below
    result = 0;

bail:
    free(cachedName);
    if (dexFd >= 0) {
        close(dexFd);
    }
    if (optFd >= 0) {
        if (locked)
            (void) dvmUnlockCachedDexFile(optFd);
        close(optFd);
    }
    return result;
}

最後成功的話,填充RawDexFile。

dvmJarFileOpen的代碼處理也是差不多的。
int dvmJarFileOpen(const char* fileName, const char* odexOutputName,
    JarFile** ppJarFile, bool isBootstrap)
{
    ...
    ...
    ...
    //調用函數dexZipOpenArchive來打開zip文件,並緩存到系統內存裏
    if (dexZipOpenArchive(fileName, &archive) != 0)
        goto bail;
    archiveOpen = true;
    ...
    //這行代碼設置當執行完成後,關閉這個文件句柄
    dvmSetCloseOnExec(dexZipGetArchiveFd(&archive));
    ...
    //優先處理已經優化了的Dex文件
    fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName);
    ...
    //從壓縮包裏找到Dex文件,然後打開這個文件
    entry = dexZipFindEntry(&archive, kDexInJarName);
    ...
    //把未經過優化的Dex文件進行優化處理,並輸出到指定的文件
    if (odexOutputName == NULL) {
                cachedName = dexOptGenerateCacheFileName(fileName,
                                kDexInJarName);
    }
    ...
    //創建緩存的優化文件
    fd = dvmOpenCachedDexFile(fileName, cachedName,
                    dexGetZipEntryModTime(&archive, entry),
                    dexGetZipEntryCrc32(&archive, entry),
                    isBootstrap, &newFile, /*createIfMissing=*/true);
    ...
    //調用函數dexZipExtractEntryToFile從壓縮包裏解壓文件出來
    if (result) {
                    startWhen = dvmGetRelativeTimeUsec();
                    result = dexZipExtractEntryToFile(&archive, entry, fd) == 0;
                    extractWhen = dvmGetRelativeTimeUsec();
                 }
    ...
    //調用函數dvmOptimizeDexFile對Dex文件進行優化處理
    if (result) {
                    result = dvmOptimizeDexFile(fd, dexOffset,
                                dexGetZipEntryUncompLen(&archive, entry),
                                fileName,
                                dexGetZipEntryModTime(&archive, entry),
                                dexGetZipEntryCrc32(&archive, entry),
                                isBootstrap);
                }
    ...
    //調用函數dvmDexFileOpenFromFd來緩存dex文件
    //並分析文件的內容。比如標記是否優化的文件,通過簽名檢查Dex文件是否合法
    if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) {
        ALOGI("Unable to map %s in %s", kDexInJarName, fileName);
        goto bail;
    }
    ...
    //保存文件到緩存裏,標記這個文件句柄已經保存到緩存
    if (locked) {
        /* unlock the fd */
        if (!dvmUnlockCachedDexFile(fd)) {
            /* uh oh -- this process needs to exit or we'll wedge the system */
            ALOGE("Unable to unlock DEX file");
            goto bail;
        }
        locked = false;
    }
    ...
     //設置一些相關信息返回前面的函數處理。
    *ppJarFile = (JarFile*) calloc(1, sizeof(JarFile));
    (*ppJarFile)->archive = archive;
    (*ppJarFile)->cacheFileName = cachedName;
    (*ppJarFile)->pDvmDex = pDvmDex;
    cachedName = NULL;      // don't free it below
    result = 0;
    ...

}

最後成功的話,填充JarFile。

本專欄爲那些想要進階成爲高級Android工程師所準備。 從初中級轉向高級工程師需要從技術的廣度,深度上都有一定的造詣。所以本專欄就主要爲大家分享一些技術,技術原理等。 包含源碼解析,自定義View,動畫實現,架構分享等。 內容難度適中,篇幅精煉,每天只需花上十幾分鍾閱讀即可。 大家可以跟我一起探討,歡迎加羣探討,有flutter—底層開發-性能優化—移動架構—資深UI工程師 —NDK-人工智能相關專業人員和視頻教學資料 。後續還有最新鴻蒙系統相關內容分享。

詳情請查看主頁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章