關於做者html
郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。java
文章目錄android
這篇文章咱們來聊一聊關於Android虛擬機的那些事,固然這裏咱們並不須要去講解關於虛擬機的底層細節,所講的東西都是你們日常在開發中常常用的。例如類的加載機制、資源加載機制、APK打包流程、APK安裝流程 以及Apk啓動流程等。講解這些知識是爲了後續的文章《大型Android項目的工程化實踐:插件化》、《大型Android項目的工程化實踐:熱更新》、《大型Android項目的工程化實踐:模塊化》等系列的文章作一個 原理鋪墊。git
好了,讓咱們開始吧~😁程序員
Class文件是一組以8位字節爲基礎的單位的二進制流,各個數據項按嚴格的順序緊密的排列在Class文件中,中間沒有任何間隔。github
這麼說有點抽象,咱們先來舉一個簡單的小例子。🤞數組
public class TestClass {
public int sum(int a, int b) {
return a + b;
}
}
複製代碼
編譯生成Class文件,而後使用hexdump命令查看Class文件裏的內容。緩存
javac TestClass.java
hexdump TestClass.class
複製代碼
Class文件內容以下所示:bash
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文件十六機制內容以下所示:cookie
注:筆者用的二進制查看軟件是iHex,能夠去AppStore下載,Windows用戶可使用WinHex。
這是一份十六進制表示的二進制流,每一個位排列緊密,都有其對應的含義,具體說來,以下所示:
注:下列表中四個段分別爲 類型、名稱、說明、數量
咱們能夠看着在上面這張表中有相似u二、attribute_info這樣的類型,事實上Class文件採用一種相似於C語言結構體的僞結構struct來存儲數據,這種結構有兩種數據類型:
咱們分別來看看上述的各個字段的具體含義已經對應數值。
注:這一塊的內容可能有點枯燥,可是它是咱們後續學習類加載機制,Android打包機制,以及學習插件化、熱更新框架的基礎,因此須要掌握。 可是也不必都記住每一個段的含義,你只須要有個總體性的認識便可,後續若是忘了具體的內容,能夠再回來查閱。😁
具體含義
魔數:1-4字節,用來肯定這個文件是否爲一個能被虛擬機接受的Class文件,它的值爲0xCAFEBABE。
對應數值
ca fe ba be
具體含義
版本號:5-6字節是次版本號,7-8字節是主版本號
對應數值
5-6字節是次版本號0x0000(即0),7-8字節是主版本號0x0034(即52).
JDK版本號與數值的對應關係以下所示:
具體含義
常量池計數:常量池中常量的數量不是固定的,所以常量池入口處會放置一項u2類型的數據,表明常量池容器計數。注意容器計數從1開始,索引爲0表明不引用任何一個 常量池的項目。
對應數值
9-10字節是常量池容器計數0x0013(即19)。說明常量池裏有18個常量,從1-18.
這是咱們上面用javap分析的字節碼文件裏的常量池裏常量的個數是一直的。
舉個常量池裏的常量的例子🤞
它的常量值以下所示:
#17 = Utf8 com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass
複製代碼
常量池主要存放字面量與符號引用。
字面量包括:
符號引用包括:
常量池裏的每一個常量都用一個表來表示,表的結構以下所示:
cp_info {
//表明常量類型
u1 tag;
//表明存儲的常量,不一樣的常量類型有不一樣的結構
u1 info[];
}
複製代碼
目標一共有十四中常量類型,以下所示:
注:下表字段分別爲 類型、標誌(tag)、描述
具體含義
訪問標誌:常量池以後就是訪問標誌,該標誌用於識別一些類或則接口層次的訪問信息。這些訪問信息包括這個Class是類仍是接口,是否認義Abstract類型等。
對應數值
常量池以後就是訪問標誌,前兩個字節表明訪問標誌。
從上面的分析中常量池最後一個常量是#14 = Utf8 java/lang/Object,因此它後面的兩個字節就表明訪問標誌,以下所示:
訪問表示值與含義以下所示:
咱們上面寫了一個普通的Java類,ACC_PUBLIC位爲真,又因爲JDK 1.0.2之後編譯出來的類ACC_SUPER標誌位都爲真,因此最終的值爲:
0x0001 & 0x0020 = 0x0021
複製代碼
這個值就是上圖中的值。
具體含義
類索引(用來肯定該類的全限定名)、父類索引(用來肯定該類的父類的全限定名)是一個u2類型的數據(單個類、單繼承),接口索引是一個u2類型的集合(多接口實現,用來描述該類實現了哪些接口)
對應數值
類索引、父類索引與接口索引牢牢排列在訪問標誌以後。
類索引爲0x0002,它的全限定名爲com/guoxiaoxing/android/framework/demo/native_framwork/vm/TestClass。
父類索引爲0x0003,它的全限定名爲java/lang/Object。
接口索引的第一項是一個u2類型的數據表示接口計數器,表示實現接口的個數。這裏沒有實現任何接口,因此爲0x0000。
具體含義
字段表用來描述接口或者類裏聲明的變量、字段。包括類級變量以及實例級變量,但不包括方法內部聲明的變量。
字段表結構以下所示:
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取值以下所示:
descriptor_index裏描述符的含義以下所示:
對應數值
方法便用來描述方法相關信息。
方法表的類型與字段表徹底相同,以下所示:
method_info {
u2 access_flags;//訪問標誌位,例如private、public等
u2 name_index;//方法名
u2 descriptor_index;//方法的描述符,描述字段的數據類型,方法的參數列表和返回值
u2 attributes_count;
attribute_info attributes[attributes_count];
}
複製代碼
對應的值
後續還有屬性表集合等相關信息,這裏就再也不贅述,更多內容請參見Java虛擬機規範(Java SE 7).pdf。
經過上面的描述,咱們理解了Class存儲格式的細節,那麼這些是如何被加載到虛擬機中去的呢,加載到虛擬機以後又會發生什麼變化呢?🤔
咱們接着來看。
什麼是類的加載?🤔
類的加載就是虛擬機經過一個類的全限定名來獲取描述此類的二進制字節流。
類加載的流程圖以下所示:
加載
事實上,從哪裏將一個類加載成二進制流是有很開發的,具體說來:
驗證
驗證主要是驗證加載進來的字節碼二進制流是否符合虛擬機規範。
準備
準備階段正式爲類變量分爲內存並設置變量的初始值,所使用的內存在方法去裏被分配,這些變量指的是被static修飾的變量,而不包括實例的變量,實例的變量會伴隨着對象的實例化一塊兒在Java堆 中分配。
解析
解析階段將符號引用轉換爲直接引用,符號引用咱們前面已經說過,它以CONSTANT_class_info等符號來描述引用的目標,而直接引用指的是這些符號引用加載到虛擬機中之後 的內存地址。
這裏的解析主要是針對咱們上面提到的字段表、方法表、屬性表裏面的信息,具體說來,包括如下類型:
初始化
初始化階段開始執行類構造器()方法,該方法是由全部類變量的賦值動做和static語句塊合併產生的
關於類構造器()方法,它和實例構造器()是不一樣的,關於這個方法咱們須要注意如下幾點:
講完了類的加載流程,咱們接着來看看類加載器。
類的加載就是虛擬機經過一個類的全限定名來獲取描述此類的二進制字節流,而完成這個加載動做的就是類加載器。
類和類加載器息息相關,斷定兩個類是否相等,只有在這兩個類被同一個類加載器加載的狀況下才有意義,不然即使是兩個類來自同一個Class文件,被不一樣類加載器加載,它們也是不相等的。
注:這裏的相等性保函Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果以及Instance關鍵字對對象所屬關係的斷定結果等。
類加載器能夠分爲三類:
這麼多類加載器,那麼當類在加載的時候會使用哪一個加載器呢?🤔
這個時候就要提到類加載器的雙親委派模型,流程圖以下所示:
雙親委派模型的整個工做流程很是的簡單,以下所示:
若是一個類加載器收到了加載類的請求,它不會本身當即去加載類,它會先去請求父類加載器,每一個層次的類加載器都是如此。層層傳遞,直到傳遞到最高層的類加載器,只有當 父類加載器反饋本身沒法加載這個類,纔會有當前子類加載器去加載該類。
關於雙親委派機制,在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虛擬機上上是如何加載類的。
Java虛擬機加載的是class文件,而Android虛擬機加載的是dex文件(多個class文件合併而成),因此二者既有類似的地方,也有所不一樣。
Android類加載器類圖以下所示:
能夠看到Android類加載器的基類是BaseDexClassLoader,它有派生出兩個子類加載器:
除了這兩個子類覺得,還有兩個類:
咱們先來看看基類BaseDexClassLoader的構造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
複製代碼
BaseDexClassLoader構造方法的四個參數的含義以下:
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方法來完成的。