JNI: 鏈接Java世界的JavaVM和JNIEnv

不使用IDE作一次JNI開發 一文中,咱們作了一次從 Java 層到 Native 層的開發。那麼,咱們能不能反過來,完成一次從 Native 層到 Java 層的開發呢?固然能,不過過程可沒那麼簡單,而掌握 JavaVMJNIEnv 這兩個結構體就是關鍵,這兩個結構體就是通往 Java 世界的大門,重要性不言而喻。html

JavaVM

JavaVM 這個結構體指針在簡單的 JNI 開發中不多使用到,它是虛擬機的表明,從 JDK 1.2 開始,一個進程只容許建立一個虛擬機。java

當 Java 層訪問 Nativce 層的時候會自動在 JNI 層建立一個 JavaVM 指針,而咱們在 JNI 層一般所使用的都是從 JavaVM 中獲取的 JNIEnv 指針。那麼如今咱們來看下 JavaVM 這個結構體android

struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM() { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread() { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version) { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args) { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif
};
複製代碼

本文分析的代碼都是C++版本,由於C++版本的JNI使用起來方便些。c++

能夠看到全部方法都是由結構體 JNIInvokeInterface 實現的,那麼來看下這個結構體吧oracle

/* * JNI invocation interface. */
struct JNIInvokeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;

    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
複製代碼

很是簡單,前三個指針做爲保留使用,後五個指針爲函數指針,從函數指針的名字能夠推測出函數的用途,其中 DestoryJavaVM 函數是用來銷燬虛擬機的, getEnv 函數是用來獲取 JNIEnv 指針的。後面會舉例解釋這幾個函數用途。ide

建立JavaVM

從 Java 層到 Native 層的開發的時候,咱們並不須要手動建立 JavaVM 對象,所以虛擬機自動幫咱們完成了這些工做。然而,若是從 Native 層到 Java 層開發的時候,咱們就須要手動建立 JavaVM 對象,建立的函數原型以下函數

#inlcude <jni.h>

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);
複製代碼

JNI_CreateJavaVM 函數不屬於任何結構體,方法聲明在 jni.h 頭文件中。post

參數解釋學習

  1. p_vm: 是一個指向 JavaVM * 的指針,函數成功返回時會給 JavaVM *指針賦值。
  2. p_env: 是一個指向 JNIEnv * 的指針,函數成功返回時會給 JNIEnv * 指針賦值。
  3. vm_args: 是一個指向 JavaVMInitArgs 的指針,是初始化虛擬機的參數。

若是函數執行成功,返回 JNI_OK(值爲0),若是失敗返回負值。ui

基本上能夠這樣理解 JNI_CreateJavaVM() 函數,它就是爲了給 JavaVM *指針 和 JNIEnv *指針賦值。咱們獲得這兩個指針即可以操縱"萬物",這裏的"萬物"指的是 Java 世界的"萬物"。

使用 JavaVM

那麼咱們如何使用這個函數呢?這個仍是有點小複雜的,須要對 Java虛擬機 有比較深的認知。那麼咱們只能找個例子來學習,找哪一個例子呢?在 Android 源碼中,Zygote 進程開啓 Java 世界就是一個絕佳的例子。

Zygote 進程啓動入口爲 App_main.cppmain() 函數

int main(int argc, char* const argv[])
{
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}
複製代碼

代碼是簡化的,只是爲了學習JNI而已。

main()函數最終會調用 AppRuntime 類的 start() 函數,AppRuntime 仍是定義在 App_main.cpp 文件中,它是 AndroidRuntime 的子類,而且 start() 方法是由 AndroidRuntime 類實現的。

那麼,如今看下 AndroidRuntime.cppstart() 函數實現

JavaVM* AndroidRuntime::mJavaVM = NULL;

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    // 1. 建立虛擬機
    JNIEnv* env;
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    
    // 2. 註冊函數
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }
    
    // 3. 調用ZygoteInit.java的main()方法
    env->CallStaticVoidMethod(startClass, startMeth, strArray);
    
    // 4. 從JavaVM中分離當前線程
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    // 5. 銷燬JavaVM
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}
複製代碼

第一步建立虛擬機就是調用了 JNI_CreateJavaVM() 函數,函數成功返回後,就能夠獲得賦值的 JavaVM *指針和JNIEnv *指針,也就是代碼中的全局變量mJavaVM 和 局部變量env。這裏,須要注意下爲什麼 mJavaVM 是全局變量,而 env 是局部變量?這個後面講 JNIEnv 的時候會解釋。

第二步,第三步就是利用獲取的 JNIEnv * 來訪問 Java 世界。

第四步調用 JavaVMDeatchCurrentThread() 函數,這個函數從命名就能夠看出是從虛擬機分離當前線程。此時,咱們應該想到了 JavaVM 的另一個函數 AttachCurrentThread(),這個函數是把當前線程附着到虛擬機中。然而咱們並無調用附着的操做,怎麼就直接出現 DeatchCurrentThread() 函數呢? 那是由於 JNI_CreateJavaVM() 直接把當前線程附着到了虛擬機中。這個在後面講 JNIEnv 也會有解釋。

第五步調用 JavaVMDestroyJavaVM() 函數,銷燬虛擬機,

JNIEnv

_JavaVM 結構體中有一個函數 getEnv(),與之相對應的函數原型以下

jint GetEnv(JavaVM *vm, void **env, jint version);
複製代碼

參數說明

  1. vm: 虛擬機對象。
  2. env: 一個指向 JNIEnv 結構的指針的指針。
  3. version: JNI版本,根據jdk的版本,目前有四種值,分別爲 JNI_VERSION_1_1, JNI_VERSION_1_2, JNI_VERSION_1_4, JNI_VERSION_1_6

這個函數執行結果有幾種狀況:

  1. 若是當前線程沒有附着到虛擬機中,也就是沒有調用 JavaVM 的 AttachCurrentThread() 函數,那麼就會設置 *env 的值爲 NULL,而且返回 JNI_EDETACHED (值爲-2)。
  2. 若是參數version鎖指定的版本不支持,那麼就會設置 *env 的值爲 NULL,而且返回 JNI_EVERSION(值爲-3)。
  3. 除去上面的兩種異常狀況,就會給 *env 設置正確的值,而且返回 JNI_OK(值爲0)。

JNIEnv使用限制

函數執行結果的第一種狀況來能夠說明幾個問題

  1. JNIEnv * env 是與線程相關,所以多個線程之間不能共享同一個 env
  2. 若是在Native層新建一個線程,要獲取 JNIEnv * env,那麼必須作到以下兩點
    • 線程必須調用 JavaVMAttachCurrentThread() 函數。
    • 必須全局保存 JavaVM * mJavaVm,那麼就能夠在線程中經過調用 JavaVMgetEnv() 函數來獲取 JNIEnv * env的值。

咱們還記得 JNI_CreateJavaVM() 函數也設置 *env 的值嗎?那麼它確定也會執行 AttachCurrentThread() 函數把當前線程附着到虛擬機中。這也就是解釋了爲什麼在沒有明顯調用 AttachCurrentThread() 的狀況下,能夠執行 JavaVMDetachCurrentThread() 函數。

JNIEnv做用

GetEnv() 函數從命名能夠看出是給 JNIEnv *env 賦值的,那麼這個 JNIEnv 又有什麼做用呢?來看下結構體聲明吧

struct _JNIEnv {
    const struct JNINativeInterface* functions;

    jclass FindClass(const char* name) { return functions->FindClass(this, name); }
    
    jobject NewGlobalRef(jobject obj) { return functions->NewGlobalRef(this, obj); }
    
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) { return functions->GetMethodID(this, clazz, name, sig); }
    
    // ... 省略無數個函數
}    
複製代碼

從聲明能夠看出,_JNIEnv_JavaVM 玩了同樣的套路,函數的都是交由另一個指針實現,而這裏就是交給 JNINativeInterface 結構體指針,從結構體命名能夠大體猜下意思,它應該是定義了JNI函數調用的接口,到底是不是呢,看下結構體聲明

/* * Table of interface function pointers. */
struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;
    
    jclass      (*FindClass)(JNIEnv*, const char*);
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    
    // 調用返回值爲int類型的Java方法
    jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    
    // 獲取Java對象某個變量的值
    jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
    // 設置Java對象某個變量的值
    void        (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean);
    
    // 建立一個Java的String對象
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    
    // ... 省略無數個操做Java的方法
}    
複製代碼

從函數的做用來看,原來這個結構體是操做 Java 層的入口,從這裏就可見 JNIEnv * 指針的做用了,而這個指針正是本地函數的第一個參數,例如在不使用IDE作一次JNI開發 一文中實現的一個本地函數以下

extern "C" JNIEXPORT jstring JNICALL Java_com_bxll_jnidemo_Hello_helloFromJNI (JNIEnv * env, jclass clazz) {
    const char * str_hello = "Hello from C++";
    return env->NewStringUTF(str_hello);
}
複製代碼

注意看第一個參數JNIEnv * env,這是自動虛擬機自動幫咱們傳入的,意思就是告訴你,能夠利用這個指針來操做 Java 層。

總結

經過對 _JavaVM_JNIEnv 結構的瞭解,咱們就知道利用這兩個結構體指針是能夠打通 Java 世界的,而具體操縱 Java 世界的是 JNIEnv * 指針,那麼具體如何操做 Java 世界"萬物"呢,後面文章會一一詳述。

參考

Oracle官網的Invocation API

docs.oracle.com/javase/7/do…

相關文章
相關標籤/搜索