1.引言
上節(jié)我問過自己?jiǎn)栴}dvm,將Class轉(zhuǎn)成成.dex文件,然后再將.dex文件轉(zhuǎn)換成Class文件。那么為什么要這樣做。這樣做不是多此一舉嗎,為什么不直接用Class文件,這樣做豈不是很方面嗎?解決這些問題之前我首要的是要明白什么是.dex,內(nèi)部結(jié)構(gòu),有什么用。接下來我將談?wù)?dex。主要參考著幾篇優(yōu)秀的文章。Android中dex文件的加載與優(yōu)化流程 。
分析的參考博客.dex淺析在此感謝他們的付出。
2.正題
2.1 .dex的結(jié)構(gòu)
.dex結(jié)構(gòu)分成三部分:
文件頭:表明了是dex文件,已經(jīng)文件的大小等等數(shù)據(jù)
索引頭:如下圖所示
數(shù)據(jù)區(qū):數(shù)據(jù)區(qū),就像是jvm中的堆保存方法+變量。(寫在這對(duì)jvm的常量池,堆棧,寄存器不是很清楚,準(zhǔn)備專門在寫一篇文章記錄下加強(qiáng)自己的記憶)

2.2 odex文件
odex是OptimizedDEX的縮寫,表示對(duì)dex的優(yōu)化。寫到這讓我想起上一篇DexClassLoad的構(gòu)造方法.里面需要傳入一個(gè)OptimizedPath。這個(gè)OptimizedPath大概就是這個(gè)odex的路徑。
2.3 DexFile文件
DexFile是dex文件被映射到內(nèi)存中的結(jié)構(gòu),出了基本的dex文件結(jié)構(gòu)外,還包含了DexOptHead和尾部附加的數(shù)據(jù),這些數(shù)據(jù)是Android系統(tǒng)為了結(jié)合當(dāng)前平臺(tái)特性對(duì)dex文件的結(jié)構(gòu)進(jìn)行了優(yōu)化和擴(kuò)充,是運(yùn)行效率更高。

從上面的圖可以看出。DexFile 保存了一個(gè)Class對(duì)象的屬性+方法的索引。也就是說 我們可以根據(jù)一個(gè)DexFile 得到一個(gè)Class對(duì)象。
2.4 .dex的加載過程
**加載過程略微的繁瑣,了解下過程就像。我直接復(fù)制過來的 **
Android提供了一個(gè)專門驗(yàn)證與優(yōu)化dex文件的工具dexopt。其源碼位于Android系統(tǒng)源碼的dalvik/dexopt目錄下,Dalvik虛擬機(jī)在加載一個(gè)dex文件時(shí),通過指定的驗(yàn)證與優(yōu)化選項(xiàng)來調(diào)用dexopt進(jìn)行相應(yīng)的驗(yàn)證與優(yōu)化操作。
dexopt的主程序?yàn)镺ptMain.cpp,其中處理apk/jar/zip文件中的classes.dex的函數(shù)為extractAndProcessZip(),extractAndProcessZip()首先通過dexZipFindEntry()函數(shù)檢查目標(biāo)文件中是否擁有class.dex,如果沒有就失敗返回,成功的話就調(diào)用dexZipGetEntryInfo()函數(shù)來讀取classes.dex的時(shí)間戳與crc校驗(yàn)值,如果這一步?jīng)]有問題,接著調(diào)用dexZipExtractEntryTo-File()函數(shù)釋放classes.dex為緩存文件,然后開始解析傳遞過來的驗(yàn)證與優(yōu)化選項(xiàng),驗(yàn)證選項(xiàng)使用“v=”指出,優(yōu)化選項(xiàng)使用“o=”指出。所有的預(yù)備工作都做完后,調(diào)用dvmPrepForDexOpt()函數(shù)啟動(dòng)一個(gè)虛擬機(jī)進(jìn)程,在這個(gè)函數(shù)中,優(yōu)化選項(xiàng)dexOptMode與驗(yàn)證選項(xiàng)varifyMode被傳遞到了全局DvmGlobals結(jié)構(gòu)gDvm的dexOptMode與classVerifyMode字段中。這時(shí)候所有的初始化工作已經(jīng)完成,dexopt調(diào)用dvmContinueOptimization()函數(shù)開始真正的驗(yàn)證和優(yōu)化工作。
dvmContinueOptimization()函數(shù)的調(diào)用鏈比較長(zhǎng)。首先從OptMain.cpp轉(zhuǎn)移到、dalvik/vm/analysis/DexPrepare.cpp,因?yàn)檫@里有dvmContinueOptimization()函數(shù)的實(shí)現(xiàn)。函數(shù)首先對(duì)dex文件做簡(jiǎn)單的檢查,確保傳遞進(jìn)來的目標(biāo)文件屬于dex或odex,接著調(diào)用mmap()函數(shù)將整個(gè)文件映射到內(nèi)存中,然后根據(jù)gDvm的dexOptMode與classVerifyMode字段來設(shè)置doVarify與doOpt兩個(gè)布爾值,接著調(diào)用rewriteDex()函數(shù)來重寫dex文件,這里的重寫內(nèi)容包括字符調(diào)整、結(jié)構(gòu)重新對(duì)齊、類驗(yàn)證信息以及輔助數(shù)據(jù)。rewriteDex()函數(shù)調(diào)用dexSwapAndVerify()調(diào)整字節(jié)序,接著調(diào)用dvmDexFileOpenPartial()創(chuàng)建DexFile結(jié)構(gòu),dvmDexFileOpenPartial()函數(shù)的實(shí)現(xiàn)在Android系統(tǒng)源碼dalvik/vm/DvmDex.cpp文件中,該函數(shù)調(diào)用dexFileParse()函數(shù)解析dex文件,dexFileParse()函數(shù)讀取dex文件的頭部,并根據(jù)需要調(diào)用驗(yàn)證dexComputeChecksum()函數(shù)或調(diào)用dexComputeOptChecksum()函數(shù)來驗(yàn)證dex或odex文件愛你頭的checksum與signature字段。
接著回到DvmDex.cpp文件繼續(xù)看代碼,當(dāng)驗(yàn)證成功后,dvmDexFileOpenPartial()函數(shù)調(diào)用allocateAuxStructures()函數(shù)設(shè)置DexFile結(jié)構(gòu)輔助數(shù)據(jù)的相關(guān)字段,最后執(zhí)行完后返回到rewriteDex()函數(shù)。rewriteDex()接下來調(diào)用loadAllClasses()加載dex文件中所有的類,如果這一步失敗了,程序等不到后面的優(yōu)化與驗(yàn)證就退出了,如果沒有錯(cuò)誤發(fā)生,會(huì)調(diào)用verifyAndOptimizeClasses()函數(shù)進(jìn)行真正的驗(yàn)證工作,這個(gè)函數(shù)會(huì)調(diào)用verifyAndOptimizeClass()函數(shù)來優(yōu)化與驗(yàn)證具體的類,而verifyAndOptimizeClass()函數(shù)會(huì)細(xì)分這些工作,調(diào)用dvmVerifyClass()函數(shù)進(jìn)行驗(yàn)證,再調(diào)用dvmOptimizeClass()函數(shù)進(jìn)行優(yōu)化。
dvmVerifyClass()函數(shù)的實(shí)現(xiàn)代碼位于Android系統(tǒng)源碼的dalvik/vm/analysis/DexVerify.cpp文件中。這個(gè)函數(shù)調(diào)用verifyMethod()函數(shù)對(duì)類的所有直接方法與虛方法進(jìn)行驗(yàn)證,verifyMethod()函數(shù)具體的工作是先調(diào)用verifyInstructions()函數(shù)來驗(yàn)證方法中的指令及其數(shù)據(jù)的正確性,再調(diào)用dvmVerifyCodeFlow()函數(shù)來驗(yàn)證代碼流的正確性。
dvmOptimizeClass()函數(shù)的實(shí)現(xiàn)代碼位于Android系統(tǒng)源碼的dalvik/vm/analysis/Optimize.cpp文件愛你中。這個(gè)函數(shù)調(diào)用optimizeMethod()函數(shù)對(duì)類的所有直接方法與虛方法進(jìn)行優(yōu)化,優(yōu)化的主要工作是進(jìn)行“指令替換”,替換原則的優(yōu)先級(jí)為“volatile”替換-正確性替換-高性能替換。比如指令iget-wide會(huì)根據(jù)優(yōu)先級(jí)替換為“volatile”形式的iget-wide-volatile,而不是高性能的iget-wide-quick.
rewriteDex函數(shù)返回后,會(huì)再次調(diào)用dvmDexFileOpenPartial()來驗(yàn)證odex文件,接著調(diào)用dvmGenerateRegisterMaps()函數(shù)來填充輔助數(shù)據(jù)區(qū)結(jié)構(gòu),填充結(jié)構(gòu)完成后,接下來調(diào)用updateChecksum()函數(shù)重寫dex文件愛你的checksum值,再往下就是writeDependencies()與writeOptData()了。
流程圖:



3.0 源碼分析
結(jié)合上面的概念。對(duì)DexFile等有所了解?,F(xiàn)在我結(jié)合一個(gè)大神的博客,將源碼分析也貼出來吧。個(gè)人覺得他的源碼分析很貼切,很有條理。
DexClassLoader的源碼
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader 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);
}
接下來看DexPathList:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
//省略參數(shù)校驗(yàn)以及異常處理的代碼
this.definingContext = definingContext;
……
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
……
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
我們繼續(xù)閱讀DexPathList.java文件中makeDexElements 的關(guān)鍵代碼:
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
// ……
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()) {
// 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);
}
}
//……
return elements.toArray(new Element[elements.size()]);
}
從上面的代碼可以看出ArrayList<File> files。就是我們所有的dex文件。根絕后綴判斷文件的類型。然后調(diào)用loadDexFile。得到一個(gè)DexFile。前面說到了DexFile 是包含了一個(gè)Class類里面屬性+方法的索引。然后根據(jù)new Element(file, true, null, null) 得到一個(gè)Element,添加到了elements中。
接下來看下loadDexFile:
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);
}
}
//生成odex的目錄
private static String optimizedPathFor(File path,
File optimizedDirectory) {
String fileName = path.getName();
if (!fileName.endsWith(DEX_SUFFIX)) {
int lastDot = fileName.lastIndexOf(".");
if (lastDot < 0) {
fileName += DEX_SUFFIX;
} else {
StringBuilder sb = new StringBuilder(lastDot + 4);
sb.append(fileName, 0, lastDot);
sb.append(DEX_SUFFIX);
fileName = sb.toString();
}
}
File result = new File(optimizedDirectory, fileName);
return result.getPath();
}
optimizedPathFor 方法:就是將dex 優(yōu)化形成optimizedDex文件。然后
通過DexFile的靜態(tài)方法將.dex文件。或者apk,.jar加載成DexFile文件。DexFile 是C語(yǔ)言中的結(jié)構(gòu)體,里面保存的都是一些指針。經(jīng)過不斷的校檢,最后通過dvm里面的c代碼處理,得到DexFile。
總結(jié):
我們可以簡(jiǎn)要總結(jié)下整個(gè)的加載流程,首先是對(duì)文件名的修正,后綴名置為”.dex”作為輸出文件,然后生個(gè)一個(gè)DexPathList對(duì)象函數(shù)直接返回一個(gè)DexPathList對(duì)象,
在DexPathList的構(gòu)造函數(shù)中調(diào)用makeDexElements()函數(shù),在makeDexElement()函數(shù)中調(diào)用loadDexFile()開始對(duì).dex或者是.jar .zip .apk文件進(jìn)行處理,
跟入loadDexFile()函數(shù)中,會(huì)發(fā)現(xiàn)里面做的工作很簡(jiǎn)單,調(diào)用optimizedPathFor()函數(shù)對(duì)optimizedDiretcory路徑進(jìn)行修正。
之后才真正通過DexFile.loadDex()開始加載文件中的數(shù)據(jù),其中的加載也只是返回一個(gè)DexFile對(duì)象。
在DexFile類的構(gòu)造函數(shù)中,重點(diǎn)便放在了其調(diào)用的openDexFile()函數(shù),在openDexFile()中調(diào)用了openDexFileNative()真正進(jìn)入native層,
在openDexFileNative()的真正實(shí)現(xiàn)中,對(duì)于后綴名為.dex的文件或者其他文件(.jar .apk .zip)分開進(jìn)行處理:
.dex文件調(diào)用dvmRawDexFileOpen();
其他文件調(diào)用dvmJarFileOpen()。
在dvmRawDexFileOpen()函數(shù)中,檢驗(yàn)dex文件的標(biāo)志,檢驗(yàn)odex文件的緩存名稱,之后將dex文件拷貝到odex文件中,并對(duì)odex進(jìn)行優(yōu)化
調(diào)用dvmDexFileOpenFromFd()對(duì)優(yōu)化后的odex文件進(jìn)行映射,通過mprotect置為"只讀"屬性并將映射的內(nèi)存結(jié)構(gòu)保存在DvmDex*結(jié)構(gòu)中。
dvmJarFileOpen()先對(duì)文件進(jìn)行映射,結(jié)構(gòu)保存在ZipArchive中,然后再嘗試以文件名作為dex文件名來“打開”文件,
如果失敗,則調(diào)用dexZipFindEntry在ZipArchive的名稱hash表中找名為"class.dex"的文件,然后創(chuàng)建odex文件,下面就和
dvmRawDexFileOpen()一樣了,就是對(duì)dex文件進(jìn)行優(yōu)化和映射。
也只是分析了一個(gè)大概流程,還有很多有待之后進(jìn)行深入。而這里對(duì)于閱讀Android源碼,有了新的體會(huì),首先是工具上,我之前一直是用Source InSight 但是對(duì)于一些函數(shù)的實(shí)現(xiàn),找起來卻是不太方便,因?yàn)楸仨氁獙⒑瘮?shù)實(shí)現(xiàn)的文件導(dǎo)入到工程中,而用VS來閱讀源碼,利用Ctrl+Shift+F的功能,在Android源碼目錄下搜索更為方便,然后可以在Source InSight中進(jìn)行導(dǎo)入,閱讀。其次不得不說閱讀源碼真的是一個(gè)比較痛苦的過程,但真的學(xué)習(xí)下來,收獲還是很大的。