Android JNI 基本操作

最近對NDK的進階學習了些,所以本來打算記錄下,以免後面忘了,之前寫過一篇基礎的入門使用,地址在這裏,但是看到網上這個系列的文章已經寫過,寫得挺好的,也挺詳細的,所以就厚着老臉轉載過來了,方便複習使用,後面有啥問題就直接在上面修改了。

轉載地址在這裏

自從 Android Studio 升級到 2.3 版本以後,使用 CMake 進行編譯就方便多了,不需要再寫 Android.mk 了,也不需要用 javah 來生成頭文件了,直接寫好 native 方法,快捷方式就可以生成對應的 C++ 方法,只要專注寫好 C++ 代碼,CMake 就可以指定的 CPU 架構生成對應的 SO 庫。不得不說google對於開發者越來越良心了。

JNI 和 NDK 的區別


NDK 開發難免會搞不清 JNI 和 NDK 的區別。

JNI 全稱是 Java Native Interface,即 Java 本地接口。它是用來使得 Java 語言和 C/C++ 語言相互調用的。它本身和 Android 並無關係,只是在 Android 開發中會用到,在其他地方也會用到的。

而 NDK 的全稱是 Native Development Kit,和 SDK 的全稱是 Software Development Kit 一樣,都是開發工具包。NDK 是 Android 開發的工具包,主要用作 C/C++ 開發,提供了相關的動態庫。

在 Android 上進行 NDK 開發還是得先學會 JNI 相關技能,先可以從 Java 層到 C/C++ 層的相互調用,然後再學習 NDK 開發的那些技巧。

簡單實例


在 AS 新建工程時若選擇了 Include C++ Support,就會自帶配置好的 C++ 開發環境。

在聲明 native 方法時還是用 Java 來寫比較好,比 Kotlin 的 external 關鍵字要友好多了,可以直接快捷鍵生成對用的 C++ 方法。

聲明 native 方法如下:

public static native int plus(int a, int b);

快捷鍵便會生成對應的 C++ 方法

extern "C"
JNIEXPORT jint JNICALL
Java_com_glumes_myapplication_NativeClass_plus(JNIEnv *env, jobject instance, jint a, jint b) {
    jint sum = a + b;
    return sum;
}

這是一個簡單的計算 a+b 的 native 方法,但卻包含了許多基本內容,在 C++ 層接收來自 Java 層的參數,並轉換成 C++ 層的數據類型,計算之後再返回成 Java 層的數據類型。

在 Java 層中只有兩個參數,而在 C++ 代碼就有四個參數了,至少都會包含前面兩個參數,下面講解這些參數意義。

其中: * env變量是 JNIEnv 類型的對象,該對象是一個 Java 虛擬機所運行的環境,通過它可以訪問到 Java 虛擬機內部的各種對象。

JNIEnv 類型對象參數 env
JNIEnv* 是定義任意 native 函數的第一個參數,它是一個指針,通過它可以訪問虛擬機內部的各種數據結構,同時它還指向 JVM 函數表的指針,函數表中的每一個入口指向一個 JNI 函數,每個函數用於訪問 JVM 中特定的數據結構。

結構如下圖所示:
在這裏插入圖片描述
可以看到這裏面涉及了三類指針,JNIEnv * 本身就是指針,而它指向的也是指針,在 JVM 函數表裏面的每一項又都是指針。

jobject 參數

jobject 是 native 函數裏的第二個參數類型,但卻不是一定的。

如果該 native 方法是一個靜態 static 方法,那麼第二個參數就是 jobject 類型,指的是調用該函數的對象;

如果是一個實例方法,那麼第二個參數就是 jclass 類型,指的是調用該函數的類。

類型轉換


基本類型轉換

Java類型 Native類型 符號屬性 字長
boolean jboolean 無符號 8位
byte jbyte 無符號 8位
char jchar 無符號 16位
short jshort 有符號 16位
int jint 有符號 32位
long jlong 有符號 64位
float jfloat 有符號 32位
double jdouble 有符號 64位

基礎類型在JNI中都有相應的數據類型

引用類型轉換

Java引用類型 Native類型 Java引用類型 Native類型
All objects jobject char jcharArray
java.long.Class jclass short[] jshortArray
java.long.String jstring int[] jintArray
Object[] jobjectArray long[] jlongArray
boolean[] jboolean float[] jfloatArray
byte[] jbyteArray double[] jdoubleArray
java.lang.Throwable jthrowable

知道了這些類型轉換,當要返回對應的返回值給java時候,就需要明白。

String字符串的操作


對於基本數據類型的操作,比如 boolean、int、float 等都大同小異,無非是在原來的數據類型前面加了一個 j來表示 JNI 數據類型。

而對於 String 類型,必須要使用合適的 JNI 函數來將 jstring 轉變成 C/C++ 字符串。

對於下面的 Native 方法,傳入一個字符串,並要求返回一個字符串。

 public static native String getNativeString(String str);

生成的對應的 C++ 代碼如下:

extern "C"
JNIEXPORT jstring JNICALL
Java_com_glumes_cppso_SampleNativeMethod_getNativeString(JNIEnv *env, jclass type, jstring str_) {
    
    // 生成 jstring 類型的字符串
    jstring returnValue = env->NewStringUTF("hello native string");
    // 將 jstring 類型的字符串轉換爲 C 風格的字符串,會額外申請內存
    const char *str = env->GetStringUTFChars(str_, 0);
    // 釋放掉申請的 C 風格字符串的內存
    env->ReleaseStringUTFChars(str_, str);
    // 返回 jstring 類型字符串
    return returnValue;
}

Java 層的字符串到了 JNI 就成了 jstring 類型的,但 jstring 指向的是 JVM 內部的一個字符串,它不是 C 風格的字符串 char*,所以不能像使用 C 風格字符串一樣來使用 jstring 。

JNI 支持將 jstring 轉換成 UTF 編碼和 Unicode 編碼兩種。因爲 Java 默認使用 Unicode 編碼,而 C/C++ 默認使用 UTF 編碼。

  • GetStringUTFChars(jstring string, jboolean* isCopy)
    將 jstring 轉換成 UTF 編碼的字符串
  • GetStringChars(jstring string, jboolean* isCopy)

將 jstring 轉換成 Unicode 編碼的字符串,由於 Native 層是 C/C++ 編碼,默認使用 UTF 格式,所以 GetStringChars 並不常用。

其中,jstring 類型參數就是我們需要轉換的字符串,而 isCopy 參數的值爲 JNI_TRUE 或者 JNI_FALSE ,代表是否返回 JVM 源字符串的一份拷貝。如果爲JNI_TRUE 則返回拷貝,並且要爲產生的字符串拷貝分配內存空間;如果爲JNI_FALSE 就直接返回了 JVM 源字符串的指針,意味着可以通過指針修改源字符串的內容,但這就違反了 Java 中字符串不能修改的規定,在實際開發中,直接填 NULL 就好了。

當調用完 GetStringUTFChars 方法時別忘了做完全檢查。因爲 JVM 需要爲產生的新字符串分配內存空間,如果分配失敗就會返回 NULL,並且會拋出 OutOfMemoryError 異常,所以要對 GetStringUTFChars 結果進行判斷。

當使用完 UTF 編碼的字符串時,還不能忘了釋放所申請的內存空間。調用 ReleaseStringUTFChars 方法進行釋放。

完整地轉換字符串的代碼如下

    // 申請分配內存空間,jstring 轉換爲 C 風格字符串
    const char *utfStr = env->GetStringUTFChars(str_,NULL);
    // 做檢查判斷
    if (utfStr == NULL){
        return NULL;
    }
    // 實際操作
    printf("%s",utfStr);
    // 操作結束後,釋放內存
    env->ReleaseStringUTFChars(str_,utfStr);

除了將 jstring 轉換爲 C 風格字符串,JNI 還提供了將 C 風格字符串轉換爲 jstring 類型。

通過 NewStringUTF 函數可以將 UTF 編碼的 C 風格字符串轉換爲 jstring 類型,通過 NewString 函數可以將 Unicode 編碼的 C 風格字符串轉換爲 jstring 類型。這個 jstring 類型會自動轉換成 Java 支持的 Unicode 編碼格式。

除了 jstring 和 C 風格字符串的相互轉換之外,JNI 還提供了其他的函數。

獲得源字符串的指針

在某些情況下,我們只需要獲得 Java 字符串的直接指針,而不需要把它轉換成 C 風格的字符串。

比如,一個字符串內容很大,有 1 M 多,而我們只是需要讀取字符串內容,這種情況下再把它轉換爲 C 風格字符串,不僅多此一舉(通過直接字符串指針也可以讀取內容),而且還需要爲 C 風格字符串分配內存。

爲此,JNI 提供了 GetStringCritical 和 ReleaseStringCritical 函數來返回字符串的直接指針,這樣只需要分配一個指針的內存空間就好了。

    const jchar *c_str = NULL;
    c_str = env->GetStringCritical(str_, NULL);
    
    if (c_str == NULL) {
        // error handle
    }
    env->ReleaseStringCritical(str_, c_str);

和 GetStringUTFChars 一樣,在使用完之後,還需要將分配的指針內存空間給釋放掉。

注意它的返回值指針類型是 const jchar ,而 GetStringUTFChars 函數的返回值就是 const char,這就說明 GetStringUTFChars 返回的是 C 風格字符串的指針,而 GetStringCritical 返回的是源 Java 字符串的直接指針。

另外,GetStringCritical 還有額外的限制。

在 GetStringCritical 和 ReleaseStringCritical 兩個函數之間的 Native 代碼不能調用任何會讓線程阻塞或者等待 JVM 中其他線程的 Native 函數或 JNI 函數。

因爲通過 GetStringCritical 得到的是一個指向 JVM 內部字符串的直接指針,獲取這個直接指針後會導致暫停 GC 線程,當 GC 線程被暫停後,如果其他線程觸發 GC 繼續運行的話,都會導致阻塞調用者。所以,GetStringCritical 和 ReleaseStringCritical 這對函數中間的任何本地代碼都不可以執行導致阻塞的調用或爲新對象在 JVM 中分配內存,否則,JVM 有可能死鎖。

另外還是需要檢查是否因爲內存溢出而導致返回值爲 NULL,因爲 JVM 在執行 GetStringCritical 函數時,仍有發生數據複製的可能性,尤其是當 JVM 內部存儲的數組不連續時,爲了返回一個指向連續內存空間的指針,JVM 必須複製所有數據。

獲得字符串的長度:
由於 UTF-8 編碼的字符串以 \0 結尾,而 Unicode 字符串不是,所以對於兩種編碼獲得字符串長度的函數也是不同的。

  • GetStringLength
    獲得 Unicode 編碼的字符串的長度。

  • GetStringUTFLength
    獲得 UTF-8 編碼的字符串的長度,或者使用 C 語言的 strlen 函數。

這裏的字符串指的是 Java 層的字符串,傳入的參數都是 jsting 類型,而 Java 層默認是 Unicode 編碼,所以大多使用 GetStringLength 方法。

獲得指定範圍的字符串內容

JNI 提供了函數來獲得字符串指定範圍的內容,這裏的字符串指的是 Java 層的字符串。函數會把源字符串複製到一個預先分配的緩衝區內。

  • GetStringRegion
    獲得 Unicode 編碼的字符串指定內容。
  • GetStringUTFRegion
    獲得 UTF-8 編碼的字符串指定內容。
	jchar outbuf[128],inbuf[128];
	int len = env->GetStringLength(str_);
	env->GetStringRegion(str_,0,len,outbuf);
	LOGD("%s",outbuf);

String 字符串函數操作總結

關於字符串的函數彙總

函數 描述
GetStringChars / ReleaseStringChars 獲得或釋放一個指向 Unicode 編碼的字符串的指針(指 C/C++ 字符串)
GetStringUTFChars / ReleaseStringUTFChars 獲得或釋放一個指向 UTF-8 編碼的字符串的指針(指 C/C++ 字符串)
GetStringLength 返回 Unicode 編碼的字符串的長度
getStringUTFLength 返回 UTF-8 編碼的字符串的長度
NewString 將 Unicode 編碼的 C/C++ 字符串轉換爲 Java 字符串
NewStringUTF 將 UTF-8 編碼的 C/C++ 字符串轉換爲 Java 字符串
GetStringCritical / ReleaseStringCritical 獲得或釋放一個指向字符串內容的指針(指 Java 字符串)
GetStringRegion 獲取或者設置 Unicode 編碼的字符串的指定範圍的內容
GetStringUTFRegion 獲取或者設置 UTF-8 編碼的字符串的指定範圍的內容

選擇合適的 JNI 函數
在這裏插入圖片描述
對於 JNI String 操作,要選擇合適的函數,上表可以作爲參考。

JNI靜態註冊和動態註冊


穿插一個小知識點就是,JNI有兩種註冊方式,第一種就是靜態註冊,我們創建項目的默認方式就是靜態註冊,這種可以直接使用快捷鍵快速生成,特點就是方法名必須和java中使用的位置需要對應,有一定的耦合性,還有一種就是動態註冊,它在源碼中使用的比較多,特點就是和要註冊的類文件解耦了。

給一個模板的方法

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * javaVm, void *pVoid){

    jvm = javaVm;

    JNIEnv * jniEnv = nullptr;
    jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);
    if(result != JNI_OK){
        return -1;
    }

//獲取要註冊的Java類文件的jclass類型
    const char * mainActivityClassStr = "com/narkang/cppstudy/MainActivity";
    jclass mainActivityClass = jniEnv->FindClass(mainActivityClassStr);
//jniNativeMethod爲要註冊的數組類型    
    jniEnv->RegisterNatives(mainActivityClass, jniNativeMethod, sizeof(jniNativeMethod) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_6;
}

static const JNINativeMethod jniNativeMethod[] = {
//第一個參數爲java中對應聲明的native方法,第二個爲方法簽名,第三個爲jni方法
        {"testDynamicRegisterFromJava01", "()V", (void *)(testDynamicRegister01)},
        {"testDynamicRegisterFromJava02", "()V", (void *)(testDynamicRegister02)},
        {"testActivityUiUpdateJava", "()I", (void *)(testActivityUiUpdate)},
        {"unThreadJava", "()V", (void *)(testUnThread)},
};

參考


1.Android JNI 基礎知識

2.《The Java Native Interface》

3.https://github.com/glumes/AndroidDevWithCpp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章