轉自:老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
在Android系統中,應用程序進程都是由Zygote進程孵化出來的,而Zygote進程是由Init進程啓動的。Zygote進程在啓動時會創建一個Dalvik虛擬機實例,每當它孵化一個新的應用程序進程時,都會將這個Dalvik虛擬機實例複製到新的應用程序進程裏面去,從而使得每一個應用程序進程都有一個獨立的Dalvik虛擬機實例。在本文中,我們就分析Dalvik虛擬機在Zygote進程中的啓動過程。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
《android系統源代碼情景分析》一書正在進擊的程序員網(http://0xcc0xcd.com)中連載,點擊進入!
Zygote進程在啓動的過程中,除了會創建一個Dalvik虛擬機實例之外,還會將Java運行時庫加載到進程中來,以及註冊一些Android核心類的JNI方法來前面創建的Dalvik虛擬機實例中去。注意,一個應用程序進程被Zygote進程孵化出來的時候,不僅會獲得Zygote進程中的Dalvik虛擬機實例拷貝,還會與Zygote一起共享Java運行時庫,這完全得益於Linux內核的進程創建機制(fork)。這種Zygote孵化機制的優點是不僅可以快速地啓動一個應用程序進程,還可以節省整體的內存消耗,缺點是會影響開機速度,畢竟Zygote是在開機過程中啓動的。不過,總體來說,是利大於弊的,畢竟整個系統只有一個Zygote進程,而可能有無數個應用程序進程,而且我們不會經常去關閉手機,大多數情況下只是讓它進入休眠狀態。
從前面Android系統進程Zygote啓動過程的源代碼分析一文可以知道,Zygote進程在啓動的過程中,會調用到AndroidRuntime類的成員函數start,接下來我們就這個函數開始分析Dalvik虛擬機啓動相關的過程,如圖1所示:
圖1 Dalvik虛擬機的啓動過程
這個過程可以分爲8個步驟,接下來我們就詳細分析每一個步驟。
Step 1. AndroidRuntime.start
這個函數定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。AndroidRuntime類的成員函數start主要做了以下四件事情:
1. 調用成員函數startVm來創建一個Dalvik虛擬機實例,並且保存在成員變量mJavaVM中。
2. 調用成員函數startReg來註冊一些Android核心類的JNI方法。
3. 調用參數className所描述的一個Java類的靜態成員函數main,來作爲Zygote進程的Java層入口。從前面Android系統進程Zygote啓動過程的源代碼分析一文可以知道,這個入口類就爲com.android.internal.os.ZygoteInit。執行這一步的時候,Zygote進程中的Dalvik虛擬機實例就開始正式運作了。注意,在這一步中,也就是在com.android.internal.os.ZygoteInit類的靜態成員函數main,會進行大量的Android核心類和系統資源文件預加載。其中,預加載的Android核心類可以參考frameworks/base/preloaded-classes這個文件,而預加載的系統資源就是包含在/system/framework/framework-res.apk中。
4. 從com.android.internal.os.ZygoteInit類的靜態成員函數main返回來的時候,就說明Zygote進程準備要退出來了。在退出之前,會調用前面創建的Dalvik虛擬機實例的成員函數DetachCurrentThread和DestroyJavaVM。其中,前者用來將Zygote進程的主線程脫離前面創建的Dalvik虛擬機實例,後者是用來銷燬前面創建的Dalvik虛擬機實例。
接下來,我們就主要關注Dalvik虛擬機實例的創建過程,以及Android核心類JNI方法的註冊過程,即AndroidRuntime類的成員函數startVm和startReg的實現。
Step 2. AndroidRuntime.startVm
這個函數定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。
在啓動Dalvik虛擬機的時候,可以指定一系列的選項,這些選項可以通過特定的系統屬性來指定。下面我們就簡單瞭解幾個可能有用的選項。
1. -Xcheck:jni:用來啓動JNI方法檢查。我們在C/C++代碼中,可以修改Java對象的成員變量或者調用Java對象的成員函數。加了-Xcheck:jni選項之後,就可以對要訪問的Java對象的成員變量或者成員函數進行合法性檢查,例如,檢查類型是否匹配。我們可以通過dalvik.vm.checkjni或者ro.kernel.android.checkjni這兩個系統屬性來指定是否要啓用-Xcheck:jni選項。注意,加了-Xcheck:jni選項之後,會使用得JNI方法執行變慢。
2. -Xint:portable,-Xint:fast,-Xint:jit:用來指定Dalvik虛擬機的執行模式。Dalvik虛擬機支持三種運行模式,分別是Portable、Fast和Jit。Portable是指Dalvik虛擬機以可移植的方式來進行編譯,也就是說,編譯出來的虛擬機可以在任意平臺上運行。Fast是針對當前平臺對Dalvik虛擬機進行編譯,這樣編譯出來的Dalvik虛擬機可以進行特殊的優化,從而使得它能更快地運行程序。Jit不是解釋執行代碼,而是將代碼動態編譯成本地語言後再執行。我們可以通過dalvik.vm.execution-mode系統屬笥來指定Dalvik虛擬機的解釋模式。
3. -Xstacktracefile:用來指定調用堆棧輸出文件。Dalvik虛擬機接收到SIGQUIT(Ctrl-\或者kill -3)信號之後,會將所有線程的調用堆棧輸出來,默認是輸出到日誌裏面。指定了-Xstacktracefile選項之後,就可以將線程的調用堆棧輸出到指定的文件中去。我們可以通過dalvik.vm.stack-trace-file系統屬性來指定調用堆棧輸出文件。
4. -Xmx:用來指定Java對象堆的最大值。Dalvik虛擬機的Java對象堆的默認最大值是16M,不過我們可以通過dalvik.vm.heapsize系統屬性來指定爲其它值。
更多的Dalvik虛擬機啓動選項,可以參考Controlling the Embedded VM一文。
設置好Dalvik虛擬機的啓動選項之後,AndroidRuntime的成員函數startVm就會調用另外一個函數JNI_CreateJavaVM來創建以及初始化一個Dalvik虛擬機實例。
Step 3. JNI_CreateJavaVM
這個函數定義在文件dalvik/vm/Jni.c中。JNI_CreateJavaVM主要完成以下四件事情。
1. 爲當前進程創建一個Dalvik虛擬機實例,即一個JavaVMExt對象。
2. 爲當前線程創建和初始化一個JNI環境,即一個JNIEnvExt對象,這是通過調用函數dvmCreateJNIEnv來完成的。
3. 將參數vm_args所描述的Dalvik虛擬機啓動選項拷貝到變量argv所描述的一個字符串數組中去,並且調用函數dvmStartup來初始化前面所創建的Dalvik虛擬機實例。
4. 調用函數dvmChangeStatus將當前線程的狀態設置爲正在執行NATIVE代碼,並且將面所創建和初始化好的JavaVMExt對象和JNIEnvExt對象通過輸出參數p_vm和p_env返回給調用者。
gDvm是一個類型爲DvmGlobals的全局變量,用來收集當前進程所有虛擬機相關的信息,其中,它的成員變量vmList指向的就是當前進程中的Dalvik虛擬機實例,即一個JavaVMExt對象。以後每當需要訪問當前進程中的Dalvik虛擬機實例時,就可以通過全局變量gDvm的成員變量vmList來獲得,避免了在函數之間傳遞該Dalvik虛擬機實例。
每一個Dalvik虛擬機實例都有一個函數表,保存在對應的JavaVMExt對象的成員變量funcTable中,而這個函數表又被指定爲gInvokeInterface。gInvokeInterface是一個類型爲JNIInvokeInterface的結構體,它定義在文件dalvik/vm/Jni.c中,如下所示:
有了這個Dalvik虛擬機函數表之後,我們就可以將當前線程Attach或者Detach到Dalvik虛擬機中去,或者銷燬當前進程的Dalvik虛擬機等。每一個Dalvik虛擬機實例還有一個JNI環境列表,保存在對應的JavaVMExt對象的成員變量envList中。注意,JavaVMExt對象的成員變量envList描述的是一個JNIEnvExt列表,其中,每一個Attach到Dalvik虛擬機中去的線程都有一個對應的JNIEnvExt,用來描述它的JNI環境。有了這個JNI環境之後,我們纔可以在Java函數和C/C++函數之間互相調用。
每一個JNIEnvExt對象都有兩個成員變量prev和next,它們均是一個JNIEnvExt指針,分別指向前一個JNIEnvExt對象和後一個JNIEnvExt對象,也就是說,每一個Dalvik虛擬機實例的成員變量envList描述的是一個雙向JNIEnvExt列表,其中,列表中的第一個JNIEnvExt對象描述的是主線程的JNI環境。
上述提到的DvmGlobals結構體定義文件dalvik/vm/Globals.h中,JNIInvokeInterface結構體定義在文件dalvik/libnativehelper/include/nativehelper/jni.h中,JavaVMExt和JNIEnvExt結構體定義在文件dalvik/vm/JniInternal.h中。
接下來,我們接下來就繼續分析函數dvmCreateJNIEnv和函數dvmStartup的實現,以便可以瞭解JNI環境的創建和初始化過程,以及Dalvik虛擬機的虛擬機初始化過程。
Step 4. dvmCreateJNIEnv
這個函數定義在文件dalvik/vm/Jni.c中。函數dvmCreateJNIEnv主要是執行了以下三個操作:
1. 創建一個JNIEnvExt對象,用來描述一個JNI環境,並且設置這個JNIEnvExt對象的宿主Dalvik虛擬機,以及所使用的本地接口表,即設置這個JNIEnvExt對象的成員變量funcTable和vm。這裏的宿主Dalvik虛擬機即爲當前進程的Dalvik虛擬機,它保存在全局變量gDvm的成員變量vmList中。本地接口表由全局變量gNativeInterface來描述。
2. 參數self描述的是前面創建的JNIEnvExt對象要關聯的線程,可以通過調用函數dvmSetJniEnvThreadId來將它們關聯起來。注意,當參數self的值等於NULL的時候,就表示前面的JNIEnvExt對象是要與主線程關聯的,但是要等到後面再關聯,因爲現在用來描述主線程的Thread對象還沒有準備好。通過將一個JNIEnvExt對象的成員變量envThreadId和self的值分別設置爲0x77777775和0x77777779來表示它還沒有與線程關聯。
3. 在一個Dalvik虛擬機裏面,可以運行多個線程。所有關聯有JNI環境的線程都有一個對應的JNIEnvExt對象,這些JNIEnvExt對象相互連接在一起保存在用來描述其宿主Dalvik虛擬機的一個JavaVMExt對象的成員變量envList中。因此,前面創建的JNIEnvExt對象需要連接到其宿主Dalvik虛擬機的JavaVMExt鏈表中去。
gNativeInterface是一個類型爲JNINativeInterface的結構體,它定義在文件dalvik/vm/Jni.c,如下所示:
從gNativeInterface的定義就可以看出,結構體JNINativeInterface用來描述一個本地接口表。當我們需要在C/C++代碼在中調用Java函數,就要用到這個本地接口表,例如:1. 調用函數FindClass可以找到指定的Java類;
2. 調用函數GetMethodID可以獲得一個Java類的成員函數,並且可以通過類似CallObjectMethod函數來間接調用它;
3. 調用函數GetFieldID可以獲得一個Java類的成員變量,並且可以通過類似SetIntField的函數來設置它的值;
4. 調用函數RegisterNatives和UnregisterNatives可以註冊和反註冊JNI方法到一個Java類中去,以便可以在Java函數中調用;
5. 調用函數GetJavaVM可以獲得當前進程中的Dalvik虛擬機實例。
事實上,結構體JNINativeInterface定義的可以在C/C++代碼中調用的函數非常多,具體可以參考它在dalvik\libnativehelper\include\nativehelper\jni.h文件中的定義。
這一步執行完成之後,返回到前面的Step 3中,即函數JNI_CreateJavaVM中,接下來就會繼續調用函數dvmStartup來初始化前面所創建的Dalvik虛擬機實例。
Step 5. dvmStartup
這個函數定義在文件dalvik/vm/Init.c中,用來初始化Dalvik虛擬機,我們分段來閱讀:
這段代碼用來處理Dalvik虛擬機的啓動選項,這些啓動選項保存在參數argv中,並且個數等於argc。在處理這些啓動選項之前,還會執行以下兩個操作:1. 調用函數setCommandLineDefaults來給Dalvik虛擬機設置默認參數,因爲啓動選項不一定會指定Dalvik虛擬機的所有屬性。
2. 調用函數dvmPropertiesStartup來分配足夠的內存空間來容納由參數argv和argc所描述的啓動選項。
完成以上兩個操作之後,就可以調用函數dvmProcessOptions來處理參數argv和argc所描述的啓動選項了,也就是根據這些選項值來設置Dalvik虛擬機的屬性,例如,設置Dalvik虛擬機的Java對象堆的最大值。
在上述代碼中,函數setCommandLineDefaults和dvmPropertiesStartup定義在文件dalvik/vm/Init.c中,函數dvmPropertiesStartup定義在文件dalvik/vm/Properties.c中。
我們繼續往下閱讀代碼:
如果我們沒有在Dalvik虛擬機的啓動選項中指定-Xrs,那麼gDvm.reduceSignals的值就會被設置爲false,表示要在當前線程中屏蔽掉SIGQUIT信號。在這種情況下,會有一個線程專門用來處理SIGQUIT信號。這個線程在接收到SIGQUIT信號的時候,就會將各個線程的調用堆棧打印出來,因此,這個線程又稱爲dump-stack-trace線程。屏蔽當前線程的SIGQUIT信號是通過調用函數blockSignals來實現的,這個函數定義在文件dalvik/vm/Init.c中。
我們繼續往下閱讀代碼:
這段代碼用來初始化Dalvik虛擬機的各個子模塊,接下來我們就分別描述。1. dvmAllocTrackerStartup
這個函數定義在文件dalvik/vm/AllocTracker.c中,用來初始化Davlik虛擬機的對象分配記錄子模塊,這樣我們就可以通過DDMS工具來查看Davlik虛擬機的對象分配情況。
2. dvmGcStartup
這個函數定義在文件dalvik/vm/alloc/Alloc.c中,用來初始化Davlik虛擬機的垃圾收集( GC)子模塊。
3. dvmThreadStartup
這個函數定義在文件dalvik/vm/Thread.c中,用來初始化Davlik虛擬機的線程列表、爲主線程創建一個Thread對象以及爲主線程初始化執行環境。Davlik虛擬機中的所有線程均是本地操作系統線程。在linux系統中,一般都是使用pthread庫來創建和管理線程的,Android系統也不例外,也就是說,Davlik虛擬機中的每一個線程均是一個pthread線程。注意,Davlik虛擬機中的每一個線程均用一個Thread結構體來描述,這些Thread結構體組織在一個列表中,因此,這裏要先對它進行初始化。
4. dvmInlineNativeStartup
這個函數定義在文件dalvik/vm/InlineNative.c中,用來初始化Davlik虛擬機的內建Native函數表。這些內建Native函數主要是針對java.Lang.String、java.Lang.Math、java.Lang.Float和java.Lang.Double類的,用來替換這些類的某些成員函數原來的實現(包括Java實現和Native實現)。例如,當我們調用java.Lang.String類的成員函數compareTo來比較兩個字符串的大小時,實際執行的是由Davlik虛擬機提供的內建函數javaLangString_compareTo(同樣是定義在文件dalvik/vm/InlineNative.c中)。在提供有__memcmp16函數的系統中,函數javaLangString_compareTo會利用它來直接比較兩個字符串的大小。由於函數__memcmp16是用優化過的彙編語言的來實現的,它的效率會更高。
5. dvmVerificationStartup
這個函數定義在文件dalvik/vm/analysis/DexVerify.c中,用來初始化Dex文件驗證器。Davlik虛擬機與Java虛擬機一樣,在加載一個類文件的時候,一般需要驗證它的合法性,也就是驗證文件中有沒有非法的指令或者操作等。
6. dvmRegisterMapStartup
這個函數定義在文件dalvik/vm/analysis/RegisterMap.c中,用來初始化寄存器映射集(Register Map)子模塊。Davlik虛擬機支持精確垃圾收集(Exact GC或者Precise GC),也就是說,在進行垃圾收集的時候,Davlik虛擬機可以準確地判斷當前正在使用的每一個寄存器裏面保存的是對象引用還是非對象引用。對於對象引用,意味被引用的對象現在還不可以回收,因此,就可以進行精確的垃圾收集。
爲了幫助垃圾收集器準備地判斷寄存器保存的是對象引用還是非對象引用,Davlik虛擬機在驗證了一個類之後,還會爲它的每一個成員函數生成一個寄存器映射集。寄存器映射集記錄了類成員函數在每一個GC安全點(Safe Point)中的寄存器使用情況,也就是記錄每一個寄存器裏面保存的是對象引用還是非對象引用。由於垃圾收集器一定是在GC安全點進行垃圾收集的,因此,根據每一個GC安全點的寄存器映射集,就可以準確地知道對象的引用情況,從而可以確定哪些可以回收,哪些對象還不可以回收。
7. dvmInstanceofStartup
這個函數定義在文件dalvik/vm/oo/TypeCheck.c中,用來初始化instanceof操作符子模塊。在使用instanceof操作符來判斷一個對象A是否是一個類B的實例時,Davlik虛擬機需要檢查類B是否是從對象A的聲明類繼承下來的。由於這個檢查的過程比較耗時,Davlik虛擬機在內部使用一個緩衝,用來記錄第一次兩個類之間的instanceof操作結果,這樣後面再碰到相同的instanceof操作時,就可以快速地得到結果。
8. dvmClassStartup
這個函數定義在文件dalvik/vm/oo/Class.c中,用來初始化啓動類加載器(Bootstrap Class Loader),同時還會初始化java.lang.Class類。啓動類加載器是用來加載Java核心類的,用來保證安全性,即保證加載的Java核心類是合法的。
9. dvmThreadObjStartup
這個函數定義在文件dalvik/vm/Thread.c中,用來加載與線程相關的類,即java.lang.Thread、java.lang.VMThread和java.lang.ThreadGroup。
10. dvmExceptionStartup
這個函數定義在文件dalvik/vm/Exception.c中,用來加載與異常相關的類,即java.lang.Throwable、java.lang.RuntimeException、java.lang.StackOverflowError、java.lang.Error、java.lang.StackTraceElement和java.lang.StackTraceElement類。
11. dvmStringInternStartup
這個函數定義在文件dalvik/vm/Intern.c中,用來初始化java.lang.String類內部私有一個字符串池,這樣當Dalvik虛擬機運行起來之後,我們就可以調用java.lang.String類的成員函數intern來訪問這個字符串池裏面的字符串。
12. dvmNativeStartup
這個函數定義在文件dalvik/vm/Native.c中,用來初始化Native Shared Object庫加載表,也就是SO庫加載表。這個加載表是用來描述當前進程有哪些SO文件已經被加載過了。
13. dvmInternalNativeStartup
這個函數定義在文件dalvik/vm/native/InternalNative.c中,用來初始化一個內部Native函數表。所有需要直接訪問Dalvik虛擬機內部函數或者數據結構的Native函數都定義在這張表中,因爲它們如果定義在外部的其它SO文件中,就無法直接訪問Dalvik虛擬機的內部函數或者數據結構。例如,前面提到的java.lang.String類的成員函數intent,由於它要訪問Dalvik虛擬機內部的一個私有字符串池,因此,它所對應的Native函數就要在Dalvik虛擬機內部實現。
14. dvmJniStartup
這個函數定義在文件dalvik/vm/Jni.c中,用來初始化全局引用表,以及加載一些與Direct Buffer相關的類,如DirectBuffer、PhantomReference和ReferenceQueue等。
我們在一個JNI方法中,可能會需要訪問一些Java對象,這樣就需要通知GC,這些Java對象現在正在被Native Code引用,不能回收。這些被Native Code引用的Java對象就會被記錄在一個全局引用表中,具體的做法就是調用JNI環境對象(JNIEnv)的成員函數NewLocalRef/DeleteLocalRef和NewGlobalRef/DeleteGlobalRef等來顯式地引用或者釋放Java對象。
有時候我們需要在Java代碼中,直接在Native層分配內存,也就直接使用malloc來分配內存。這些Native內存不同於在Java堆中分配的內存,區別在於前者需要不接受GC管理,而後者接受GC管理。這些直接在Native層分配的內存有什麼用呢?考慮一個場景,我們需要在Java代碼中從一個IO設備中讀取數據。從IO設備讀取數據意味着要調用由本地操作系統提供的read接口來實現。這樣我們就有兩種做法。第一種做法在Native層臨時分配一個緩衝區,用來保存從IO設備read回來的數據,然後再將這個數據拷貝到Java層中去,也就是拷貝到Java堆去使用。第二種做法是在Java層創建一個對象,這個對象在Native層直接關聯有一塊內存,從IO設備read回來的數據就直接保存這塊內存中。第二種方法和第一種方法相比,減少了一次內存拷貝,因而可以提高性能。
我們將這種能夠直接在Native層中分配內存的Java對象就稱爲DirectBuffer。由於DirectBuffer使用的內存是不接受GC管理的,因此,我們就需要通過其它的方式來管理它們。具體做法就是爲每一個DirectBuffer對象創建一個PhantomReference引用。注意,DirectBuffer對象本身是一個Java對象,它是接受GC管理的。當GC準備回收一個DirectBuffer對象時,如果發現它還有PhantomReference引用,那就會在回收它之前,把相應的PhantomReference引用加入到與之關聯的一個ReferenceQueue隊列中去。這樣我們就可以通過判斷一個DirectBuffer對象的PhantomReference引用是否已經加入到一個相關的ReferenceQueue隊列中。如果已經加入了的話,那麼就可以在該DirectBuffer對象被回收之前,釋放掉之前爲它在Native層分配的內存。
15. dvmReflectStartup
這個函數定義在文件dalvik/vm/reflect/Reflect.c中,用來加載反射相關的類,如java.lang.reflect.AccessibleObject、java.lang.reflect.Constructor、java.lang.reflect.Field、java.lang.reflect.Method和java.lang.reflect.Proxy等。
16. dvmProfilingStartup
這個函數定義在文件dalvik/vm/Profile.c,用來初始化Dalvik虛擬機的性能分析子模塊,以及加載dalvik.system.VMDebug類等。
Dalvik虛擬機的各個子模塊初始化完成之後,我們繼續往下閱讀代碼:
這段代碼檢查java.lang.Class、java.lang.Object、java.lang.Thread、java.lang.VMThread和java.lang.ThreadGroup這五個核心類經過前面的初始化操作後已經得到加載,並且確保系統中存在java.lang.InternalError、java.lang.StackOverflowError、java.lang.UnsatisfiedLinkError和java.lang.NoClassDefFoundError這四個核心類。我們繼續往下閱讀代碼:
這段代碼繼續執行其它函數來執行其它的初始化和檢查工作,如下所示:1. dvmValidateBoxClasses
這個函數定義在文件dalvik/vm/reflect/Reflect.c中,用來驗證Dalvik虛擬機中存在相應的裝箱類,並且這些裝箱類有且僅有一個成員變量,這個成員變量是用來描述對應的數字值的。這些裝箱類包括java.lang.Boolean、java.lang.Character、java.lang.Float、java.lang.Double、java.lang.Byte、java.lang.Short、java.lang.Integer和java.lang.Long。
所謂裝箱,就是可以自動將一個數值轉換一個對象,例如,將數字1自動轉換爲一個java.lang.Integer對象。相應地,也要求能將一個裝箱類對象轉換成一個數字,例如,將一個值等於1的java.lang.Integer對象轉換爲數字1。
2. dvmPrepMainForJni
這個函數定義在文件dalvik/vm/Thread.c中,用來準備主線程的JNI環境,即將在前面的Step 5中爲主線程創建的Thread對象與在前面Step 4中創建的JNI環境關聯起來。回憶在前面的Step 4中,雖然我們已經爲當前線程創建好一個JNI環境了,但是還沒有將該JNI環境與主線程關聯,也就是還沒有將主線程的ID設置到該JNI環境中去。
3. registerSystemNatives
這個函數定義在文件dalvik/vm/Init.c中,它調用另外一個函數jniRegisterSystemMethods,後者接着又調用了函數registerCoreLibrariesJni來爲Java核心類註冊JNI方法。函數registerCoreLibrariesJni定義在文件libcore/luni/src/main/native/Register.cpp中。
4. dvmCreateStockExceptions
這個函數定義在文件dalvik/vm/alloc/Alloc.c中,用來預創建一些與內存分配有關的異常對象,並且將它們緩存起來,以便以後可以快速使用。這些異常對象包括java.lang.OutOfMemoryError、java.lang.InternalError和java.lang.NoClassDefFoundError。
5. dvmPrepMainThread
這個函數定義在文件dalvik/vm/Thread.c中,用來爲主線程創建一個java.lang.ThreadGroup對象、一個java.lang.Thread對角和java.lang.VMThread對象。這些Java對象和在前面Step 5中創建的C++層Thread對象關聯一起,共同用來描述Dalvik虛擬機的主線程。
6. dvmReferenceTableEntries
這個函數定義在文件dalvik/vm/ReferenceTable.h中,用來確保主線程當前不引用有任何Java對象,這是爲了保證主線程接下來以乾淨的方式來執行程序入口。
7. dvmDebuggerStartup
這個函數定義在文件dalvik/vm/Debugger.c中,用來初始化Dalvik虛擬機的調試環境。注意,Dalvik虛擬機與Java虛擬機一樣,都是通過JDWP協議來支持遠程調試的。
上述初始化和檢查操作執行完成之後,我們再來看最後一段代碼:
這段代碼完成Dalvik虛擬機的最後一步初始化工作。它檢查Dalvik虛擬機是否指定了-Xzygote啓動選項。如果指定了的話,那麼就說明當前是在Zygote進程中啓動Dalvik虛擬機,因此,接下來就會調用函數dvmInitZygote來執行最後一步初始化工作。否則的話,就會調用另外一個函數dvmInitAfterZygote來執行最後一步初始化工作。由於當前是在Zygote進程中啓動Dalvik虛擬機的,因此,接下來我們就繼續分析函數dvmInitZygote的實現。在接下來的文章中分析Android應用程序進程的創建過程時,我們再分析函數dvmInitAfterZygote的實現。
Step 6. dvmInitZygote
這個函數定義在文件dalvik/vm/Init.c中。函數dvmInitZygote的實現很簡單,它只是調用了系統調用setpgid來設置當前進程,即Zygote進程的進程組ID。注意,在調用setpgid的時候,傳遞進去的兩個參數均爲0,這意味着Zygote進程的進程組ID與進程ID是相同的,也就是說,Zygote進程運行在一個單獨的進程組裏面。
這一步執行完成之後,Dalvik虛擬機的創建和初始化工作就完成了,回到前面的Step 1中,即AndroidRuntime類的成員函數start中,接下來就會調用AndroidRuntime類的另外一個成員函數startReg來註冊Android核心類的JNI方法。
Step 7. AndroidRuntime.startReg
這個函數定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。AndroidRuntime類的成員函數startReg首先調用函數androidSetCreateThreadFunc來設置一個線程創建鉤子javaCreateThreadEtc。這個線程創建鉤子是用來初始化一個Native線程的JNI環境的,也就是說,當我們在C++代碼中創建一個Native線程的時候,函數javaCreateThreadEtc會被調用來初始化該Native線程的JNI環境。後面在分析Dalvik虛擬機線程的創建過程時,我們再詳細分析函數javaCreateThreadEtc的實現。
AndroidRuntime類的成員函數startReg接着調用函數register_jni_procs來註冊Android核心類的JNI方法。在註冊JNI方法的過程中,需要在Native代碼中引用到一些Java對象,這些Java對象引用需要記錄在當前線程的一個Native堆棧中。但是此時Dalvik虛擬機還沒有真正運行起來,也就是當前線程的Native堆棧還沒有準備就緒。在這種情況下,就需要在註冊JNI方法之前,手動地將在當前線程的Native堆棧中壓入一個幀(Frame),並且在註冊JNI方法之後,手動地將該幀彈出來。
當前線程的JNI環境是由參數env所指向的一個JNIEnv對象來描述的,通過調用它的成員函數PushLocalFrame和PopLocalFrame就可以手動地往當前線程的Native堆棧壓入和彈出一個幀。注意,這個幀是一個本地幀,只可以用來保存Java對象在Native代碼中的本地引用。
函數register_jni_procs的實現如下所示:
這個函數定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。從前面的調用過程可以知道,參數array指向的是全局變量gRegJNI所描述的一個JNI方法註冊函數表,其中,每一個表項都用一個RegJNIRec對象來描述,而每一個RegJNIRec對象都有一個成員變量mProc,指向一個JNI方法註冊函數。通過依次調用這些註冊函數,就可以將Android核心類的JNI方法註冊到前面的所創建的Dalvik虛擬機中去。
通過觀察全局變量gRegJNI所描述的JNI方法註冊函數表,我們就可以看出註冊了哪些Android核心類的JNI方法,如下所示:
上述函數表同樣是定義在文件frameworks/base/core/jni/AndroidRuntime.cpp中。回到AndroidRuntime類的成員函數startReg中,接下來我們就繼續分析函數androidSetCreateThreadFunc的實現,以便可以瞭解線程創建鉤子javaCreateThreadEtc的註冊過程。
Step 8. androidSetCreateThreadFunc
這個函數定義在文件frameworks/base/libs/utils/Threads.cpp中。從這裏就可以看到,線程創建鉤子javaCreateThreadEtc被保存在一個函數指針gCreateThreadFn中。注意,函數指針gCreateThreadFn默認是指向函數androidCreateRawThreadEtc的,也就是說,如果我們不設置線程創建鉤子的話,函數androidCreateRawThreadEtc就是默認使用的線程創建函數。後面在分析Dalvik虛擬機線程的創建過程時,我們再詳細分析函數指針gCreateThreadFn是如何使用的。
至此,我們就分析完成Dalvik虛擬機在Zygote進程中的啓動過程,這個啓動過程主要就是完成了以下四個事情:
1. 創建了一個Dalvik虛擬機實例;
2. 加載了Java核心類及其JNI方法;
3. 爲主線程的設置了一個JNI環境;
4. 註冊了Android核心類的JNI方法。
換句話說,就是Zygote進程爲Android系統準備好了一個Dalvik虛擬機實例,以後Zygote進程在創建Android應用程序進程的時候,就可以將它自身的Dalvik虛擬機實例複製到新創建Android應用程序進程中去,從而加快了Android應用程序進程的啓動過程。此外,Java核心類和Android核心類(位於dex文件中),以及它們的JNI方法(位於so文件中),都是以內存映射的方式來讀取的,因此,Zygote進程在創建Android應用程序進程的時候,除了可以將自身的Dalvik虛擬機實例複製到新創建的Android應用程序進程之外,還可以與新創建的Android應用程序進程共享Java核心類和Android核心類,以及它們的JNI方法,這樣就可以節省內存消耗。
同時,我們也應該看到,Zygote進程爲了加快Android應用程序進程的啓動過程,犧牲了自己的啓動速度,因爲它需要加載大量的Java核心類,以及註冊大量的Android核心類JNI方法。Dalvik虛擬機在加載Java核心類的時候,還需要對它們進行驗證以及優化,這些通常都是比較耗時的。又由於Zygote進程是由init進程啓動的,也就是說Zygote進程在是開機的時候進行啓動的,因此,Zygote進程的犧牲是比較大的。不過畢竟我們在玩手機的時候,很少會關機,也就是很少開機,因此,犧牲Zygote進程的啓動速度是值得的,換來的是Android應用程序的快速啓動。而且,Android系統爲了加快Java類的加載速度,還會想方設法地提前對Dex文件進行驗證和優化,這些措施具體參考Dalvik Optimization and Verification With dexopt一文。
學習了Dalvik虛擬機的啓動過程之後,在接下來的一篇文章中,我們就繼續分析Dalvik虛擬機的運行機制,敬請關注!
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!