本系列文章專注分享大型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-人工智能相關專業人員和視頻教學資料 。後續還有最新鴻蒙系統相關內容分享。
詳情請查看主頁