在前面一篇文章中,我們分析了Dalvik虛擬機在Zygote進程中的啓動過程。Dalvik虛擬機啓動完成之後,也就是在各個子模塊初始化完成以及加載了相應的Java核心類庫之後,就是可以執行Java代碼了。當然,Dalvik虛擬機除了可以執行Java代碼之外,還可以執行Native代碼,也就是C和C++代碼。在本文中,我們就將繼續以Zygote進程的啓動過程爲例,來分析Dalvik虛擬機的運行過程。
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!
從前面Dalvik虛擬機的啓動過程分析一文可以知道,Dalvik虛擬機在Zygote進程中啓動完成之後,就會獲得一個JavaVM實例和一個JNIEnv實例。其中,獲得的JavaVM實例就是用來描述Zygote進程的Dalvik虛擬機實例,而獲得的JNIEnv實例描述的是Zygote進程的主線程的JNI環境。緊接着,Zygote進程就會通過前面獲得的JNIEnv實例的成員函數CallStaticVoidMethod來調用com.android.internal.os.ZygoteInit類的靜態成員函數main。這就相當於是將com.android.internal.os.ZygoteInit類的靜態成員函數main作爲Java代碼的入口點。
接下來,我們就從JNIEnv類的成員函數CallStaticVoidMethod開始,分析Dalvik虛擬機的運行過程,如圖1所示:
圖1 Dalvik虛擬機的運行過程
這個過程可以分爲9個步驟,接下來我們就詳細分析每一個步驟。
Step 1. JNIEnv.CallStaticVoidMethod
struct _JNIEnv; ...... typedef _JNIEnv JNIEnv; ...... struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions; ...... void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...) { va_list args; va_start(args, methodID); functions->CallStaticVoidMethodV(this, clazz, methodID, args); va_end(args); } ...... };
這個函數定義在文件dalvik/libnativehelper/include/nativehelper/jni.h中。
JNIEnv實際上是一個結構,它有一個成員變量functions,指向的是一個回調函數表。這個回調函數表使用一個JNINativeInterface對象來描述。JNIEnv結構體的成員函數CallStaticVoidMethod的實現很簡單,它只是調用該回調函數表中的CallStaticVoidMethodV函數來執行參數clazz和methodID所描述的Java代碼。
Step 2. JNINativeInterface.CallStaticVoidMethodV
struct JNINativeInterface { ...... void (*CallStaticVoidMethodV)(JNIEnv*, jclass, jmethodID, va_list); ...... };
這個函數定義在文件dalvik/libnativehelper/include/nativehelper/jni.h中。
JNINativeInterface是一個結構體,它的成員變量CallStaticVoidMethodV是一個函數指針。
從前面Dalvik虛擬機的啓動過程分析一文可以知道,Dalvik虛擬機在內部爲Zygote進程的主線程所創建的Java環境是用一個JNIEnvExt結構體來描述的,並且這個JNIEnvExt結構體會被強制轉換成一個JNIEnv結構體返回給Zygote進程。
JNIEnvExt結構體定義在文件dalvik/vm/JniInternal.h中,如下所示:
typedef struct JNIEnvExt { const struct JNINativeInterface* funcTable; /* must be first */ ...... } JNIEnvExt;
從這裏就可以看出,雖然結構體JNIEnvExt和JNIEnv之間沒有繼承關係,但是它們的第一個成員變量的類型是一致的,也就是它們都是指向一個類型爲JNINativeInterface的回調函數表,因此,Dalvik虛擬機可以將一個JNIEnvExt結構體強制轉換成一個JNIEnv結構體返回給Zygote進程,這時候我們通過JNIEnv結構體來訪問其成員變量functions所描述的回調函數表時,實際訪問到的是對應的JNIEnvExt結構體的成員變量funcTable所描述的回調函數表。
爲什麼不直接讓JNIEnvExt結構體從JNIEnv結構體繼承下來呢?這樣把一個JNIEnvExt結構體轉換爲一個JNIEnv結構體就是相當直觀的。然而,Dalvik虛擬機的源代碼並一定是要以C++語言的形式來編譯的,它也可以以C語言的形式來編譯的。由於C語言沒有繼承的概念,因此,爲了使得Dalvik虛擬機的源代碼能同時兼容C++和C,這裏就使用了一個Trick:只要兩個結構體的內存佈局相同,它們就可以相互轉換訪問。當然,這並不要求兩個結構體的內存佈局完全相同,但是至少開始部分要求是相同的。在這種情況下,將一個結構體強制轉換成另外一個結構體之外,只要不去訪問內存佈局不一致的地方,就沒有問題。在Android系統的Native代碼中,我們可以常常看到這種Trick。
接下來,我們需要搞清楚的是JNIEnvExt結構體的成員變量funcTable指向的回調函數表是什麼。同樣是從前面Dalvik虛擬機的啓動過程分析一文可以知道,Dalvik虛擬機在創建一個JNIEnvExt結構體的時候,會將它的成員變量funcTable指向全局變量gNativeInterface所描述的一個回調函數表。
gNativeInterface定義在文件dalvik/vm/Jni.c中,如下所示:
static const struct JNINativeInterface gNativeInterface = { ...... CallStaticVoidMethodV, ...... };
在這個回調函數表中,名稱爲CallStaticVoidMethodV的函數指針指向的是一個同名函數CallStaticVoidMethodV。
函數CallStaticVoidMethodV同樣是定義在文件dalvik/vm/Jni.c中,不過它是通過宏CALL_STATIC來定義的,如下所示:
#define CALL_STATIC(_ctype, _jname, _retfail, _retok, _isref) \ ...... \ static _ctype CallStatic##_jname##MethodV(JNIEnv* env, jclass jclazz, \ jmethodID methodID, va_list args) \ { \ UNUSED_PARAMETER(jclazz); \ JNI_ENTER(); \ JValue result; \ dvmCallMethodV(_self, (Method*)methodID, NULL, true, &result, args);\ if (_isref && !dvmCheckException(_self)) \ result.l = addLocalReference(env, result.l); \ JNI_EXIT(); \ return _retok; \ } ...... \ CALL_STATIC(void, Void, , , false);
通過上面的分析就可以知道,在JNIEnvExt結構體的成員變量funcTable所描述的回調函數表中,名稱爲CallStaticVoidMethodV的函數指針指向的是一個同名函數CallStaticVoidMethodV。這就是說,我們通過JNIEnv結構體的成員變量functions所訪問到的名稱爲CallStaticVoidMethodV函數指針實際指向的是函數CallStaticVoidMethodV。
Step 3. CallStaticVoidMethodV
我們將上面的CALL_STATIC宏開之後,就可以得到函數CallStaticVoidMethodV的實現,如下所示:
static _ctype CallStaticVoidMethodV(JNIEnv* env, jclass jclazz, jmethodID methodID, va_list args) { UNUSED_PARAMETER(jclazz); JNI_ENTER(); JValue result; dvmCallMethodV(_self, (Method*)methodID, NULL, true, &result, args); if (_isref && !dvmCheckException(_self)) result.l = addLocalReference(env, result.l); JNI_EXIT(); return _retok; }
函數CallStaticVoidMethodV的實現很簡單,它通過調用另外一個函數dvmCallMethodV來執行由參數jclazz和methodID所描述的Java代碼,因此,接下來我們就繼續分析函數dvmCallMethodV的實現。
Step 4. dvmCallMethodV
void dvmCallMethodV(Thread* self, const Method* method, Object* obj, bool fromJni, JValue* pResult, va_list args) { ...... if (dvmIsNativeMethod(method)) { TRACE_METHOD_ENTER(self, method); /* * Because we leave no space for local variables, "curFrame" points * directly at the method arguments. */ (*method->nativeFunc)(self->curFrame, pResult, method, self); TRACE_METHOD_EXIT(self, method); } else { dvmInterpret(self, method, pResult); } ...... }
這個函數定義在文件dalvik/vm/interp/Stack.c中。
函數dvmCallMethodV首先檢查參數method描述的函數是否是一個JNI方法。如果是的話,那麼它所指向的一個Method對象的成員變量nativeFunc就指向該JNI方法的地址,因此就可以直接對它進行調用。否則的話,就說明參數method描述的是一個Java函數,這時候就需要繼續調用函數dvmInterpret來執行它的代碼。
Step 5. dvmInterpret
void dvmInterpret(Thread* self, const Method* method, JValue* pResult) { InterpState interpState; ...... /* * Initialize working state. * * No need to initialize "retval". */ interpState.method = method; interpState.fp = (u4*) self->curFrame; interpState.pc = method->insns; ...... typedef bool (*Interpreter)(Thread*, InterpState*); Interpreter stdInterp; if (gDvm.executionMode == kExecutionModeInterpFast) stdInterp = dvmMterpStd; #if defined(WITH_JIT) else if (gDvm.executionMode == kExecutionModeJit) /* If profiling overhead can be kept low enough, we can use a profiling * mterp fast for both Jit and "fast" modes. If overhead is too high, * create a specialized profiling interpreter. */ stdInterp = dvmMterpStd; #endif else stdInterp = dvmInterpretStd; change = true; while (change) { switch (interpState.nextMode) { case INTERP_STD: LOGVV("threadid=%d: interp STD\n", self->threadId); change = (*stdInterp)(self, &interpState); break; case INTERP_DBG: LOGVV("threadid=%d: interp DBG\n", self->threadId); change = dvmInterpretDbg(self, &interpState); break; default: dvmAbort(); } } *pResult = interpState.retval; ...... }
這個函數定義在文件dalvik/vm/interp/Interp.c中。
在前面Dalvik虛擬機的啓動過程分析一文中提到,Dalvik虛擬機支持三種執行模式:portable、fast和jit,它們分別使用kExecutionModeInterpPortable、kExecutionModeInterpFast和kExecutionModeJit三個常量來表示。Dalvik虛擬機在啓動的時候,會通過解析命令行參數獲得所要執行的模式,並且記錄在全局變量gDvm所指向的一個DvmGlobals結構體的成員變量executionMode中。
kExecutionModeInterpPortable表示Dalvik虛擬機以可移植模式來解釋Java代碼,也就是這種執行模式可以應用在任何一個平臺上,例如,可以同時在arm和x86各個平臺上執行。這時候使用函數dvmInterpretStd來作爲Java代碼的執行函數。
kExecutionModeInterpFast表示Dalvik虛擬機以快速模式來解釋Java代碼。以這種模式執行的Dalvik虛擬機是針對某一個特定的目標平臺進行過優化的,因此,它可以更快速地對Java代碼進行解釋以及執行。這時候使用函數dvmMterpStd來作爲Java代碼的執行函數。
kExecutionModeJit表示Dalvik虛擬機支持JIT模式來執行Java代碼,也就是先將Java代碼動態編譯成Native代碼再執行。這時候使用函數dvmMterpStd來作爲Java代碼的執行函數。
我們可以將函數dvmInterpretStd和dvmMterpStd理解爲Dalvik虛擬機的解釋器入口點。很顯然,解釋器是虛擬機的核心模塊,它的性能關乎到整個虛擬機的性能。Dalvik虛擬機的解釋器開始的時候都是以C語言來實現的,後來爲了提到性能,就改成以彙編語言來實現。注意,無論Dalvik虛擬機的解釋器是以C語言來實現,還是以彙編語言來實現,它們的源代碼都是以一種模塊化方法來自動生成的,並且能夠根據某一個特定的平臺進行優化。
所謂模塊化代碼生成方法,就是說將解釋器的實現劃分成若干個模塊,每一個模塊都對應有一系列的輸入文件(本身也是源代碼文件),最後通過工具(一個Python腳本)將這些輸入文件組裝起來形成一個C語言文件或者彙編語言文件。這個最終得到的C語言文件或者彙編語言文件就是Dalvik虛擬機的解釋器的實現文件。有了這種模塊化代碼生成方法之後,爲某一個特定的平臺生成優化過的解釋器就是相當容易的:我們只需要爲該平臺的Dalvik虛擬機解釋器的相關模塊提供一個特殊版本的輸入文件即可。也就是說,我們需要爲每一個支持的平臺提供一個配置文件,該配置文件描述了該平臺的Dalvik虛擬機解釋器的各個模塊所要使用的輸入文件。這種模塊化代碼生成方法不僅能避免手動編寫解釋器容易出錯的問題,還能方便快速地將Dalvik虛擬機從一個平臺移植到另外一個平臺。
採取了模塊化方法來生成Dalvik虛擬機解釋器的源代碼之後,Dalvik虛擬機解釋器的源代碼整體上就劃分成兩部分:第一部分相當於一個Wrapper,定義在dalvik/vm/interp目錄中;第二部分對應於具體的實現,定義在dalvik/vm/mterp目錄中。
在dalvik/vm/mterp目錄中,又定義了一系列的輸入文件,以及各個平臺所使用的配置文件。有了這些輸入文件和配置文件之後,就可以通過一個Python腳本來爲每一個平臺生成一個Dalvik虛擬機解釋器的輸出文件,並且保存在dalvik/vm/mterp/out目錄中。由於針對各個平臺生成的輸出文件是一個彙編語言文件,即一個 *.S文件,爲了方便理解它的邏輯,每一個彙編語言文件都對應有一個C語言文件。
雖然Dalvik虛擬機解釋器針對每一個平臺都有一個優化的版本,但是同時也會提供一個通用的版本,也就是一個可移植版本。該可移植版本的解釋器源文件只有C語言實現版本,定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中。在本文中,我們只考慮可移植版本的Dalvik虛擬機解釋器的實現,也就是隻考慮執行模式爲kExecutionModeInterpPortable的Dalvik虛擬機的運行機制。從前面的分析可以知道,該可移植版本的Dalvik虛擬機解釋器的入口函數爲dvmInterpretStd。
函數dvmInterpretStd在執行之前,需要知道解釋器的當前狀態,也就是它所要執行的Java函數及其指令入口,以及當前要執行的線程的堆棧。這些信息都用一個InterpState結構體來描述。其中,這個InterpState結構體的成員變量method描述的是要執行的Java函數,成員變量fp描述的是要執行的線程的當前堆棧幀,成員變量pc描述的是要執行的Java函數的入口點。
在函數dvmInterpret中,參數self描述的是當前用來執行Java代碼的線程,而參數method描述的是要執行的Java函數。通過這兩個參數我們就可以初始化上述的InterpState結構體。Dalvik虛擬機解釋器除了可以在正常模式執行之外,還可以在調試模式執行,即決於上述初始化後得到的InterpState結構體的成員變量nextMode的值。
在本文中,我們只考慮正常模式執行的Dalvik虛擬機解釋器的實現,也就是我們只分析函數dvmInterpretStd的實現。函數dvmInterpretStd解釋完成指定的Java函數之後,獲得的返回值就保存在上述InterpState結構體的成員變量retval中。
Step 6. dvmInterpretStd
#define INTERP_FUNC_NAME dvmInterpretStd ...... bool INTERP_FUNC_NAME(Thread* self, InterpState* interpState) { ...... DvmDex* methodClassDex; // curMethod->clazz->pDvmDex JValue retval; /* core state */ const Method* curMethod; // method we're interpreting const u2* pc; // program counter u4* fp; // frame pointer u2 inst; // current instruction ...... /* copy state in */ curMethod = interpState->method; pc = interpState->pc; fp = interpState->fp; retval = interpState->retval; /* only need for kInterpEntryReturn? */ methodClassDex = curMethod->clazz->pDvmDex; ...... while (1) { ...... /* fetch the next 16 bits from the instruction stream */ inst = FETCH(0); switch (INST_INST(inst)) { ...... HANDLE_OPCODE(OP_INVOKE_DIRECT /*vB, {vD, vE, vF, vG, vA}, meth@CCCC*/) GOTO_invoke(invokeDirect, false); OP_END ...... HANDLE_OPCODE(OP_RETURN /*vAA*/) vsrc1 = INST_AA(inst); ...... retval.i = GET_REGISTER(vsrc1); GOTO_returnFromMethod(); OP_END ...... } } ...... /* export state changes */ interpState->method = curMethod; interpState->pc = pc; interpState->fp = fp; /* debugTrackedRefStart doesn't change */ interpState->retval = retval; /* need for _entryPoint=ret */ interpState->nextMode = (INTERP_TYPE == INTERP_STD) ? INTERP_DBG : INTERP_STD; ...... return true; }
這個函數定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中。
函數dvmInterpretStd對應的是Dalvik虛擬機解釋器的可移植版本實現,它大概可以劃分三個邏輯塊:
1. 初始化當前要解釋的類(methodClassDex)及其成員變量函數(curMethod)、棧幀(fp)、程序計數器(pc)和返回值(retval),這些值都可以從參數interpState獲得。
2. 在一個無限while循環中,通過FETCH宏依次獲得當前程序計數器(pc)中的指令inst,並且通過宏INST_INST獲得指令inst的類型,最後就switch到對應的分支去解釋指令inst。
宏FETCH和INST_INST的定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中,如下所示:
/* * Get 16 bits from the specified offset of the program counter. We always * want to load 16 bits at a time from the instruction stream -- it's more * efficient than 8 and won't have the alignment problems that 32 might. * * Assumes existence of "const u2* pc". */ #define FETCH(_offset) (pc[(_offset)]) /* * Extract instruction byte from 16-bit fetch (_inst is a u2). */ #define INST_INST(_inst) ((_inst) & 0xff)
從這裏我們就可以看出,pc實際上指向的就是當前要執行的Java函數的方法區,也就是一個指令流。這個指令流包含了很多指令,需要通過一個while循環來依次對它們進行解釋,直到碰到一個return指令爲止。這就是Dalvik虛擬機解釋器的核心功能。例如,假設當前遇到的是一條OP_INVOKE_DIRECT指令,它表示要調用當前類的一個非靜態非虛成員函數,這時候就會通過宏GOTO_invoke跳到invokeDirect這個分支去。
宏HANDLE_OPCODE和GOTO_invoke定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中,如下所示:
# define HANDLE_OPCODE(_op) case _op: #define GOTO_invoke(_target, _methodCallRange) \ do { \ methodCallRange = _methodCallRange; \ goto _target; \ } while(false)
分支invokeDirect是通過另外一個宏GOTO_TARGET來定義的,在接下來的Step 7中我們再分析。
當遇到return指令時,例如,遇到OP_RETURN指令時,首先會通過宏INST_AA和GET_REGISTER來獲得函數的返回值,接着再通過宏GOTO_returnFromMethod跳到returnFromMethod分支去結束整個while循環。
注意,當指令inst是return指令時,它的執行結果即爲當要正在解釋的Java函數返回值。通過宏INST_AA可以知道一條指令的執行結果保存在哪個寄存器中,而通過宏GET_REGISTER可以獲得該寄存器的值。
宏INST_AA和GET_REGISTER定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中,如下所示:
/* * Get the 8-bit "vAA" 8-bit register index from the instruction word. * (_inst is u2) */ #define INST_AA(_inst) ((_inst) >> 8) # define GET_REGISTER(_idx) \ ( (_idx) < curMethod->registersSize ? \ (fp[(_idx)]) : (assert(!"bad reg"),1969) )
#define GOTO_returnFromMethod() goto returnFromMethod;
分支returnFromMethod和invokeDirect一樣,都是通過宏GOTO_TARGET來定義的,在接下來的Step 9中我們再分析。
接下來,我們就以分支invokeDirect爲例來說明Dalvik虛擬機解釋一條指令的過程,接着再以分支returnFromMethod的實現來說明Dalvik虛擬機解釋器從一個函數返回的過程。
Step 7. invokeDirect
GOTO_TARGET(invokeDirect, bool methodCallRange) { ...... vsrc1 = INST_AA(inst); /* AA (count) or BA (count + arg 5) */ ref = FETCH(1); /* method ref */ vdst = FETCH(2); /* 4 regs -or- first reg */ EXPORT_PC(); ...... methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref); ...... GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst); } GOTO_TARGET_END
根據Step 6的分析,invokeDirect是一個分支,它通過宏GOTO_TARGET定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中。
分支invokeDirect用來調用當前類(methodClassDex)的非靜態非虛成員函數。這個被調用的成員函數的引用可以通過宏FETCH(1)來獲取。知道了被調用的成員函數的引用之後,就可以通過調用函數dvmDexGetResolvedMethod來獲得對應的成員函數(methodToCall)。
此外,宏FETCH(2)用來獲得要傳遞給成員函數(methodToCall)的參數列表,而宏EXPORT_PC是用來記錄當前程序計數器pc的位置的,用來幫助實現精確GC。關於精確GC(Extra/Precise GC),可以參考前面Dalvik虛擬機的啓動過程分析一文的介紹。
宏GOTO_invokeMethod定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中,如下所示:
#define GOTO_invokeMethod(_methodCallRange, _methodToCall, _vsrc1, _vdst) goto invokeMethod;
它表示要跳到分支invokeMethod去解釋當前類的成員函數_methodToCall,接下來我們就繼續分析它的實現。
Step 8. invokeMethod
GOTO_TARGET(invokeMethod, bool methodCallRange, const Method* _methodToCall, u2 count, u2 regs) { STUB_HACK(vsrc1 = count; vdst = regs; methodToCall = _methodToCall;); StackSaveArea* newSaveArea; u4* newFp; ...... newFp = (u4*) SAVEAREA_FROM_FP(fp) - methodToCall->registersSize; newSaveArea = SAVEAREA_FROM_FP(newFp); ...... newSaveArea->prevFrame = fp; newSaveArea->savedPc = pc; ...... if (!dvmIsNativeMethod(methodToCall)) { /* * "Call" interpreted code. Reposition the PC, update the * frame pointer and other local state, and continue. */ curMethod = methodToCall; methodClassDex = curMethod->clazz->pDvmDex; pc = methodToCall->insns; fp = self->curFrame = newFp; ...... FINISH(0); // jump to method start } else { /* set this up for JNI locals, even if not a JNI native */ ...... self->curFrame = newFp; ...... /* * Jump through native call bridge. Because we leave no * space for locals on native calls, "newFp" points directly * to the method arguments. */ (*methodToCall->nativeFunc)(newFp, &retval, methodToCall, self); ...... if (true /*invokeInstr >= OP_INVOKE_VIRTUAL && invokeInstr <= OP_INVOKE_INTERFACE*/) { FINISH(3); } ...... } ...... } GOTO_TARGET_END
分支invokeMethod通過宏GOTO_TARGET定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中。
分支invokeMethod首先是爲當前要解釋的成員函數methodToCall創建一個新的棧幀newFp,接着再通過調用函數dvmIsNativeMethod來判斷成員函數methodToCall是否是一個JNI方法。
在新創建的棧幀newFp中,分別在其成員變量prevFrame和savedPc中保存了當前棧幀fp和當前程序計數器pc的值,這樣使得在調用完成員函數methodToCall之後,可以返回到當前正在執行的成員函數的下一條指令中去,以及恢復當前線程的棧幀。
如果成員函數methodToCall不是一個JNI方法,那麼就說明接下來仍然是要通過Dalvik虛擬機解釋器來執行它。不過這時候需要將當前線程激活的棧幀fp設置爲newFp,以及將程序計數器pc指向成員函數methodToCall的方法區。最後通過宏FINISH(0)來跳出當前分支,實際上就是跳出前面Step 6的switch語句,然後重複執行while循環語句。此時傳遞給宏FINISH的參數爲0,表示不需要調整程序計數器pc的值,因爲前面已經調整過了。
如果成員函數methodToCall是一個JNI方法,那麼該JNI方法的地址就保存在methodToCall->nativeFunc中,這時候只需要直接對它進行調用即可。調用JNI方法結束之後,需要通過宏FINISH(3)調整程序計數器pc的值,以及跳出當前分支,以及可以回到前面Step 6的while循環去執行下一條指令。此時傳遞給宏FINISH的參數爲3,表示要將程序計數器pc的值增加3,正好是跳過當前執行的指令。
宏FINISH定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中,如下所示:
# define ADJUST_PC(_offset) do { \ pc += _offset; \ EXPORT_EXTRA_PC(); \ } while (false) #endif # define FINISH(_offset) { ADJUST_PC(_offset); break; }
注意,在宏ADJUST_PC中,又會通過宏EXPORT_EXTRA_PC來記錄當前程序計數器pc的值,也是用來幫助實現精確GC的。
這一步執行完成之後,就回到前面的Step 6的while循環中,繼續執行下一條指令,直到遇到reutrn指令爲止。接下來我們就以OP_RETURN指令爲例,來說明Java函數的返回操作,也就分析分支returnFromMethod的實現。
Step 9. returnFromMethod
GOTO_TARGET(returnFromMethod) { StackSaveArea* saveArea; ...... saveArea = SAVEAREA_FROM_FP(fp); ...... fp = saveArea->prevFrame; ...... /* update thread FP, and reset local variables */ self->curFrame = fp; curMethod = SAVEAREA_FROM_FP(fp)->method; //methodClass = curMethod->clazz; methodClassDex = curMethod->clazz->pDvmDex; pc = saveArea->savedPc; ...... if (true /*invokeInstr >= OP_INVOKE_VIRTUAL && invokeInstr <= OP_INVOKE_INTERFACE*/) { FINISH(3); } ...... } GOTO_TARGET_END
分支returnFromMethod通過宏GOTO_TARGET定義在文件dalvik/vm/mterp/out/InterpC-portstd.c中。
分支returnFromMethod的實現比較簡單,它主要就是恢復上一個執行的成員函數的棧幀,以及該成員函數下一條要執行的指令。由於在前面的Step 8中,我們在當前棧幀中保存了上一個執行的成員函數的下一條要執行的指令及其棧幀,因此,這裏對它們進行恢復是很直覺的。
不過有一點需要注意的是,前面的Step 8保存的是當前正在執行的成員函數的程序計算器,現在由於該程序計算器所指向的指令已經執行完成了,因此,我們需要繼續調整從當前棧幀中恢復回來的指令值,使得它指向的是上一個執行的成員函數的下一條要執行的指令的值,這是通過宏FINISH(3)來完成的。
至此,我們就分析完成Dalvik虛擬機解釋器的執行過程了,這個過程也就相當於是Dalvik虛擬機的運行過程,也就是說,Step 7到Step 9實際上是會不斷地重複執行,直至進程退出爲止的。以Zygote進程爲例,Dalvik虛擬機解釋器就是以com.android.internal.os.ZygoteInit類的靜態成員函數main爲入口點執行,然後在一個Socket上進行循環,用來等待和處理ActivityManagerService服務向它發送創建新應用程序進程的請求,直至系統退出爲止。又以Android應用程序進程爲例,Dalvik虛擬機解釋器就是以android.app.ActivityThread類的靜態成員函數main爲入口點執行,然後在一消息隊列上進行循環,用來等待和處理主線程的消息,直到應用程序退出爲止。
當然,Dalvik虛擬機在運行的過程中,除了解釋執行之外,還可能會進行JIT。JIT的目的就是將Java代碼即時編譯成Native代碼之後再直接執行,這樣對於經常運行的代碼來說,可以提高性能。由於在將Java代碼即時編譯成Native代碼的過程中,可以進一步利用運行時信息來進行激進優化,因此,JIT獲得的Native代碼比AOT獲得的Native可能會更優化。關於JIT的實現,可以參考Hello, JIT World: The Joy of Simple JITs一文。
此外,從Step 4和Step 8可以知道,Dalvik虛擬機在運行的過程中,除了需要執行Java函數之外,還可能需要執行Native函數,這些Native函數也就是我們平時所說的JNI方法。在接下來的一篇文章中,我們就將分析這些JNI方法註冊到Dalvik虛擬機裏面去的,敬請關注!
老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!