本文會介紹Andorid系統上曾經使用過的Dalvik虛拟機。後面還會有一篇文章講解Android系統上現在使用的虛拟機:ART。
另外,我的部落格裡有一篇關于Java虛拟機的預習文章也可以看一看:
Java虛拟機與垃圾回收算法也許有人會問,既然Dalvik虛拟機都已經被廢棄了,為什麼我們還要了解它呢?出于下面的原因,讓我覺得還是有必要了解一下Dalvik虛拟機的:
- Dalvik留下的很多機制在現在的Android系統是一樣适用的,例如Dalvik指令,dex檔案
- 并非每個人都是在最新版本的Android系統上工作
- 了解一項技術曾經的曆史和演進過程,有助于增加對于現在狀态的了解
Dalvik是Google專門為Android作業系統開發的虛拟機。它支援.dex(即“Dalvik Executable”)格式的Java應用程式的運作。.dex格式是專為Dalvik設計的一種壓縮格式,适合記憶體和處理器速度有限的系統。
Dalvik由Dan Bornstein編寫,名字來源于他的祖先曾經居住過的小漁村達爾維克(Dalvík),位于冰島。
棧 VS 寄存器
大多數虛拟機都是基于堆棧架構的,例如前面提到的HotSpot JVM。然而Dalvik虛拟機卻恰好不是,它是基于寄存器架構的虛拟機。
對于基于棧的虛拟機來說,每一個運作時的線程,都有一個獨立的棧。棧中記錄了方法調用的曆史,每有一次方法調用,棧中便會多一個棧桢。最頂部的棧桢稱作目前棧桢,其代表着目前執行的方法。棧桢中通常包含四個資訊:
- 局部變量:方法參數和方法中定義的局部變量
- 操作數棧:後入先出的棧
- 動态連接配接:指向運作時常量池該棧桢所屬方法的引用
- 傳回位址:目前方法的傳回位址
棧幀的結構如下圖所示:

基于堆棧架構的虛拟機的執行過程,就是不斷在操作數棧上操作的過程。例如,對于計算“1+1”的結果這樣一個計算,基于棧的虛拟機需要先将這兩個數壓入棧,然後通過一條指針對棧頂的兩個數字進行加法運算,然後再将結果存儲起來。其指令集會是這樣子:
iconst_1
iconst_1
iadd
istore_0
而對于基于寄存器的虛拟機來說執行過程是完全不一樣的。該類型虛拟機會将運算的參數放至寄存器中,然後在寄存器上直接進行運算。是以如果是基于寄存器的虛拟機,其指令可能會是這個樣子:
mov eax,1
add eax,1
這兩種架構哪種更好呢?
很顯然,既然它們同時存在,那就意味着它們各有優劣,假設其中一種明顯優于另外一種,那劣勢的那一種便就不會存在了。
如果我們對這兩種架構進行對比,我們會發現它們存在如下的差別:
- 基于棧的架構具有更好的可移植性,因為其實作不依賴于實體寄存器
- 基于棧的架構通常指令更短,因為其操作不需要指定操作數和結果的位址
- 基于寄存器的架構通常運作速度更快,因為有寄存器的支撐
- 基于寄存器的架構通常需要較少的指令來完成同樣的運算,因為不需要進行壓棧和出棧
dex檔案
如果我們對比jar檔案和dex檔案,就會發現:dex檔案格式相對來說更加的緊湊。
jar檔案以class為區域進行劃分,在連續的class區域中會包含每個class中的常量,方法,字段等等。而dex檔案按照類型(例如:常量,字段,方法)劃分,将同一類型的元素集中到一起進行存放。這樣可以更大程度上避免重複,減少檔案大小。
兩種檔案格式的對比如下圖所示:
dex檔案的完整格式參見這裡:
Dalvik 可執行檔案格式。
由于Dex檔案相較于Jar來說,對同一類型的元素進行了規整,并且去掉了重複項。是以通常情況下,對于同樣的内容,前者比後者檔案要更小。以下是Google給出的資料,從這個對比資料可以看出,兩者的差距還是很大的。
内容 | 未壓縮jar包 | 已壓縮jar包 | 未壓縮dex檔案 |
---|---|---|---|
系統庫 | 100% | 50% | 48% |
Web浏覽器 | 49% | 44% | |
鬧鐘應用 | 52% |
為了便于開發者分析dex檔案中的内容,Android系統中内置了
dexdump
工具。借助這個工具,我們可以詳細了解到dex的檔案結構和内容。以下是這個工具的幫助文檔。在接下來的内容中,我們将借這個工具來反編譯出dex檔案中的Dalvik指令。
angler:/ # dexdump
dexdump: no file specified
Copyright (C) 2007 The Android Open Source Project
dexdump: [-c] [-d] [-f] [-h] [-i] [-l layout] [-m] [-t tempfile] dexfile...
-c : verify checksum and exit
-d : disassemble code sections
-f : display summary information from file header
-h : display file header details
-i : ignore checksum failures
-l : output layout, either 'plain' or 'xml'
-m : dump register maps (and nothing else)
-t : temp file name (defaults to /sdcard/dex-temp-*)
Dalvik指令
Dalvik虛拟機一共包含兩百多條指令。讀者可以通路下面這個網址擷取這些指令的詳細資訊:
Dalvik 位元組碼我們這裡不會對每條指令做詳細講解,建議讀者大緻浏覽一下上面這個網頁。
下面以一個簡單的例子來讓讀者對Dalvik指令有一個直覺的認識。
下面是一個Activity的源碼,在這個Activity中,我們定義了一個sum方法,進行兩個整數的相加。然後在Activity的
onCreate
方法中,在
setContentView
之後,調用這個
sum
方法并傳遞1和2,然後再将結果通過
System.out.print
進行輸出。這段代碼很簡單,簡單到幾乎沒有什麼實際的作用,不過這不要緊,因為這裡我們的目的僅僅想看一下我們編寫的源碼最終得到的Dalvik指令究竟是什麼樣的。
package test.android.com.helloandroid;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
int sum(int a, int b) {
return a + b;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
System.out.print(sum(1,2));
}
}
将這個工程編譯之後獲得了APK檔案。APK檔案其實是一種壓縮格式,我們可以使用任何可以解壓Zip格式的軟體對其解壓縮。解壓縮之後的檔案清單如下所示:
├── AndroidManifest.xml
├── META-INF
│ ├── CERT.RSA
│ ├── CERT.SF
│ └── MANIFEST.MF
├── classes.dex
├── res
│ ├── layout
│ │ └── activity_main.xml
│ ├── mipmap-hdpi-v4
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ ├── mipmap-mdpi-v4
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ ├── mipmap-xhdpi-v4
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi-v4
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ └── mipmap-xxxhdpi-v4
│ ├── ic_launcher.png
│ └── ic_launcher_round.png
└── resources.arsc
其他的檔案不用在意,這裡我們隻要關注dex檔案即可。我們可以通過
adb push
指令将classes.dex檔案拷貝到手機上,然後通過手機上的
dexdump
指令來進行分析。
直接輸入
dexdump classes.dex
會得到一個非常長的輸出。下面是其中的一個片段:
...
Class #40 -
Class descriptor : 'Ltest/android/com/helloandroid/MainActivity;'
Access flags : 0x0001 (PUBLIC)
Superclass : 'Landroid/app/Activity;'
Interfaces -
Static fields -
Instance fields -
Direct methods -
#0 : (in Ltest/android/com/helloandroid/MainActivity;)
name : '<init>'
type : '()V'
access : 0x10001 (PUBLIC CONSTRUCTOR)
code -
registers : 1
ins : 1
outs : 1
insns size : 4 16-bit code units
catches : (none)
positions :
0x0000 line=6
locals :
0x0000 - 0x0004 reg=0 this Ltest/android/com/helloandroid/MainActivity;
Virtual methods -
#0 : (in Ltest/android/com/helloandroid/MainActivity;)
name : 'onCreate'
type : '(Landroid/os/Bundle;)V'
access : 0x0004 (PROTECTED)
code -
registers : 5
ins : 2
outs : 3
insns size : 20 16-bit code units
catches : (none)
positions :
0x0000 line=14
0x0003 line=15
0x0008 line=17
0x0013 line=18
locals :
0x0000 - 0x0014 reg=3 this Ltest/android/com/helloandroid/MainActivity;
0x0000 - 0x0014 reg=4 savedInstanceState Landroid/os/Bundle;
#1 : (in Ltest/android/com/helloandroid/MainActivity;)
name : 'sum'
type : '(II)I'
access : 0x0000 ()
code -
registers : 4
ins : 3
outs : 0
insns size : 3 16-bit code units
catches : (none)
positions :
0x0000 line=9
locals :
0x0000 - 0x0003 reg=1 this Ltest/android/com/helloandroid/MainActivity;
0x0000 - 0x0003 reg=2 a I
0x0000 - 0x0003 reg=3 b I
source_file_idx : 455 (MainActivity.java)
...
從這個片段中,我們看到了剛剛編寫的MainActivity類的詳細資訊。包括每一個方法的名稱,簽名,通路級别,使用的寄存器等資訊。
接下來,我們通過
dexdump -d classes.dex
來反編譯代碼段,以檢視方法實作邏輯所對應的Dalvik指令。
通過這個指令,我們得到sum方法的指令如下:
[019f98] test.android.com.helloandroid.MainActivity.sum:(II)I
0000: add-int v0, v2, v3
為了看懂
add-int
指令的含義,我們可以查閱Dalvik指令的說明文檔:
Mnemonic / Syntax | Arguments | Description |
---|---|---|
binop vAA, vBB, vCC 90: add-int | A: destination register or pair (8 bits) B: first source register or pair (8 bits) C: second source register or pair (8 bits) | Perform the identified binary operation on the two source registers, storing the result in the destination register. |
這段說明文檔的含義是:
add-int
是一個需要兩個操作數的指令,其指令格式是:
add-int vAA, vBB, vCC
。其指令的運算過程,是将後面兩個寄存器中的值進行(加)運算,然後将結果放在(第一個)目标寄存器中。
很顯然,對應到
add-int v0, v2, v3
就是将v2和v3兩個寄存器的值相加,并将結果存儲到v0寄存器上。這正好是對應了我們所寫的代碼:
return a + b;
下面,我們再看一下稍微複雜一點的
onCreate
方法其對應的Dalvik指令:
[019f60] test.android.com.helloandroid.MainActivity.onCreate:(Landroid/os/Bundle;)V
0000: invoke-super {v3, v4}, Landroid/app/Activity;.onCreate:(Landroid/os/Bundle;)V // method@0001
0003: const/high16 v0, #int 2130903040 // #7f03
0005: invoke-virtual {v3, v0}, Ltest/android/com/helloandroid/MainActivity;.setContentView:(I)V // method@0318
0008: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@02e0
000a: const/4 v1, #int 1 // #1
000b: const/4 v2, #int 2 // #2
000c: invoke-virtual {v3, v1, v2}, Ltest/android/com/helloandroid/MainActivity;.sum:(II)I // method@0319
000f: move-result v1
0010: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.print:(I)V // method@02d7
0013: return-void
同樣,通過查閱指令的說明文檔,我們可以知道這裡牽涉到的幾條指令含義如下:
-
: 調用父類中的方法invoke-super
-
: 将指定的字面值的高16位拷貝到指定的寄存器中,這是一個16bit的操作const/high16
-
: 調用一個virtual方法invoke-virtual
-
: 擷取類中static字段的對象,并存放到指定的寄存器上sget-object
-
: 将指定的字面值拷貝到指定的寄存器中,這是一個32bit的操作const/4
-
: 該指令緊接着invoke-xxx指令,将上一條指令的結果移動到指定的寄存器中move-result
-
: void方法傳回return-void
由此,我們便能看懂這段指令的含義了。甚至我們已經具備了閱讀任何Dalvik代碼的能力,因為無非就是明白每個指令的含義罷了。
單純的閱讀指令的說明文檔可能很枯燥,也不容易記住。建議讀者繼續寫一些複雜的代碼然後通過反編譯方式檢視其對應的虛拟機指令來進行學習。或者對已有的項目進行反編譯來檢視其機器指令。也許一些讀者覺得,開發者根本不必去閱讀這些原本就不準備給人類閱讀的機器指令。但實際上,對于底層指令越是熟悉,對底層機制越是了解,往往能讓我們寫出越是高效的程式來,因為一旦我們深刻了解機制背後的運作原理,就可以避過或者減少一些不必要的重複運算。再者,具備對于底層指令的了解能力,也為我們分析解決一些從源碼層無法分析的問題提供了一個新的手段。
最後筆者想提醒一下,即便在ART虛拟機時代,這裡學習的Dalvik指令和反編譯手段仍然是沒有過時的。因為這種分析方式是依然可用的。這也是為什麼我們要講解Dalvik虛拟機的原因。
Dalvik啟動過程
注:自Android 5.0開始,Dalvik虛拟機已經被廢棄,其源碼也已經被從AOSP中删除。是以想要檢視其源碼,需要擷取Android 4.4或之前版本的代碼。本小節接下來貼出的源碼取自AOSP代碼TAG android-4.4_r1。
Dalvik虛拟機的源碼位于下面這個目錄中:
/dalvik/vm/
在其他的文章(
這裡,
還有這裡)中,我們講解了系統的啟動過程,并且也介紹了zygote程序。我們提到zygote程序會啟動虛拟機,但是卻沒有深入了解過虛拟機是如何啟動的,而這正是本文接下來要講解的内容。
zygote程序是由app_process啟動的,我們來回顧一下app_process
main
函數中的關鍵代碼:
// app_process.cpp
int main(int argc, char* const argv[])
{
...
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.\n");
app_usage();
LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
return 10;
}
}
這裡通過
runtime.start
方法指定入口類啟動了虛拟機。虛拟機在啟動之後,會以入口類的main函數為起點來執行。
runtime
是
AppRuntime
類的對象,
start
方法是在
AppRuntime
類的父類
AndroidRuntime
中定義的方法。該方法中的關鍵代碼如下:
// AndroidRuntime.cpp
void AndroidRuntime::start(const char* className, const char* options)
{
...
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env) != 0) { ①
return;
}
onVmCreated(env);
/*
* Register android functions.
*/
if (startReg(env) < 0) { ②
ALOGE("Unable to register all android natives\n");
return;
}
...
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName); ③
if (startClass == NULL) {
ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
"([Ljava/lang/String;)V");
if (startMeth == NULL) {
ALOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray); ④
#if 0
if (env->ExceptionCheck())
threadExitUncaughtException(env);
#endif
}
}
free(slashClassName);
ALOGD("Shutting down VM\n"); ⑤
if (mJavaVM->DetachCurrentThread() != JNI_OK)
ALOGW("Warning: unable to detach main thread\n");
if (mJavaVM->DestroyJavaVM() != 0)
ALOGW("Warning: VM did not shut down cleanly\n");
}
這段代碼主要邏輯如下:
- 通過
方法啟動虛拟機startVm
-
方法注冊Android Framework類相關的JNI方法startReg
- 查找入口類的定義
- 調用入口類的
方法main
- 處理虛拟機退出前執行的邏輯
接下來我們先看
startVm
方法的實作,然後再看
startReg
方法。
AndroidRuntime::startVm
方法有三百多行代碼。但其邏輯卻很簡單,因為這個方法中的絕大部分代碼都是在确定虛拟機的啟動參數的值。這些值主要來自于許多的系統屬性,這個方法中讀取的屬性以及這些屬性的含義如下表所示:
屬性名稱 | 屬性的含義 |
---|---|
dalvik.vm.checkjni | 是否要執行擴充的JNI檢查,CheckJNI是一種添加額外JNI檢查的模式;出于性能考慮,這些選項在預設情況下并不會啟用。此類檢查将捕獲一些可能導緻堆損壞的錯誤,例如使用無效/過時的局部和全局引用。如果這個值為false,則讀取ro.kernel.android.checkjni的值 |
ro.kernel.android.checkjni | 隻讀屬性,是否要執行擴充的JNI檢查。當dalvik.vm.checkjni為false,此值才生效 |
dalvik.vm.execution-mode | Dalvik虛拟機的執行模式,即:所使用的解釋器,下文會講解 |
dalvik.vm.stack-trace-file | 指定堆棧跟蹤檔案路徑 |
dalvik.vm.check-dex-sum | 是否要檢查dex檔案的校驗和 |
log.redirect-stdio | 是否将stdout/stderr轉換成log消息 |
dalvik.vm.enableassertions | 是否啟用斷言 |
dalvik.vm.jniopts | JNI可選配置 |
dalvik.vm.heapstartsize | 堆的起始大小 |
dalvik.vm.heapsize | 堆的大小 |
dalvik.vm.jit.codecachesize | JIT代碼緩存大小 |
dalvik.vm.heapgrowthlimit | 堆增長的限制 |
dalvik.vm.heapminfree | 堆的最小剩餘空間 |
dalvik.vm.heapmaxfree | 堆的最大剩餘空間 |
dalvik.vm.heaptargetutilization | 理想的堆記憶體使用率,其取值位于0與1之間 |
ro.config.low_ram | 該裝置是否是低記憶體裝置 |
dalvik.vm.dexopt-flags | 是否要啟用dexopt特性,例如位元組碼校驗以及為精确GC計算寄存器映射 |
dalvik.vm.lockprof.threshold | 控制Dalvik虛拟機調試記錄程式内部鎖資源争奪的門檻值 |
dalvik.vm.jit.op | 對于指定的操作碼強制使用解釋模式 |
dalvik.vm.jit.method | 對于指定的方法強制使用解釋模式 |
dalvik.vm.extra-opts | 其他選項 |
注:Android系統中很多服務都有類似的做法,即:通過屬性的方式将子產品的配置參數外化。這樣外部隻要設定屬性值即可以改變這些子產品的内部行為。
這些屬性的值會被讀取并最終會被組裝到
initArgs
中,并以此傳遞給
JNI_CreateJavaVM
函數來啟動虛拟機:
// AndroidRuntime.cpp
if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
ALOGE("JNI_CreateJavaVM failed\n");
goto bail;
}
JNI_CreateJavaVM
函數是虛拟機實作的一部分,是以該方法代碼已經位于Dalvik中。具體的在這個檔案中:/dalvik/vm/Jni.pp。
JNI_CreateJavaVM
方法中的關鍵代碼如下所示:
// Jni.cpp
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
const JavaVMInitArgs* args = (JavaVMInitArgs*) vm_args;
...
memset(&gDvm, 0, sizeof(gDvm));
JavaVMExt* pVM = (JavaVMExt*) calloc(1, sizeof(JavaVMExt));
pVM->funcTable = &gInvokeInterface;
pVM->envList = NULL;
dvmInitMutex(&pVM->envListLock);
UniquePtr<const char*[]> argv(new const char*[args->nOptions]);
memset(argv.get(), 0, sizeof(char*) * (args->nOptions));
...
JNIEnvExt* pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);
gDvm.initializing = true;
std::string status =
dvmStartup(argc, argv.get(), args->ignoreUnrecognized, (JNIEnv*)pEnv);
gDvm.initializing = false;
...
dvmChangeStatus(NULL, THREAD_NATIVE);
*p_env = (JNIEnv*) pEnv;
*p_vm = (JavaVM*) pVM;
ALOGV("CreateJavaVM succeeded");
return JNI_OK;
}
在這個函數中,會讀取啟動的參數值,并将這些值設定到兩個全局變量中,它們是:
// Init.cpp
struct DvmGlobals gDvm;
struct DvmJniGlobals gDvmJni;
DvmGlobals
這個結構體的定義非常之大,總計有約700行,其中存儲了Dalvik虛拟機相關的全局屬性,這些屬性在虛拟機運作過程中會被用到。而
gDvmJni
中則記錄了Jni相關的屬性。
JNI_CreateJavaVM
函數中最關鍵的就是調用
dvmStartup
函數。很顯然,這個函數的含義是:Dalvik Startup。是以這個函數負責了Dalvik虛拟機的初始化工作,由于虛拟機本身也是有很多子子產品群組件構成的,是以這個函數中調用了一系列的初始化方法來完成整個虛拟機的初始化工作,這其中包含:虛拟機堆的建立,記憶體配置設定跟蹤器的建立,線程的啟動,基本核心類加載等一系列工作,在這之後整個虛拟機就啟動完成了。
這些方法是與Dalvik的實作細節緊密相關的,這裡我們就不深入了,有興趣的讀者可以自行去學習。
虛拟機啟動完成之後就可以用了。但對于Android系統來說,還有一些工作要做,那就是Android Framework相關類的JNI方法注冊。我們知道,Android Framework主要是Java語言實作的,但其中很多類都需要依賴于native實作,是以需要通過JNI将兩種實作銜接起來。例如,在第1章我們講解Binder機制中的Parcel類就是既有Java層接口也有native層的實作。除了Parcel類,還有其他類也是類似的。并且,Framework中的類是幾乎每個應用程式都可能會被用到的,為了減少每個應用程度單獨加載的邏輯,是以虛拟機在啟動之後直接就将這些類的JNI方法全部注冊到虛拟機中了。完成這個邏輯的便是上面我們看到的
startReg
方法:
/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
這個函數是在注冊所有Android Framework中類的JNI方法,在AndroidRuntime類中,通過
gRegJNI
這個全局組數進行了記錄了這些資訊。這個數組包含了一百多個條目,下面是其中的一部分:
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_debug_JNITest),
REG_JNI(register_com_android_internal_os_RuntimeInit),
REG_JNI(register_android_os_SystemClock),
REG_JNI(register_android_util_EventLog),
REG_JNI(register_android_util_Log),
REG_JNI(register_android_util_FloatMath),
REG_JNI(register_android_text_format_Time),
REG_JNI(register_android_content_AssetManager),
REG_JNI(register_android_content_StringBlock),
REG_JNI(register_android_content_XmlBlock),
REG_JNI(register_android_emoji_EmojiFactory),
REG_JNI(register_android_text_AndroidCharacter),
REG_JNI(register_android_text_AndroidBidi),
REG_JNI(register_android_view_InputDevice),
REG_JNI(register_android_view_KeyCharacterMap),
REG_JNI(register_android_os_Process),
REG_JNI(register_android_os_SystemProperties),
REG_JNI(register_android_os_Binder),
REG_JNI(register_android_os_Parcel),
...
};
這個數組中的每一項包含了一個函數,每個函數由Framework中對應的類提供,負責該類的JNI函數注冊。這其中就包含我們在第二章提到的Binder和Parcel。
我們以Parcel為例來看一下:
register_android_os_Parcel
函數由android_os_Parcel.cpp提供,代碼如下:
int register_android_os_Parcel(JNIEnv* env)
{
jclass clazz;
clazz = env->FindClass(kParcelPathName);
LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.Parcel");
gParcelOffsets.clazz = (jclass) env->NewGlobalRef(clazz);
gParcelOffsets.mNativePtr = env->GetFieldID(clazz, "mNativePtr", "I");
gParcelOffsets.obtain = env->GetStaticMethodID(clazz, "obtain",
"()Landroid/os/Parcel;");
gParcelOffsets.recycle = env->GetMethodID(clazz, "recycle", "()V");
return AndroidRuntime::registerNativeMethods(
env, kParcelPathName,
gParcelMethods, NELEM(gParcelMethods));
}
這段代碼的最後是調用
AndroidRuntime::registerNativeMethods
對每個JNI方法進行注冊,
gParcelMethods
包含了Parcel類中的所有JNI方法清單,下面是其中一部分:
static const JNINativeMethod gParcelMethods[] = {
{"nativeDataSize", "(I)I", (void*)android_os_Parcel_dataSize},
{"nativeDataAvail", "(I)I", (void*)android_os_Parcel_dataAvail},
{"nativeDataPosition", "(I)I", (void*)android_os_Parcel_dataPosition},
{"nativeDataCapacity", "(I)I", (void*)android_os_Parcel_dataCapacity},
{"nativeSetDataSize", "(II)V", (void*)android_os_Parcel_setDataSize},
{"nativeSetDataPosition", "(II)V", (void*)android_os_Parcel_setDataPosition},
{"nativeSetDataCapacity", "(II)V", (void*)android_os_Parcel_setDataCapacity},
...
}
總結起來這裡的邏輯就是:
- Android Framework中每個包含了JNI方法的類負責提供一個
方法,這個方法負責該類中所有JNI方法的注冊register_xxx
- 類中的所有JNI方法通過一個二維數組記錄
-
中羅列了所有Framework層類提供的gRegJNI
函數指針,并以此指針來完成調用,以使得整個JNI注冊過程完成register_xxx
至此,Dalvik虛拟機的啟動過程我們就講解完了,下圖描述了完整的Dalvik虛拟機啟動過程:
程式的執行:解釋與編譯
程式員通過源碼的形式編寫程式,而機器隻能認識機器碼。從編寫完的程式到在機器上運作,中間必須經過一個轉換的過程。這個轉換的過程由兩種做法,那就是:解釋和編譯。
- 解釋是指:源程式由程式解釋器邊掃描邊翻譯執行,這種方式不會産生目标檔案,是以如果程式執行多次就需要重複解釋多次。
- 編譯是指:通過編譯器将源程式完整的地翻譯成用機器語言表示的與之等價的目标程式。是以,這種方式隻要編譯一次,得到的産物可以反複執行。
許多腳本語言,例如JavaScript用的就是解釋方式,是以其開發的過程中不牽涉到任何編譯的步驟(注意,這裡僅僅是指程式員的開發階段,在虛拟機的内部解釋過程中,仍然會有編譯的過程,隻不過對程式員隐藏了)。而對于C/C++這類靜态編譯語言來說,在寫完程式之後到真正運作之前,必須經由編譯器将程式編譯成機器對應的機器碼。
正如前面說過的觀點那樣:既然一個問題還存在兩種解決方法,那麼它們自然各有優勢。
解釋性語言通常都具有的一個優點就是跨平台:因為這些語言由解釋器承擔了不同平台上的相容工作,而開發者不用關心這一點。相反,編譯性語言的編譯産物是與平台向對應的,Windows上編譯出來的C++可執行檔案(不使用交叉編譯工具鍊)不能在Linux或者Mac運作。但反過來,解釋性語言的缺點就是運作效率較慢,因為有很多編譯的動作延遲到運作時來執行了,這就必要導緻運作時間較長。
而Java語言介于完全解釋和靜态編譯兩者之間。因為無論是JVM上的class檔案還是Dalvik上的dex檔案,這些檔案是已經經過詞法和文法分析的中間産物。但這個産物與C/C++語言所對應的編譯産物還不一樣,因為Java語言的編譯産物隻是一個中間産物,并沒有完全對應到機器碼。在運作時,還需要虛拟機進行解釋執行或者進一步的編譯。
有些Java的虛拟機隻包含解釋器,有些隻包含編譯器。而在Dalvik在最早期的版本中,隻包含了解釋器,從Android 2.2版本開始,包含了JIT編譯器。
下圖描述了解釋和編譯的流程:
Dalvik上的解釋器
解釋器正如其名稱那樣:負責程式的解釋執行。在Dalvik中,内置了三個解析器,分别是:
- fast: 預設解釋器。這個解釋器專門為平台優化過,因為其中包含了手寫的彙編代碼
- portable: 顧名思義,具有較好可移植性的解釋器,因為這個解釋器是用C語言實作的
- debug: 專門為debug和profile所用的解析器,性能較弱
使用者可以通過設定屬性來選擇解釋器,例如下面這條指令設定解釋器為portable:
adb shell "echo dalvik.vm.execution-mode = int:portable >> /data/local.prop"
前面我們已經看到,Dalvik虛拟機在啟動的時候會讀取這個屬性,是以當你修改了這個屬性之後,需要重新啟動才能使之生效。
Dalvik解釋器的源碼位于這個路徑:
/dalvik/vm/mterp
portable是最先實作的解釋器,這個解釋器以單個C語言函數的形式實作的。但是為了改進性能,Google後來使用彙編語言重寫了,這也就是fast解釋器。為了使得這些彙程式設計式更容易移植,解釋器的實作采用了子產品化的方法:這使得允許每次開發特定平台上的特定操作碼。
每個配置都有一個“config-*”檔案來控制來源代碼的生成。源代碼被寫入
/dalvik/vm/mterp/out
目錄,Android編譯系統會讀取這裡的檔案。
熟悉解釋器的最好方法就是看翻譯生成的檔案在“out”目錄下的檔案。
關于這部分内容我們就不深入展開了,有興趣的讀者可以自定閱讀這部分代碼。
Dalvik上的JIT
Java虛拟機的引入是将傳統靜态編譯的過程進行了分解:首先編譯出一個中間産物(無論是JVM的class檔案格式還是Android的dex檔案格式),這個中間産物是平台無關的。而在真正運作這個中間産物的時候,再由解釋器将其翻譯成具體裝置上的機器碼然後執行。
而虛拟機上的解釋器通常隻對運作到的代碼進行機器碼翻譯。這樣做效率就很低,因為有些代碼可能要重複執行很多遍(例如日志輸出),但每遍都要重新翻譯。
而JIT就是為了解決這個問題而産生的,JIT在運作時進行代碼的編譯,這樣下次再次執行同樣的代碼的時候,就不用再次解釋翻譯了,而是可以直接使用編譯後的結果,這樣就加快了執行的速度。但它并非編譯所有代碼,而是有選擇性的進行編譯,并且這個“選擇性”是JIT編譯器尤其需要考慮的。因為編譯是一個非常耗時的事情,對于那些運作較少的“冷門”代碼進行編譯可能會适得其反。
總的來說,JIT在選擇哪些代碼進行編譯時,有兩種做法:
- Method JIT
- Trace JIT
第一種是以Java方法為機關進行編譯。第二種是以代碼行為機關進行編譯。考慮到移動裝置上記憶體較小(編譯的過程需要消耗記憶體),是以Dalvik上的JIT以後一種做法為主。
實際上,對于JIT來說,最重要還是需要确定哪些代碼是“熱門”代碼并需要編譯,解決這個問題的做法如下圖所示:
這個過程描述如下:
- 首先需要記錄代碼的執行次數
- 并設定一個“熱門”代碼的門檻值,每次執行時都比對一下看看有沒有到門檻值
- 如果沒有,則還是繼續用解釋的方式執行
- 如果到了門檻值,則檢查該代碼是否存在已經編譯好的産物
- 如果有編譯好的産物直接使用
- 如果沒有編譯好的産物,則發送編譯的請求
- 虛拟機需要對已經編譯好的機器碼進行緩存
Dalvik上的垃圾回收
垃圾回收是Java虛拟機最為重要的一個特性。垃圾回收使得程式員不用再關心對象的釋放問題,極大的簡化了開發的過程。在前面的内容中,我們已經介紹了主要的垃圾回收算法。這裡我們來具體看一下Dalvik虛拟機上的垃圾回收。
Davlik上的垃圾回收主要是在下面的這些時機會觸發:
- 堆中無法再建立對象的時候
- 堆中的記憶體使用率超過門檻值的時候
- 程式通過
主動GC的時候Runtime.gc()
- 在OOM發生之前的時候
不同時機下GC的政策是有差別的,在Heap.h中定義了這四種GC的政策:
// Heap.h
/* Not enough space for an "ordinary" Object to be allocated. */
extern const GcSpec *GC_FOR_MALLOC;
/* Automatic GC triggered by exceeding a heap occupancy threshold. */
extern const GcSpec *GC_CONCURRENT;
/* Explicit GC via Runtime.gc(), VMRuntime.gc(), or SIGUSR1. */
extern const GcSpec *GC_EXPLICIT;
/* Final attempt to reclaim memory before throwing an OOM. */
extern const GcSpec *GC_BEFORE_OOM;
不同的垃圾回收政策會有一些不同的特性,例如:是否隻清理應用程式的堆,還是連Zygote的堆也要清理;該垃圾回收算法是否是并行執行的;是否需要對軟引用進行處理等。
Dalvik的垃圾回收算法在下面這個檔案中實作:
/dalvik/vm/alloc/MarkSweep.h
/dalvik/vm/alloc/MarkSweep.cpp
從檔案名稱上我們就能看得出,Dalvik使用的是标記清除的垃圾回收算法。
Heap.cpp中的
dvmCollectGarbageInternal
函數控制了整個垃圾回收過程,其主要過程如下圖所示:
在垃圾收集過程中,Dalvik使用的是對象追蹤方法,這其中的詳細步驟說明如下:
- 在開始垃圾回收之前,要暫停所有線程的執行:
;dvmSuspendAllThreads(SUSPEND_FOR_GC)
- 建立GC标記的上下文:
dvmHeapBeginMarkStep
- 對GC的根對象進行标記:
dvmHeapMarkRootSet
- 然後以此為起點進行對象的追蹤:
dvmHeapScanMarkedObjects
- 處理引用關系:
dvmHeapProcessReferences
- 執行清理:
-
dvmHeapSweepSystemWeaks
-
dvmHeapSourceSwapBitmaps
-
dvmHeapSweepUnmarkedObjects
-
- 完成标記工作:
dvmHeapFinishMarkStep
- 恢複所有線程的執行:
dvmResumeAllThreads
dvmHeapSweepUnmarkedObjects
函數會調用
sweepBitmapCallback
來清理對象,這個函數的代碼如下所示:
// MarkSweep.cpp
static void sweepBitmapCallback(size_t numPtrs, void **ptrs, void *arg)
{
assert(arg != NULL);
SweepContext *ctx = (SweepContext *)arg;
if (ctx->isConcurrent) {
dvmLockHeap();
}
ctx->numBytes += dvmHeapSourceFreeList(numPtrs, ptrs);
ctx->numObjects += numPtrs;
if (ctx->isConcurrent) {
dvmUnlockHeap();
}
}
在
一文中,我們講過:垃圾回收清理完對象之後會遺留下記憶體碎片,是以虛拟機還需要對碎片進行整理。在Dalvik虛拟機中,是直接利用了底層記憶體管理庫完成這項工作。Dalvik的記憶體管理是基于dlmalloc實作的,這是由Doug Lea實作的記憶體配置設定器。而Dalvik的記憶體整理是直接利用了dlmalloc中的
mspace_bulk_free
函數進行了處理。讀者可以在這裡了解
dlmalloc看到Dalvik垃圾回收算法的讀者應該能夠發現,Dalvik虛拟機上的垃圾回收有一個很嚴重的問題,那就是在進行垃圾回收的時候,會暫停所有線程。而這個在程式執行過程中幾乎是不能容忍的,這個暫停會造成應用程式的卡頓,并且這個卡頓會伴随着每次垃圾回收而存在。這也是為什麼早期Android系統給大家的感受就是:很卡。這也是Google要用新的虛拟機來徹底替代Dalvik的原因之一。
在下一篇文章中,我們會講解Android系統上新的虛拟機:ART,也會看到它是如何解決垃圾回收的卡頓問題的。