熱修復技術和插件化技術風靡,大部分人都說得上來其原理基於ClassLoader動態加載機制,卻一直沒有仔細瞭解過其實現細節,今天抽空追蹤了下DexClassLoader 源碼。java
DexClassLoader 能夠從 SD 卡上加載包含 class.dex 的 .jar 和 .apk 文件,這也是插件化和熱修復的基礎,在不須要安裝應用的狀況下,完成須要使用的 dex 的加載git
經過其構造函數發現須要提供一個應用私有且具有讀寫權限的目錄去緩存優化的classes。能夠用使用File dexoutputDir = context.getDir(「dex」,0);建立一個這樣的目錄,不要使用外部緩存,以保護你的應用被代碼注入。緩存
DexClassLoader源碼以下:cookie
public class DexClassLoader extends BaseDexClassLoader { /** * Creates a {@code DexClassLoader} that finds interpreted and native * code. Interpreted classes are found in a set of DEX files contained * in Jar or APK files. * * <p>The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } }
首先調用的是父類的構造函數:super(dexPath, new File(optimizedDirectory), libraryPath, parent);數據結構
BaseDexClassLoader 的構造函數以下:app
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }
依然首先調用父類的構造函數,也就是ClassLoader的構造函數,該構造函數把傳進來的父類加載器賦給了私有變量parent:ide
protected ClassLoader(ClassLoader parentLoader) { this(parentLoader, false); } /* * constructor for the BootClassLoader which needs parent to be null. */ ClassLoader(ClassLoader parentLoader, boolean nullAllowed) { if (parentLoader == null && !nullAllowed) { throw new NullPointerException(「parentLoader == null && !nullAllowed」); } parent = parentLoader; }
其次並初始化了DexPathList類型的對象pathList:函數
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
pathList爲BaseDexClassLoader類的私有成員變量,類型爲DexPathList,進入到DexPathList函數:優化
/** * Constructs an instance. * * @param definingContext the context in which any as-yet unresolved * classes should be defined * @param dexPath list of dex/resource path elements, separated by * {@code File.pathSeparator} * @param librarySearchPath list of native library directory path elements, * separated by {@code File.pathSeparator} * @param optimizedDirectory directory where optimized {@code .dex} files * should be found and written to, or {@code null} to use the default * system directory for same */ public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. This class loader's library path for application libraries (librarySearchPath): // 1.1. Native library directories // 1.2. Path to libraries in apk-files // 2. The VM's library path from the system property for system libraries // also known as java.library.path // // This order was reversed prior to Gingerbread; see http://b/2933456. this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } }
前面是一些對於傳入參數的驗證,而後調用了makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader)方法,源碼以下:this
/** * Makes an array of dex/resource path elements, one per element of * the given array. */ private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * Open all files and load the (direct or contained) dex files up front. */ for (File file : files) { if (file.isDirectory()) { // We support directories for looking up resources. Looking up resources in // directories is useful for running libcore tests. elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { DexFile dex = null; try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } 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); } if (dex == null) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } } else { System.logW("ClassLoader referenced unknown path: " + file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
無論是dex文件,仍是apk文件最終調用的都是loadDexFile,跟進這個函數:
/** * Constructs a {@code DexFile} instance, as appropriate depending on whether * {@code optimizedDirectory} is {@code null}. An application image file may be associated with * the {@code loader} if it is not null. */ private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { if (optimizedDirectory == null) { return new DexFile(file, loader, elements); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); } }
若是optimizedDirectory爲null就會從新new 一個DexFile對象,在構造函數中會調用openDexFile(fileName, null, 0);
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); }
不然調用DexFile.loadDex(String sourcePathName, String outputPathName, int flags), 而這個函數一樣會直接調用new DexFile(sourcePathName, outputPathName, flags):
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException { /* * TODO: we may want to cache previously-opened DexFile objects. * The cache would be synchronized with close(). This would help * us avoid mapping the same DEX more than once when an app * decided to open it multiple times. In practice this may not * be a real issue. */ return new DexFile(sourcePathName, outputPathName, flags); }
繼續跟進openDexFile(sourceName, outputName, flags)這個函數:
/* * Open a DEX file. The value returned is a magic VM cookie. On * failure, an IOException is thrown. */ native private static int openDexFile(String sourceName, String outputName, int flags) throws IOException; /* * Open a DEX file based on a {@code byte[]}. The value returned * is a magic VM cookie. On failure, a RuntimeException is thrown. */ native private static int openDexFile(byte[] fileContents);
能夠發現openDexFile是一個native函數,接下來就是分析native函數的實現部分了:
static void Dalvik_dalvik_system_DexFile_openDexFile(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」); } …………… }
這裏會根據hasDexExtension(sourceName)的返回結果判斷是否爲dex文件是否爲dex文件,若是是則繼續調用函數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。