JNI程式設計基礎(一)

李牙刷兒發表於2017-04-23

JNI-Java Native Interface,是Java平臺提供的一個特性,通過編寫JNI函式實現Java程式碼呼叫C/C++程式碼以及C/C++程式碼呼叫Java程式碼的作用。從而達到利用不同語言的特點。為什麼需要在Java中呼叫C/C++程式碼,在我看來最主要有以下三點:

  • C/C++程式碼相比Java有著更高的效能
  • C/C++程式碼更難被反編譯,有更好的安全性
  • 通過JNI函式可以繞開JVM的限制,完成一些在Java層面實現不了的功能。典型的例子就是Android熱修復框架AndFix

既然要實現C/C++和java程式碼之間的互動,那麼JVM就必須提供一整套的機制來實現相互之間的轉換,具體來說涉及到以下三個方面:

  • JNI函式的註冊
  • JNI層面和Java層面的資料結構對照
  • 描述符-用於描述類名或者資料型別

1.JNI函式的註冊

所謂JNI函式的註冊就是JVM能夠準確的找到對應的JNI函式,並將其連結到主程式。註冊分為動態註冊和靜態註冊,接下來通過一個例子來說明如何實現JNI函式的靜態和動態註冊。

1.例子

public class AndroidJni {
    static{
        System.loadLibrary("main");
    }
    public native void dynamicLog();
    public native void staticLog();
}

這是一個普通的Java類,類中申明瞭兩個native函式,dynamicLog和staticLog。native關鍵字告訴JVM,兩個函式是通過JNI實現的,那麼在哪裡去找這兩個函式JNI實現呢?注意,在這個類初始化的時候載入一個庫叫做main。沒錯,JVM就是會去main(如果是Linux平臺,這個庫就是libmain.so)這個庫中去找對應的函式。對應的C++程式碼如下:

#include <jni.h>
#define LOG_TAG "main.cpp"
#include "mylog.h"
static void nativeDynamicLog(JNIEnv *evn, jobject obj){
    LOGE("hell main");
}
JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj)
{
      LOGE("static register log ");
}

JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog},};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env;
    if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    LOGE("JNI_OnLoad comming");
    jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");
    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
    return JNI_VERSION_1_4;
}

這裡引用了兩個標頭檔案,jni.h和mylog.h,其中jni.h是定義

1.靜態註冊

在上面的程式碼中看到了JNIEXPORT和JNICALL關鍵字,這兩個關鍵字是兩個巨集定義,他主要的作用就是說明該函式為JNI函式,在Java虛擬機器載入的時候會連結對應的native方法,在AndroidJni.java的類中宣告瞭staticLog()為native方法,他對應的JNI函式就是Java_com_github_songnick_jni_AndroidJni_staticLog(),那麼是怎麼連結的呢,在Java虛擬機器載入so庫時,如果發現含有上面兩個巨集定義的函式時就會連結到對應Java層的native方法,那麼怎麼知道對應Java中的哪個類的哪個native方法呢,我們仔細觀察JNI函式名的構成其實是:Java_PkgName_ClassName_NativeMethodName,以Java為字首,並且用“_”下劃線將包名、類名以及native方法名連線起來就是對應的JNI函式了。一般情況下我們可以自己手動的去按照這個規則寫,但是如果native方法特別多,那麼還是有一定的工作量,並且在寫的過程中不小心就有可能寫錯,其實Java給我們提供了javah的工具幫助生成相應的標頭檔案。在生成的標頭檔案中就是按照上面說的規則生成了對應的JNI函式,我們在開發的時候直接copy過去就可以了。這裡上面的程式碼為例,在AndroidStudio中編譯後,進入專案的目錄app/build/intermediates/classes/debug下,執行如下命令:

javah -d jni com.github.songnick.jni.AndroidJni

這裡-d指定生成.h檔案存放的目錄(如果沒有就會自動建立),com.github.songnick.jni.AndroidJni表示指定目錄下的class檔案。這裡簡單介紹一下生成的JNI函式包含兩個固定的引數變數,分別是JNIEnv和jobject,其中JNIEnv後面會介紹,jobject就是當前與之連結的native方法隸屬的類物件(類似於Java中的this)。這兩個變數都是Java虛擬機器生成並在呼叫時傳遞進來的。

2.動態註冊

上面我們介紹了靜態註冊native方法的過程,就是Java層宣告的native方法和JNI函式是一一對應的,那麼有沒有方法讓Java層的native方法和任意的JNI函式連結起來,當然是可以的,這就得使用動態註冊的方法。接下來就看看如何實現動態註冊的。

1) JNI_OnLoad函式

 當我們使用System.loadLibarary()方法載入so庫的時候,Java虛擬機器就會找到這個函式並呼叫該函式,因此可以在該函式中做一些初始化的動作,其實這個函式就是相當於Activity中的onCreate()方法。該函式前面有三個關鍵字,分別是JNIEXPORT、JNICALL和jint,其中JNIEXPORT和JNICALL是兩個巨集定義,用於指定該函式是JNI函式。jint是JNI定義的資料型別,因為Java層和C/C++的資料型別或者物件不能直接相互的引用或者使用,JNI層定義了自己的資料型別,用於銜接Java層和JNI層,至於這些資料型別我們在後面介紹。這裡的jint對應Java的int資料型別,該函式返回的int表示當前使用的JNI的版本,其實類似於Android系統的API版本一樣,不同的JNI版本中定義的一些不同的JNI函式。該函式會有兩個引數,其中*jvm為Java虛擬機器例項,JavaVM結構體定義了以下函式:

DestroyJavaVM
   AttachCurrentThread
   DetachCurrentThread
   GetEnv

這裡我們使用了GetEnv函式獲取JNIEnv變數,上面的JNI_OnLoad函式中有如下程式碼:

JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
    return -1;
}

這裡呼叫了GetEnv函式獲取JNIEnv結構體指標,其實JNIEnv結構體是指向一個函式表的,該函式表指向了對應的JNI函式,我們通過呼叫這些JNI函式實現JNI程式設計,在後面我們還會對其進行介紹。

獲取Java物件,完成動態註冊

上面介紹瞭如何獲取JNIEnv結構體指標,得到這個結構體指標後我們就可以呼叫JNIEnv中的RegisterNatives函式完成動態註冊native方法了。該方法如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)11

第一個引數是Java層對應包含native方法的物件(這裡就是AndroidJni物件),通過呼叫JNIEnv對應的函式獲取class物件(FindClass函式的引數為需要獲取class物件的類描述符):

jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");11

第二個引數是JNINativeMethod結構體指標,這裡的JNINativeMethod結構體是描述Java層native方法的,它的定義如下:

typedef struct {
    const char* name;//Java層native方法的名字
    const char* signature;//Java層native方法的描述符
    void*       fnPtr;//對應JNI函式的指標
} JNINativeMethod;

第三個引數為註冊native方法的數量。一般會動態註冊多個native方法,首先會定義一個JNINativeMethod陣列,然後將該陣列指標作為RegisterNative函式的引數傳入,所以這裡定義瞭如下的JNINativeMethod陣列:

JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog}};11

最後呼叫RegisterNative函式完成動態註冊:

env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));11

2JNI資料結構

  1. JNIENV結構體

JNIENV是一個JNI環境結構體,結構體重維護了一系列的函式,通過這些環境函式可以實現與Java層的互動。下圖是JNIENV成員函式的一部分:

..........
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
CallVoidMethod(jobject obj, jmethodID methodID, ...)
CallBooleanMethod(jobject obj, jmethodID methodID, ...)
..........

從上面羅列的幾個方法可以看出,通過JNIENV我們可以輕易地獲取到一個Java類中的域,方法並操作這些成員。

  1. JNI資料型別

雖然JNI和Java都包含很多相同的資料型別,但是其定義卻並不一樣,所以Java的資料型別需要經過轉換才能在JNI層面被操作。接下來就是Java和JNI資料型別的對照:

1)基礎型別

| Java Type| Native Type | Description |
| — | — | — |
| boolean | jboolean | unsigned 8 bits |
| byte | jbyte | signed 8 bits |
| char | jchar | unsigned 16 bits |
| short | jshort | signed 16 bits |
|int | jint | signed 32 bits |
|long | jlong | signed 64 bits |
|float | jfloat | 32 bits |
|double | jdouble| 64 bits |
|void | void | N/A |

2) 應用型別

jobject                     (all Java objects)
|
|-- jclass                    (java.lang.Class objects)
|-- jstring                    (java.lang.String objects)
|-- jarray                    (array)
|      |--jobjectArray       (object arrays)
|      |--jbooleanArray        (boolean arrays)
|      |--jbyteArray            (byte arrays)
|      |--jcharArray            (char arrays)
|      |--jshortArray        (short arrays)
|      |--jintArray            (int arrays)
|     |--jlongArray            (long arrays)
|      |--jfloatArray        (float arrays)
|      |--jdoubleArray         (double arrays)
|
|--jthrowable

3) 方法和變數的ID

 當需要呼叫Java中的某個方法的時候我們首先要獲取它的ID,根據ID呼叫JNI函式獲取該方法,變數的獲取過程也是同樣的過程,這些ID的結構體定義如下:

    struct _jfieldID;              /* opaque structure */ 
    typedef struct _jfieldID *jfieldID;   /* field IDs */ 
    
    struct _jmethodID;              /* opaque structure */ 
    typedef struct _jmethodID *jmethodID; /* method IDs */
  1. 描述符

1.類描述符

 前面為了獲取Java的AndroidJni物件,是通過呼叫FindClass()函式獲取的,該函式引數只有一個字串引數,我們發現該字串如下所示:

    com/github/songnick/jni/AndroidJni11

其實這個就是JNI定義了對類的描述符,它的規則就是將”com.github.songnick.jni.AndroidJni”中的“.”用“/”代替。

2.方法描述符

 前面我們動態註冊native方法的時候結構體JNINativeMethod中含有方法描述符,就是確定native方法的引數和返回值,我們這裡定義的dynamicLog()方法沒有引數,返回值為空所以對應的描述符為:”()V”,括號類為引數,V表示返回值為空。下面還是看看幾個栗子吧:

| Method Descriptor | Java Language Type |
| — | — |
|“()Ljava/lang/String;” | String f(); |
|“(ILjava/lang/Class;)J”| long f(int i, Class c);|
|“([B)V” | String(byte[] bytes); |

上面的栗子我們看到方法的返回型別和方法引數有引用型別以及boolean、int等基本資料型別,對於這些型別的描述符在下個部分介紹。這裡陣列的描述符以”[“和對應的型別描述符來表述。對於二維陣列以及三維陣列則以”[[“和”[[[“表示:

|Descriptor |Java Langauage Type|
| — | — |
|“[[I” | int |
|“[[[D” | double[] |

3.資料型別描述符

 前面我們說了方法的描述符,那麼針對boolean、int等資料型別描述符是怎樣的呢,JNI對基本資料型別的描述符定義如下:

| Field Desciptor | Java Language Type |
| — | —- |
| Z | boolean |
| B | byte |
| C | char |
|S | short |
|I | int |
|J | long |
|F | float |
|D | double |

對於引用型別描述符是以”L”開頭”;”結尾,示例如下所示:

| Field Desciptor | Java Language Type |
| — | — |
| “Ljava/lang/String;” | String |
|“[Ljava/lang/Object;” | Object[] |


相關文章