1. 程式人生 > >Android JNI 學習(十一):Invocation Api

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_OnLoadJNI_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,

其中 nOptionsoptions 的數量.

其中 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_argsJavaVMAttachArgs 結構體來指定附加資訊,或傳入 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介面指標。
  • argsJavaVMAttachArgs 結構體來指定附加資訊,或傳入 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到虛擬機器,則設定 *envNULL ,並返回 JNI_EDETACHED 。如果指定的JNI版本不被支援,則也設定 *envNULL ,並且返回 JNI_EVERSION。否則設定 *env 為正常的介面,並返回 JNI_OK 。​

結語

JNI作為Java虛擬機器的介面,銜接了Java層和native層。並且涉及到ClassLoader和JVM的內部實現等知識,全面瞭解JNI可以作為一把鑰匙來開啟JDK和JVM底層研究的大門。