Android JNI 學習(十一):Invocation Api
1. 簡介
Invocation API允許軟體提供商在原生程式中內嵌Java虛擬機器。因此可以不需要連結任何Java虛擬機器程式碼來提供Java-enabled的應用程式。
以下程式碼演示如何使用:
#include <jni.h> /* where everything is defined */ //... JavaVM *jvm; /* denotes a Java VM */ JNIEnv *env; /* pointer to native method interface */ JavaVMInitArgs vm_args;/* JDK/JRE 6 VM initialization arguments */ JavaVMOption* options = new JavaVMOption[1]; options[0].optionString = "-Djava.class.path=/usr/lib/java"; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; /* load and initialize a Java VM, return a JNI interface * pointer in env*/ JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); delete options; /* invoke the Main.test method using the JNI */ jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100);/* We are done. */ jvm->DestroyJavaVM();
建立虛擬機器
JNI_CreateJavaVM()
函式載入和初始化一個Java虛擬機器。呼叫該函式的執行緒被視為是主執行緒(main thread)。
attach到虛擬機器
JNI介面指標(JNIEnv)只在當前執行緒有效,如果需要在另一個執行緒訪問Java虛擬機器,必須先呼叫 AttachCurrentThread()
來將自己 attach 到虛擬機器來獲得JNI介面指標(JNIEnv)
被acttach到的執行緒必須有足夠的stack空間來執行一定的工作。每個執行緒分配多少棧空間根據系統而不同。
detach虛擬機器
一個attach到虛擬機器的本地執行緒必須在退出前呼叫 DetachCurrentThread()
來和虛擬機器detach。如果還有Java方法在call stack中,則這個執行緒不能detach。
unload虛擬機器
使用 JNI_DestroyJavaVM()
函式來解除安裝(unload)一個Java虛擬機器
虛擬機器會等待(阻塞),直到當前執行緒成為唯一的非守護程序的使用者程序(the only non-daemon user thread),才真正執行 unload 操作。
使用者程序(user thread)包括:
- Java執行緒(java threads)
- attached到虛擬機器的本地執行緒(attached native threads)
為什麼要做這樣的限制(強制等待),是因為Java執行緒和native執行緒可能會hold住系統資源,例如鎖,視窗等資源,而虛擬機器不能自動釋放這些資源。通過限制當前執行緒是唯一的執行中的使用者執行緒才unload虛擬機器,則將釋放這種系統資源的任務交給程式設計師自己來負責了。
2. 庫和版本管理
在 JDK/JRE 1.1,一旦本地庫被載入,它對所有classLoader都可見。因此兩個被不同的classLoader載入的類可能連結到同一個本地方法。這會出現兩個問題:
- 一個類可能錯誤的連結到了另一個classLoader載入的同名本地庫。
- 本地方法可以輕鬆的混合不同ClassLoader載入的類,這破壞了由ClassLoader提供的名稱空間隔離,會可能引發型別安全問題。
在 JDK/JRE 1.2,每個ClassLoader都有自己的一組本地庫。一個本地庫一旦被一個ClassLoader載入後,則不允許再被其他ClassLoader重複載入了。否則會丟擲 ``UnsatisfiedLinkError 異常。這樣的好處是:
- 由ClassLoader建立的名稱空間隔離在本地庫也被儲存下來了。一個本地庫不能輕易混合不同ClassLoader載入的類。
- 另外,本地庫可以在ClassLoader被垃圾收回時unload。
ClassLoader什麼時候會被垃圾收回?
JNI_OnLoad
Java虛擬機器在載入本地庫(native library)時(即呼叫 System.loadLibrary()
)後,在載入本地庫到記憶體之後,會尋找其內部的 JNI_OnLoad
函式,並執行它。這個函式必須返回本地庫使用的 JNI版本號。
如果要使用新的JNI函式,則必須返回高版本的JNI版本號。如果本地庫不提供 JNI_ONLoad
函式,則虛擬機器預設它使用的是 JNI_VERSION_1_1
版本。如果返回一個虛擬機器不支援的JNI版本號,則本地庫不能被載入。
JNI_OnUnload
虛擬機器在本地庫被垃圾回收前,呼叫其 JNI_OnUnload
函式。這個函式用來執行一個清理操作。因為函式在不確定的情況下被呼叫(例如 finalizer),因此開發者應該保守的使用Java虛擬機器服務,並避免任何Java回撥函式。
注意:JNI_OnLoad
和 JNI_OnUnload
兩個函式是可選的,不是必須要有。
3. Invocation API函式
JavaVM
型別是一個指向 Invocation API 函式表的指標。例如:
typedef const struct JNIInvokeInterface *JavaVM; const struct JNIInvokeInterface ... = { NULL, NULL, NULL, DestroyJavaVM, AttachCurrentThread, DetachCurrentThread, GetEnv, AttachCurrentThreadAsDaemon };
JNI_GetDefaultJavaVMInitArgs
jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);
返回Java虛擬機器的預設配置。
引數:
vm_args
:JavaVMIntArgs結構體的指標,包含虛擬機器預設配置。
返回值:
成功返回 JNI_OK
,失敗返回負數。
JNI_GetCreatedJavaVMs
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
返回所有已經建立過的Java虛擬機器。
在 JDK/JRE 1.2, 不支援一個程序建立多個Java虛擬機器。
引數:
vmBuf
:虛擬機器buffer,建立過的虛擬機器會被放進來。bufLen
:buffer的最大長度。nVMs
: 虛擬機器的總數。
返回值:
成功返回 JNI_OK
,失敗返回負數。
JNI_CreateJavaVM
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
載入和初始化一個Java虛擬機器,當前執行緒作為主執行緒(main thread)。
在 JDK/JRE 1.2,不允許在同一個程序建立多個Java虛擬機器。
引數:
-
p_vm
:指向JavaVM
的指標。 -
p_env
:指向JNIEnv
指標的指標。 -
vm_args
: 虛擬機器的引數。
第3個引數 vm_args
的結構體為:
typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs;
其中 version
必須大於等於 JNI_VERSION_1_2
,
其中 nOptions
為 options
的數量.
其中 ignoreUnrecognized
設定為 JNI_TRUE
,則會忽視所有不被識別的以 -X
或 _
開頭的引數字串,如果設定為 JNI_FALSE
,則遇到不被識別的引數時JNI_CreateJavaVM
函式會返回 JNI_ERR
其中 options
的結構體為:
typedef struct JavaVMOption { char *optionString; /* the option as a string in the default platform encoding */ void *extraInfo; } JavaVMOption;
另外,所有虛擬機器的實現都支援它自己的非標準引數。非標準引數必須以 -X
或 _
開頭。例如,JDK/JRE 支援 -Xms
和 -Xmx
引數來允許開發者指定初始化和最大的heap大小。
返回值:
成功返回 JNI_OK
,失敗返回負數。
使用例項:
JavaVMInitArgs vm_args; JavaVMOption options[4]; options[0].optionString = "-Djava.compiler=NONE"; /* disable JIT */ options[1].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */ options[2].optionString = "-Djava.library.path=c:\mylibs"; /* set native library path */ options[3].optionString = "-verbose:jni"; /* print JNI-related messages */ vm_args.version = JNI_VERSION_1_2; vm_args.options = options; vm_args.nOptions = 4; vm_args.ignoreUnrecognized = TRUE; /* Note that in the JDK/JRE, there is no longer any need to call * JNI_GetDefaultJavaVMInitArgs. */ res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args); if (res < 0) ...
DestoryJavaVM
jint DestroyJavaVM(JavaVM *vm);
解除安裝一個Java虛擬機器,並收回它擁有的資源。
JDK/JRE 1.1 還沒有完全支援這個函式。在JDK/JRE 1.1 只有主執行緒才允許呼叫該函式。自從 JDK/JRE 1.2,任何執行緒,不管是否已經 attached,都可以呼叫該函式,如果當前執行緒已經 attached,則虛擬機器會等待當前執行緒作為唯一的非守護使用者執行緒。如果當前執行緒沒有 attached,則先attached,再等待當前執行緒作為唯一的非守護使用者執行緒。
JDK/JRE 1.1.2 不支援unload虛擬機器。
引數:
vm
:需要被銷燬的虛擬機器。
返回值:
成功返回 JNI_OK
,失敗返回負數。
AttachCurrentThread
jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);
attach當前執行緒到Java虛擬機器,返回JNI介面指標 JNIEnv
。
嘗試attach已經attached過的執行緒不會執行任何操作(no-op)。
一個本地執行緒不能同時attach到兩個不同的Java虛擬機器。
當前一個執行緒attach到虛擬機器,它的上下文ClassLoader是Bootstrap ClassLoader。
引數:
vm
:需要被attach到的虛擬機器。p_env
:返回的當前執行緒的JNI介面指標。thr_args
:JavaVMAttachArgs
結構體來指定附加資訊,或傳入NULL
typedef struct JavaVMAttachArgs { jint version; /* must be at least JNI_VERSION_1_2 */ char *name; /* the name of the thread as a modified UTF-8 string, or NULL */ jobject group; /* global ref of a ThreadGroup object, or NULL */ } JavaVMAttachArgs
返回值:
成功返回 JNI_OK
,失敗返回負數。
AttachCurrentThreadAsDaemon
jint AttachCurrentThreadAsDaemon(JavaVM* vm, void** penv, void* args);
和 AttachCurrentThread
類似,只是新建立的 java.lang.Thread
被設定為守護執行緒(daemon)。
JDK/JRE 1.4之後才有這個函式。
引數:
vm
:需要被attach到的虛擬機器。penv
:返回的當前執行緒的JNI介面指標。args
:JavaVMAttachArgs
結構體來指定附加資訊,或傳入NULL
返回值:
成功返回 JNI_OK
,失敗返回負數。
DetachCurrentThread
從java虛擬機器detach當前執行緒。所有這個執行緒持有的Java監視區(monitor)都會被釋放。
自從 JDK/JRE 1.2, 主執行緒可以從虛擬機器detach。
引數:
vm
:需要detach的虛擬機器。
返回值:
成功返回 JNI_OK
,失敗返回負數。
GetEnv
jint GetEnv(JavaVM *vm, void **env, jint version);
獲取當前執行緒的JNI介面指標 JNIEnv
引數:
vm
:虛擬機器例項。env
:放置返回的當前執行緒的JNI介面指標。version
:JNI版本。
返回值:
如果當前執行緒還沒有attach到虛擬機器,則設定 *env
為 NULL
,並返回 JNI_EDETACHED
。如果指定的JNI版本不被支援,則也設定 *env
為 NULL
,並且返回 JNI_EVERSION
。否則設定 *env
為正常的介面,並返回 JNI_OK
。
結語
JNI作為Java虛擬機器的介面,銜接了Java層和native層。並且涉及到ClassLoader和JVM的內部實現等知識,全面瞭解JNI可以作為一把鑰匙來開啟JDK和JVM底層研究的大門。