關於作者
郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。
文章目錄
這篇文章我們來聊一聊關於Android虛擬機器的那些事,當然這裡我們並不需要去講解關於虛擬機器的底層細節,所講的東西都是大家平常在開發中經常用的。例如類的載入機制、資源載入機制、APK打包流程、APK安裝流程 以及Apk啟動流程等。講解這些知識是為了後續的文章《大型Android專案的工程化實踐:外掛化》、《大型Android專案的工程化實踐:熱更新》、《大型Android專案的工程化實踐:模組化》等系列的文章做一個 原理鋪墊。
好了,讓我們開始吧~?
一 類檔案基本結構
Class檔案是一組以8位位元組為基礎的單位的二進位制流,各個資料項按嚴格的順序緊密的排列在Class檔案中,中間沒有任何間隔。
這麼說有點抽象,我們先來舉一個簡單的小例子。?
public class TestClass {
public int sum(int a, int b) {
return a + b;
}
}
複製程式碼
編譯生成Class檔案,然後使用hexdump命令檢視Class檔案裡的內容。
javac TestClass.java
hexdump TestClass.class
複製程式碼
Class檔案內容如下所示:
Classfile /Users/guoxiaoxing/Github-app/android-open-source-project-analysis/demo/src/main/java/com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.class
Last modified 2018-1-23; size 333 bytes
MD5 checksum 72ae3ff578aa0f97b9351522005ec274
Compiled from "TestClass.java"
public class com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass.m:I
#3 = Class #17 // com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
#18 = Utf8 java/lang/Object
{
public com.guoxiaoxing.android.framework.demo.native_framwork.vm.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 15: 0
}
SourceFile: "TestClass.java"
複製程式碼
Class檔案十六機制內容如下所示:
注:筆者用的二進位制檢視軟體是iHex,可以去AppStore下載,Windows使用者可以使用WinHex。
這是一份十六進位制表示的二進位制流,每個位排列緊密,都有其對應的含義,具體說來,如下所示:
注:下列表中四個段分別為 型別、名稱、說明、數量
- u4 magic 識別Class檔案格式,具體值為0xCAFEBABE 1
- u2 minor_version Class檔案格式副版本號 1
- u2 major_version Class檔案格式主版本號 1
- u2 constant_pool_count 常數表項個數 1
- cp_info constant_pool 常數表,又稱變長符號表 constant_pool_count-1
- u2 access_flags Class的宣告中使用的修改符掩碼 1
- u2 this_class 常數表索引,索引內儲存類名或介面名 1
- u2 super_class 常數表索引,索引內儲存父類名 1
- u2 interfaces_count 超介面個數 1
- u2 interfaces 常數表索引,各超介面名稱 interfaces_count
- u2 fields_count 類的域個數 1
- field_info fields 域資料,包括屬性名稱索引 fields_count
- u2 methods_count 方法個數 1
- method_info methods 方法資料,包括方法名稱索引 methods_count
- u2 attributes_count 類附加屬性個數 1
- attribute_info attributes 類附加屬性資料,包括原始檔名稱等 attributs_count
我們可以看著在上面這張表中有類似u2、attribute_info這樣的型別,事實上Class檔案採用一種類似於C語言結構體的偽結構struct來儲存資料,這種結構有兩種資料型別:
- 無符號數:基本資料型別,例如u1代表1個位元組,u2代表2個位元組,u4代表2個位元組,u8代表8個位元組。
- 表:由多個無符號數或者其他表作為資料項而構成的複合資料結構,用於描述有層次關係的複合資料結構,一般以"_info"結尾。
我們分別來看看上述的各個欄位的具體含義已經對應數值。
注:這一塊的內容可能有點枯燥,但是它是我們後續學習類載入機制,Android打包機制,以及學習外掛化、熱更新框架的基礎,所以需要掌握。 但是也沒必要都記住每個段的含義,你只需要有個整體性的認識即可,後續如果忘了具體的內容,可以再回來查閱。?
1.1 魔數
具體含義
魔數:1-4位元組,用來確定這個檔案是否為一個能被虛擬機器接受的Class檔案,它的值為0xCAFEBABE。
對應數值
ca fe ba be
1.2 版本號
具體含義
版本號:5-6位元組是次版本號,7-8位元組是主版本號
對應數值
5-6位元組是次版本號0x0000(即0),7-8位元組是主版本號0x0034(即52).
JDK版本號與數值的對應關係如下所示:
- JDK 1.8 = 52
- JDK 1.7 = 51
- JDK 1.6 = 50
- JDK 1.5 = 49
- JDK 1.4 = 48
- JDK 1.3 = 47
- JDK 1.2 = 46
- JDK 1.1 = 45
1.3 常量池計數/常量池
具體含義
常量池計數:常量池中常量的數量不是固定的,因此常量池入口處會放置一項u2型別的資料,代表常量池容器計數。注意容器計數從1開始,索引為0代表不引用任何一個 常量池的專案。
對應數值
9-10位元組是常量池容器計數0x0013(即19)。說明常量池裡有18個常量,從1-18.
這是我們上面用javap分析的位元組碼檔案裡的常量池裡常量的個數是一直的。
舉個常量池裡的常量的例子?
它的常量值如下所示:
#17 = Utf8 com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
複製程式碼
常量池主要存放字面量與符號引用。
字面量包括:
- 文字字串
- 宣告為final的常量值等
符號引用包括:
- 類與介面的全限定名
- 欄位的名稱與描述符
- 方法的名稱與描述符
常量池裡的每個常量都用一個表來表示,表的結構如下所示:
cp_info {
//代表常量型別
u1 tag;
//代表儲存的常量,不同的常量型別有不同的結構
u1 info[];
}
複製程式碼
目標一共有十四中常量型別,如下所示:
注:下表欄位分別為 型別、標誌(tag)、描述
- CONSTANT_Utf8_info 1 UTF8編碼的Unicode字串
- CONSTANT_Integer_info 3 整型字面量
- CONSTANT_Float_info 4 浮點型字面量
- CONSTANT_Long_info 5 長整型字面量
- CONSTANT_Double_info 6 雙精度浮點型字面量
- CONSTANT_Class_info 7 類或介面的符號引用
- CONSTANT_String_info 8 字串型別字面量
- CONSTANT_Fieldref_info 9 欄位的符號引用
- CONSTANT_Methodref_info 10 類中方法的符號引用
- CONSTANT_InterfaceMethodref_info 11 介面中方法的符號引用
- CONSTANT_NameAndType_info 12 欄位或方法的部分符號引用
1.4 訪問標誌
具體含義
訪問標誌:常量池之後就是訪問標誌,該標誌用於識別一些類或則介面層次的訪問資訊。這些訪問資訊包括這個Class是類還是介面,是否定義Abstract型別等。
對應數值
常量池之後就是訪問標誌,前兩個位元組代表訪問標誌。
從上面的分析中常量池最後一個常量是#14 = Utf8 java/lang/Object,所以它後面的兩個位元組就代表訪問標誌,如下所示:
訪問表示值與含義如下所示:
- ACC_PUBLIC 0x0001 是否為public
- ACC_FINAL 0x0010 是否為final
- ACC_SUPER 0x0020 JDK 1.0.2以後編譯出來的類該標誌位都為真
- ACC_INTERFACE 0x0200 是否為介面
- ACC_ABSTRACT 0x0400 是否為抽象的(介面和抽象類)
- ACC_SYNTHETIC 0x1000 表示這個程式碼並非由使用者產生的
- ACC_ANNOTATION 0x2000 是否為註解
- ACC_ENUM 0x4000 是否為列舉
我們上面寫了一個普通的Java類,ACC_PUBLIC位為真,又由於JDK 1.0.2以後編譯出來的類ACC_SUPER標誌位都為真,所以最終的值為:
0x0001 & 0x0020 = 0x0021
複製程式碼
這個值就是上圖中的值。
1.5 類索引、父類索引與介面索引
具體含義
類索引(用來確定該類的全限定名)、父類索引(用來確定該類的父類的全限定名)是一個u2型別的資料(單個類、單繼承),介面索引是一個u2型別的集合(多介面實現,用來描述該類實現了哪些介面)
對應數值
類索引、父類索引與介面索引緊緊排列在訪問標誌之後。
類索引為0x0002,它的全限定名為com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass。
父類索引為0x0003,它的全限定名為java/lang/Object。
介面索引的第一項是一個u2型別的資料表示介面計數器,表示實現介面的個數。這裡沒有實現任何介面,所以為0x0000。
1.6 欄位表集合
具體含義
欄位表用來描述介面或者類裡宣告的變數、欄位。包括類級變數以及例項級變數,但不包括方法內部宣告的變數。
欄位表結構如下所示:
field_info {
u2 access_flags;//訪問標誌位,例如private、public等
u2 name_index;//欄位的簡單名稱,例如int、long等
u2 descriptor_index;//方法的描述符,描述欄位的資料型別,方法的引數列表和返回值
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製程式碼
access_flags取值如下所示:
- ACC_PUBLIC 0x0001 是否為 public;
- ACC_PRIVATE 0x0002 是否為 private;
- ACC_PROTECTED 0x0004 是否為 protected;
- ACC_STATIC 0x0008 是否為 static;
- ACC_FINAL 0x0010 是否為 final;
- ACC_VOLATILE 0x0040 是否為 volatile;
- ACC_TRANSIENT 0x0080 是否為 transient;
- ACC_SYNTHETIC 0x1000 是否為 synthetic;
- ACC_ENUM 0x4000 是否為enum.
descriptor_index裡描述符的含義如下所示:
- B byte
- C char
- D double
- F float
- I int
- J long
- S short
- Z boolean
- V void
- L Object, 例如 Ljava/lang/Object
對應數值
- 第一個u2型別的值為0x0001,代表當前容器計數器field_count為1,說明這個類只有一個欄位表資料。也就是我們上面定義的類成員變數private int m;
- 第二個u2型別的值為0x0002,代表access_flags,說明這個成員變數的型別為private。
- 第三個u2型別的值為0x0005,代表name_index為5。
- 第四個u2型別的值為0x0006,代表descriptor_index為6。
1.7 方法表集合
方法便用來描述方法相關資訊。
方法表的型別與欄位表完全相同,如下所示:
method_info {
u2 access_flags;//訪問標誌位,例如private、public等
u2 name_index;//方法名
u2 descriptor_index;//方法的描述符,描述欄位的資料型別,方法的引數列表和返回值
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製程式碼
對應的值
- 第一個u2型別的值為0x0002,代表當前類有兩個方法,即為建構函式和我們上面寫的inc()方法。
- 第二個u2型別的值為0x0001,代表access_flags,即方法的訪問型別為public。
- 第三個u2型別的值為0x0007,代表name_index,即為。
- 第四個u2型別的值為0x0008,代表descriptor_index,即為()V。
- 第五個u2型別的值為0x0001,代表attributes_count,表示該方法的屬性集合有一項屬性。
- 第六個u2型別的值為0x0009,代表屬性名稱,對應常量"code",代表此屬性是方法的位元組碼描述。
後續還有屬性表集合等相關資訊,這裡就不再贅述,更多內容請參見Java虛擬機器規範(Java SE 7).pdf。
通過上面的描述,我們理解了Class儲存格式的細節,那麼這些是如何被載入到虛擬機器中去的呢,載入到虛擬機器之後又會發生什麼變化呢??
我們接著來看。
二 類的載入流程
什麼是類的載入??
類的載入就是虛擬機器通過一個類的全限定名來獲取描述此類的二進位制位元組流。
類載入的流程圖如下所示:
載入
- 通過一個類的全限定名來獲取此類的二進位制流。
- 將這個位元組流所代表的靜態儲存結構轉換為方法去的執行時資料結構。
- 在記憶體中生成一個程式碼這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
事實上,從哪裡將一個類載入成二進位制流是有很開發的,具體說來:
- 從zip包中讀取,這就發展成了我們常見的JAR、AAR依賴。
- 執行時動態生成,這是我們常見的動態代理技術,在java.reflect.Proxy中就是用ProxyGenerateProxyClass來為特定介面生成代理類的二進位制流。
驗證
驗證主要是驗證載入進來的位元組碼二進位制流是否符合虛擬機器規範。
- 檔案格式驗證:驗證位元組碼流是否符合Class檔案格式的規範,並且能夠被當前版本的虛擬機器處理。
- 後設資料驗證:對位元組碼描述的語義進行分析,以保證其描述的資訊符合Java語言規範的要求。
- 位元組碼驗證:對位元組碼的資料流和控制流進行分析,確定程式語義是合法的,符合邏輯的。
- 符號引用驗證:這個階段在解析階段中完成,虛擬機器將符號引用轉換為直接引用。
準備
準備階段正式為類變數分為記憶體並設定變數的初始值,所使用的記憶體在方法去裡被分配,這些變數指的是被static修飾的變數,而不包括例項的變數,例項的變數會伴隨著物件的例項化一起在Java堆 中分配。
解析
解析階段將符號引用轉換為直接引用,符號引用我們前面已經說過,它以CONSTANT_class_info等符號來描述引用的目標,而直接引用指的是這些符號引用載入到虛擬機器中以後 的記憶體地址。
這裡的解析主要是針對我們上面提到的欄位表、方法表、屬性表裡面的資訊,具體說來,包括以下型別:
- 介面
- 欄位
- 類方法
- 介面方法
- 方法型別
- 方法控制程式碼
- 呼叫點限定符
初始化
初始化階段開始執行類構造器()方法,該方法是由所有類變數的賦值動作和static語句塊合併產生的
關於類構造器()方法,它和例項構造器()是不同的,關於這個方法我們需要注意以下幾點:
- 類構造器()方法與例項構造器()方法不同,不需要顯式的呼叫父類的構造器,虛擬機器會保證父類構造器先執行。
- 類構造器()方法對於類或者介面不是必須的,如果一個類既沒有賦值操作,也沒有靜態語句塊,則不會生成該方法。
- 介面可以有變數初始化的賦值操作,因此介面也可以生成clinit>()方法、
- 虛擬機器會保證一個類的()方法在多執行緒環境下能夠被正確的加鎖和同步。如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒執行該類的clinit>()方法 ,其他執行緒會被阻塞。
講完了類的載入流程,我們接著來看看類載入器。
三 類載入器
3.1 Java虛擬機器類載入機制
類的載入就是虛擬機器通過一個類的全限定名來獲取描述此類的二進位制位元組流,而完成這個載入動作的就是類載入器。
類和類載入器息息相關,判定兩個類是否相等,只有在這兩個類被同一個類載入器載入的情況下才有意義,否則即便是兩個類來自同一個Class檔案,被不同類載入器載入,它們也是不相等的。
注:這裡的相等性保函Class物件的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果以及Instance關鍵字對物件所屬關係的判定結果等。
類載入器可以分為三類:
- 啟動類載入器(Bootstrap ClassLoader):負責載入<JAVA_HOME>\lib目錄下或者被-Xbootclasspath引數所指定的路徑的,並且是被虛擬機器所識別的庫到記憶體中。
- 擴充套件類載入器(Extension ClassLoader):負責載入<JAVA_HOME>\lib\ext目錄下或者被java.ext.dirs系統變數所指定的路徑的所有類庫到記憶體中。
- 應用類載入器(Application ClassLoader):負責載入使用者類路徑上的指定類庫,如果應用程式中沒有實現自己的類載入器,一般就是這個類載入器去載入應用程式中的類庫。
這麼多類載入器,那麼當類在載入的時候會使用哪個載入器呢??
這個時候就要提到類載入器的雙親委派模型,流程圖如下所示:
雙親委派模型的整個工作流程非常的簡單,如下所示:
如果一個類載入器收到了載入類的請求,它不會自己立即去載入類,它會先去請求父類載入器,每個層次的類載入器都是如此。層層傳遞,直到傳遞到最高層的類載入器,只有當 父類載入器反饋自己無法載入這個類,才會有當前子類載入器去載入該類。
關於雙親委派機制,在ClassLoader原始碼裡也可以看出,如下所示:
public abstract class ClassLoader {
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//首先,檢查該類是否已經被載入
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//先呼叫父類載入器去載入
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果父類載入器沒有載入到該類,則自己去執行載入
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
}
複製程式碼
為什麼要這麼做呢??
這是為了要讓越基礎的類由越高層的類載入器載入,例如Object類,無論哪個類載入器去嘗試載入這個類,最終都會傳遞給最高層的類載入器去載入,前面我們也說過,類的相等性是由 類與其類載入器共同判定的,這樣Object類無論在何種類載入器環境下都是同一個類。
相反如果沒有雙親委派模型,那麼每個類載入器都會去載入Object,那麼系統中就會出現多個不同的Object類了,如此一來系統的最基礎的行為也就無法保證了。
理解了JVM上的類載入機制,我們再來看看Android虛擬機器上上是如何載入類的。
3.2 Android虛擬機器類載入機制
Java虛擬機器載入的是class檔案,而Android虛擬機器載入的是dex檔案(多個class檔案合併而成),所以兩者既有相似的地方,也有所不同。
Android類載入器類圖如下所示:
可以看到Android類載入器的基類是BaseDexClassLoader,它有派生出兩個子類載入器:
- PathClassLoader: 主要用於系統和app的類載入器,其中optimizedDirectory為null, 採用預設目錄/data/dalvik-cache/
- DexClassLoader: 可以從包含classes.dex的jar或者apk中,載入類的類載入器, 可用於執行動態載入, 但必須是app私有可寫目錄來快取odex檔案. 能夠載入系統沒有安裝的apk或者jar檔案, 因此很多外掛化方案都是採用DexClassLoader;
除了這兩個子類以為,還有兩個類:
- DexPathList:就跟它的名字那樣,該類主要用來查詢Dex、SO庫的路徑,並這些路徑整體呈一個陣列。
- DexFile:用來描述Dex檔案,Dex的載入以及Class額查詢都是由該類呼叫它的native方法完成的。
我們先來看看基類BaseDexClassLoader的構造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
複製程式碼
BaseDexClassLoader構造方法的四個引數的含義如下:
- dexPath:指的是在Androdi包含類和資源的jar/apk型別的檔案集合,指的是包含dex檔案。多個檔案用“:”分隔開,用程式碼就是File.pathSeparator。
- optimizedDirectory:指的是odex優化檔案存放的路徑,可以為null,那麼就採用預設的系統路徑。
- libraryPath:指的是native庫檔案存放目錄,也是以“:”分隔。
- parent:parent類載入器
DexClassLoader與PathClassLoader都繼承於BaseDexClassLoader,這兩個類只是提供了自己的建構函式,沒有額外的實現,我們對比下它們的建構函式的區別。
PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
複製程式碼
DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
複製程式碼
可以發現這兩個類的建構函式最大的差別就是DexClassLoader提供了optimizedDirectory,而PathClassLoader則沒有,optimizedDirectory正是用來存放odex檔案 的地方,以後可以利用DexClassLoader實現動態載入。
上面我們也說過,Dex的載入以及Class額查詢都是由DexFile呼叫它的native方法完成的,我們來看看它的實現。
我們來看看Dex檔案載入、類的查詢載入的序列圖,如下所示:
從上圖Dex載入的流程可以看出,optimizedDirectory決定了呼叫哪一個DexFile的建構函式。
如果optimizedDirectory為空,這個時候其實是PathClassLoader,則呼叫:
DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
throws IOException {
this(file.getPath(), loader, elements);
}
複製程式碼
如果optimizedDirectory不為空,這個時候其實是DexClassLoader,則呼叫:
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
DexPathList.Element[] elements) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
mFileName = sourceName;
//System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
複製程式碼
所以你可以看到DexClassLoader在載入Dex檔案的時候比PathClassLoader多了一個openDexFile()方法,該方法呼叫的是native方法openDexFileNative()方法。
這個方法並不是真的開啟Dex檔案,而是將Dex檔案以一種mmap的方式對映到虛擬機器程式的地址空間中去,實現檔案磁碟地址和程式虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,虛擬機器 程式就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。
關於mmap,它是一種很有用的檔案讀寫方式,限於篇幅這裡不再展開,更多關於mmap的內容可以參見文章:http://www.cnblogs.com/huxiao-tee/p/4660352.html
到這裡,Android虛擬機器的類載入機制就講的差不多了,我們再來總結一下。
Android虛擬機器有兩個類載入器DexClassLoader與PathClassLoader,它們都繼承於BaseDexClassLoader,它們內部都維護了一個DexPathList的物件,DexPathList主要用來存放指明包含dex檔案、native庫和優化odex目錄。 Dex檔案採用DexFile這個類來描述,Dex的載入以及類的查詢都是通過DexFile呼叫它的native方法來完成的。