博客原文鏈接:https://glumes.com/post/android/android-jni-basic-operation/
自從 Android Studio 升級(jí)到 2.3 版本以后,使用 CMake 進(jìn)行編譯就方便多了,不需要再寫 Android.mk 了,也不需要用 javah 來生成頭文件了狰域,直接寫好 native 方法赁严,快捷方式就可以生成對(duì)應(yīng)的 C++ 方法辫愉,只要專注寫好 C++ 代碼仅胞,CMake 就可以指定的 CPU 架構(gòu)生成對(duì)應(yīng)的 SO 庫。
JNI 和 NDK 的區(qū)別
NDK 開發(fā)難免會(huì)搞不清 JNI 和 NDK 的區(qū)別叶堆。
JNI 全稱是 Java Native Interface,即 Java 本地接口斥杜。它是用來使得 Java 語言和 C/C++ 語言相互調(diào)用的虱颗。它本身和 Android 并無關(guān)系,只是在 Android 開發(fā)中會(huì)用到蔗喂,在其他地方也會(huì)用到的忘渔。
而 NDK 的全稱是 Native Development Kit,和 SDK 的全稱是 Software Development Kit 一樣缰儿,都是開發(fā)工具包畦粮。NDK 是 Android 開發(fā)的工具包,主要用作 C/C++ 開發(fā)乖阵,提供了相關(guān)的動(dòng)態(tài)庫宣赔。
在 Android 上進(jìn)行 NDK 開發(fā)還是得先學(xué)會(huì) JNI 相關(guān)技能,先可以從 Java 層到 C/C++ 層的相互調(diào)用瞪浸,然后再學(xué)習(xí) NDK 開發(fā)的那些技巧拉背。
簡(jiǎn)單實(shí)例
在 AS 新建工程時(shí)若選擇了 Include C++ Support,就會(huì)自帶配置好的 C++ 開發(fā)環(huán)境默终。
在聲明 native 方法時(shí)還是用 Java 來寫比較好椅棺,比 Kotlin 的 external 關(guān)鍵字要友好多了,可以直接快捷鍵生成對(duì)用的 C++ 方法齐蔽。
聲明 native 方法如下:
public static native int plus(int a, int b);
快捷鍵便會(huì)生成對(duì)應(yīng)的 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;
}
這是一個(gè)簡(jiǎn)單的計(jì)算 a+b 的 native 方法两疚,但卻包含了許多基本內(nèi)容,在 C++ 層接收來自 Java 層的參數(shù)含滴,并轉(zhuǎn)換成 C++ 層的數(shù)據(jù)類型诱渤,計(jì)算之后再返回成 Java 層的數(shù)據(jù)類型。
在 Java 層中只有兩個(gè)參數(shù)谈况,而在 C++ 代碼就有四個(gè)參數(shù)了勺美,至少都會(huì)包含前面兩個(gè)參數(shù)递胧,下面講解這些參數(shù)意義。
其中:
-
env
變量是 JNIEnv 類型的對(duì)象赡茸,該對(duì)象是一個(gè) Java 虛擬機(jī)所運(yùn)行的環(huán)境缎脾,通過它可以訪問到 Java 虛擬機(jī)內(nèi)部的各種對(duì)象。
JNIEnv 類型對(duì)象參數(shù) env
JNIEnv*
是定義任意 native 函數(shù)的第一個(gè)參數(shù)占卧,它是一個(gè)指針遗菠,通過它可以訪問虛擬機(jī)內(nèi)部的各種數(shù)據(jù)結(jié)構(gòu),同時(shí)它還指向 JVM 函數(shù)表的指針华蜒,函數(shù)表中的每一個(gè)入口指向一個(gè) JNI 函數(shù)辙纬,每個(gè)函數(shù)用于訪問 JVM 中特定的數(shù)據(jù)結(jié)構(gòu)。
結(jié)構(gòu)如下圖所示:
可以看到這里面涉及了三類指針叭喜,JNIEnv * 本身就是指針贺拣,而它指向的也是指針,在 JVM 函數(shù)表里面的每一項(xiàng)又都是指針捂蕴。
jobject 參數(shù)
jobject 是 native 函數(shù)里的第二個(gè)參數(shù)類型纵柿,但卻不是一定的。
如果該 native 方法是一個(gè)靜態(tài) static 方法启绰,那么第二個(gè)參數(shù)就是 jobject 類型昂儒,指的是調(diào)用該函數(shù)的對(duì)象;
如果是一個(gè)實(shí)例方法委可,那么第二個(gè)參數(shù)就是 jclass 類型渊跋,指的是調(diào)用該函數(shù)的類。
基本數(shù)據(jù)類型轉(zhuǎn)換
在 Java 中傳遞的參數(shù)類型是 int着倾,而在 JNI 中就成了 jint拾酝,這就涉及到 Java 到 JNI 的數(shù)據(jù)類型轉(zhuǎn)換。
如下表所示:
Java 類型 | Native 類型 | 符號(hào)屬性 | 字長(zhǎng) |
---|---|---|---|
boolean | jboolean | 無符號(hào) | 8位 |
byte | jbyte | 無符號(hào) | 8位 |
char | jchar | 無符號(hào) | 16位 |
short | jshort | 有符號(hào) | 16位 |
int | jnit | 有符號(hào) | 32位 |
long | jlong | 有符號(hào) | 64位 |
float | jfloat | 有符號(hào) | 32位 |
double | jdouble | 有符號(hào) | 64位 |
我們傳遞的基本數(shù)據(jù)類型在 JNI 中都有相對(duì)的數(shù)據(jù)類型卡者。
引用數(shù)據(jù)類型轉(zhuǎn)換
除了基本數(shù)據(jù)類型之外蒿囤,引用數(shù)據(jù)類型也有著一一對(duì)應(yīng)。
Java 引用類型 | Native 類型 | Java 引用類型 | Native 類型 |
---|---|---|---|
All objects | jobject | char[] | jcharArray |
java.lang.Class | jclass | short[] | jshortArray |
java.lang.String | jstring | int[] | jintArray |
Object[] | jobjectArray | long[] | jlongArray |
boolean[] | jbooleanArray | float[] | jfloatArray |
byte[] | jbyteArray | double[] | jdoubleArray |
java.lang.Throwable | jthrowable |
可以看到崇决,除了 Java 中基本數(shù)據(jù)類型的數(shù)組材诽、Class、String 和 Throwable 外恒傻,其余所有 Java 對(duì)象的數(shù)據(jù)類型在 JNI 中都用 jobject 表示脸侥。
明白了參數(shù)類型之后,接下來就是按照正常寫代碼一樣盈厘,完成函數(shù)的返回值了睁枕。
String 字符串操作
對(duì)于基本數(shù)據(jù)類型的操作,比如 boolean、int外遇、float 等都大同小異注簿,無非是在原來的數(shù)據(jù)類型前面加了一個(gè) j
來表示 JNI 數(shù)據(jù)類型。
而對(duì)于 String 類型跳仿,必須要使用合適的 JNI 函數(shù)來將 jstring 轉(zhuǎn)變成 C/C++ 字符串诡渴。
對(duì)于下面的 Native 方法,傳入一個(gè)字符串塔嬉,并要求返回一個(gè)字符串。
public static native String getNativeString(String str);
生成的對(duì)應(yīng)的 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 類型的字符串轉(zhuǎn)換為 C 風(fēng)格的字符串租悄,會(huì)額外申請(qǐng)內(nèi)存
const char *str = env->GetStringUTFChars(str_, 0);
// 釋放掉申請(qǐng)的 C 風(fēng)格字符串的內(nèi)存
env->ReleaseStringUTFChars(str_, str);
// 返回 jstring 類型字符串
return returnValue;
}
Java 層的字符串到了 JNI 就成了 jstring 類型的谨究,但 jstring 指向的是 JVM 內(nèi)部的一個(gè)字符串,它不是 C 風(fēng)格的字符串 char*
泣棋,所以不能像使用 C 風(fēng)格字符串一樣來使用 jstring 胶哲。
JNI 支持將 jstring 轉(zhuǎn)換成 UTF 編碼和 Unicode 編碼兩種。因?yàn)?Java 默認(rèn)使用 Unicode 編碼潭辈,而 C/C++ 默認(rèn)使用 UTF 編碼鸯屿。
- GetStringUTFChars(jstring string, jboolean* isCopy)
將 jstring 轉(zhuǎn)換成 UTF 編碼的字符串
- GetStringChars(jstring string, jboolean* isCopy)
將 jstring 轉(zhuǎn)換成 Unicode 編碼的字符串,由于 Native 層是 C/C++ 編碼把敢,默認(rèn)使用 UTF 格式寄摆,所以 GetStringChars 并不常用。
其中修赞,jstring 類型參數(shù)就是我們需要轉(zhuǎn)換的字符串婶恼,而 isCopy 參數(shù)的值為 JNI_TRUE
或者 JNI_FALSE
,代表是否返回 JVM 源字符串的一份拷貝柏副。如果為JNI_TRUE
則返回拷貝勾邦,并且要為產(chǎn)生的字符串拷貝分配內(nèi)存空間;如果為JNI_FALSE
就直接返回了 JVM 源字符串的指針割择,意味著可以通過指針修改源字符串的內(nèi)容眷篇,但這就違反了 Java 中字符串不能修改的規(guī)定,在實(shí)際開發(fā)中荔泳,直接填 NULL 就好了蕉饼。
當(dāng)調(diào)用完 GetStringUTFChars 方法時(shí)別忘了做完全檢查。因?yàn)?JVM 需要為產(chǎn)生的新字符串分配內(nèi)存空間玛歌,如果分配失敗就會(huì)返回 NULL椎椰,并且會(huì)拋出 OutOfMemoryError 異常,所以要對(duì) GetStringUTFChars 結(jié)果進(jìn)行判斷沾鳄。
當(dāng)使用完 UTF 編碼的字符串時(shí)慨飘,還不能忘了釋放所申請(qǐng)的內(nèi)存空間。調(diào)用 ReleaseStringUTFChars 方法進(jìn)行釋放。
完整地轉(zhuǎn)換字符串的代碼如下:
// 申請(qǐng)分配內(nèi)存空間瓤的,jstring 轉(zhuǎn)換為 C 風(fēng)格字符串
const char *utfStr = env->GetStringUTFChars(str_,NULL);
// 做檢查判斷
if (utfStr == NULL){
return NULL;
}
// 實(shí)際操作
printf("%s",utfStr);
// 操作結(jié)束后休弃,釋放內(nèi)存
env->ReleaseStringUTFChars(str_,utfStr);
除了將 jstring 轉(zhuǎn)換為 C 風(fēng)格字符串,JNI 還提供了將 C 風(fēng)格字符串轉(zhuǎn)換為 jstring 類型圈膏。
通過 NewStringUTF 函數(shù)可以將 UTF 編碼的 C 風(fēng)格字符串轉(zhuǎn)換為 jstring 類型塔猾,通過 NewString 函數(shù)可以將 Unicode 編碼的 C 風(fēng)格字符串轉(zhuǎn)換為 jstring 類型。這個(gè) jstring 類型會(huì)自動(dòng)轉(zhuǎn)換成 Java 支持的 Unicode 編碼格式稽坤。
除了 jstring 和 C 風(fēng)格字符串的相互轉(zhuǎn)換之外丈甸,JNI 還提供了其他的函數(shù)。
獲得源字符串的指針
在某些情況下尿褪,我們只需要獲得 Java 字符串的直接指針睦擂,而不需要把它轉(zhuǎn)換成 C 風(fēng)格的字符串。
比如杖玲,一個(gè)字符串內(nèi)容很大顿仇,有 1 M 多,而我們只是需要讀取字符串內(nèi)容摆马,這種情況下再把它轉(zhuǎn)換為 C 風(fēng)格字符串臼闻,不僅多此一舉(通過直接字符串指針也可以讀取內(nèi)容),而且還需要為 C 風(fēng)格字符串分配內(nèi)存囤采。
為此述呐,JNI 提供了 GetStringCritical 和 ReleaseStringCritical 函數(shù)來返回字符串的直接指針,這樣只需要分配一個(gè)指針的內(nèi)存空間就好了蕉毯。
const jchar *c_str = NULL;
c_str = env->GetStringCritical(str_, NULL);
if (c_str == NULL) {
// error handle
}
env->ReleaseStringCritical(str_, c_str);
和 GetStringUTFChars 一樣市埋,在使用完之后,還需要將分配的指針內(nèi)存空間給釋放掉恕刘。
注意它的返回值指針類型是 const jchar *
缤谎,而 GetStringUTFChars 函數(shù)的返回值就是 const char*
,這就說明 GetStringUTFChars 返回的是 C 風(fēng)格字符串的指針褐着,而 GetStringCritical 返回的是源 Java 字符串的直接指針坷澡。
另外,GetStringCritical 還有額外的限制含蓉。
在 GetStringCritical 和 ReleaseStringCritical 兩個(gè)函數(shù)之間的 Native 代碼不能調(diào)用任何會(huì)讓線程阻塞或者等待 JVM 中其他線程的 Native 函數(shù)或 JNI 函數(shù)频敛。
因?yàn)橥ㄟ^ GetStringCritical 得到的是一個(gè)指向 JVM 內(nèi)部字符串的直接指針,獲取這個(gè)直接指針后會(huì)導(dǎo)致暫停 GC 線程馅扣,當(dāng) GC 線程被暫停后斟赚,如果其他線程觸發(fā) GC 繼續(xù)運(yùn)行的話,都會(huì)導(dǎo)致阻塞調(diào)用者差油。所以拗军,GetStringCritical 和 ReleaseStringCritical 這對(duì)函數(shù)中間的任何本地代碼都不可以執(zhí)行導(dǎo)致阻塞的調(diào)用或?yàn)樾聦?duì)象在 JVM 中分配內(nèi)存任洞,否則,JVM 有可能死鎖发侵。
另外還是需要檢查是否因?yàn)閮?nèi)存溢出而導(dǎo)致返回值為 NULL交掏,因?yàn)?JVM 在執(zhí)行 GetStringCritical 函數(shù)時(shí),仍有發(fā)生數(shù)據(jù)復(fù)制的可能性刃鳄,尤其是當(dāng) JVM 內(nèi)部存儲(chǔ)的數(shù)組不連續(xù)時(shí)盅弛,為了返回一個(gè)指向連續(xù)內(nèi)存空間的指針,JVM 必須復(fù)制所有數(shù)據(jù)叔锐。
獲得字符串的長(zhǎng)度:
由于 UTF-8 編碼的字符串以 \0
結(jié)尾挪鹏,而 Unicode 字符串不是,所以對(duì)于兩種編碼獲得字符串長(zhǎng)度的函數(shù)也是不同的愉烙。
- GetStringLength
獲得 Unicode 編碼的字符串的長(zhǎng)度讨盒。
- GetStringUTFLength
獲得 UTF-8 編碼的字符串的長(zhǎng)度,或者使用 C 語言的 strlen 函數(shù)齿梁。
這里的字符串指的是 Java 層的字符串催植,傳入的參數(shù)都是 jsting 類型肮蛹,而 Java 層默認(rèn)是 Unicode 編碼勺择,所以大多使用 GetStringLength 方法。
獲得指定范圍的字符串內(nèi)容
JNI 提供了函數(shù)來獲得字符串指定范圍的內(nèi)容伦忠,這里的字符串指的是 Java 層的字符串省核。函數(shù)會(huì)把源字符串復(fù)制到一個(gè)預(yù)先分配的緩沖區(qū)內(nèi)。
- GetStringRegion
獲得 Unicode 編碼的字符串指定內(nèi)容昆码。
- GetStringUTFRegion
獲得 UTF-8 編碼的字符串指定內(nèi)容气忠。
jchar outbuf[128],inbuf[128];
int len = env->GetStringLength(str_);
env->GetStringRegion(str_,0,len,outbuf);
LOGD("%s",outbuf);
String 字符串函數(shù)操作總結(jié)
關(guān)于字符串的函數(shù)匯總
JNI 函數(shù) | 描述 |
---|---|
GetStringChars / ReleaseStringChars | 獲得或釋放一個(gè)指向 Unicode 編碼的字符串的指針(指 C/C++ 字符串) |
GetStringUTFChars / ReleaseStringUTFChars | 獲得或釋放一個(gè)指向 UTF-8 編碼的字符串的指針(指 C/C++ 字符串) |
GetStringLength | 返回 Unicode 編碼的字符串的長(zhǎng)度 |
getStringUTFLength | 返回 UTF-8 編碼的字符串的長(zhǎng)度 |
NewString | 將 Unicode 編碼的 C/C++ 字符串轉(zhuǎn)換為 Java 字符串 |
NewStringUTF | 將 UTF-8 編碼的 C/C++ 字符串轉(zhuǎn)換為 Java 字符串 |
GetStringCritical / ReleaseStringCritical | 獲得或釋放一個(gè)指向字符串內(nèi)容的指針(指 Java 字符串) |
GetStringRegion | 獲取或者設(shè)置 Unicode 編碼的字符串的指定范圍的內(nèi)容 |
GetStringUTFRegion | 獲取或者設(shè)置 UTF-8 編碼的字符串的指定范圍的內(nèi)容 |
選擇合適的 JNI 函數(shù)
對(duì)于 JNI String 操作,要選擇合適的函數(shù)赋咽,上表可以作為參考旧噪。
具體詳情代碼可以參考我的 Github 地址:
https://github.com/glumes/AndroidDevWithCpp
參考
- 《The Java Native Interface》
一起交流學(xué)習(xí),答疑解惑脓匿,有問題淘钟,我們星球見~~~
歡迎關(guān)注微信公眾號(hào):【紙上淺談】,獲得最新文章推送~~