Android JNI 基礎(chǔ)知識(shí)

博客原文鏈接: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)如下圖所示:

image

可以看到這里面涉及了三類指針叭喜,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ù)

image

對(duì)于 JNI String 操作,要選擇合適的函數(shù)赋咽,上表可以作為參考旧噪。

具體詳情代碼可以參考我的 Github 地址:
https://github.com/glumes/AndroidDevWithCpp

參考


  1. 《The Java Native Interface》

一起交流學(xué)習(xí),答疑解惑脓匿,有問題淘钟,我們星球見~~~


圖形/圖像/音視頻交流

歡迎關(guān)注微信公眾號(hào):【紙上淺談】,獲得最新文章推送~~

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末陪毡,一起剝皮案震驚了整個(gè)濱河市米母,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌毡琉,老刑警劉巖铁瞒,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異桅滋,居然都是意外死亡慧耍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜂绎,“玉大人栅表,你說我怎么就攤上這事∈υ妫” “怎么了怪瓶?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)践美。 經(jīng)常有香客問我洗贰,道長(zhǎng),這世上最難降的妖魔是什么陨倡? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任敛滋,我火速辦了婚禮,結(jié)果婚禮上兴革,老公的妹妹穿的比我還像新娘绎晃。我一直安慰自己,他們只是感情好杂曲,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布庶艾。 她就那樣靜靜地躺著,像睡著了一般擎勘。 火紅的嫁衣襯著肌膚如雪咱揍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天棚饵,我揣著相機(jī)與錄音煤裙,去河邊找鬼。 笑死噪漾,一個(gè)胖子當(dāng)著我的面吹牛硼砰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播欣硼,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼题翰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了分别?” 一聲冷哼從身側(cè)響起遍愿,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耘斩,沒想到半個(gè)月后沼填,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡括授,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年坞笙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了岩饼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡薛夜,死狀恐怖籍茧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梯澜,我是刑警寧澤寞冯,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站晚伙,受9級(jí)特大地震影響吮龄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咆疗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一漓帚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧午磁,春花似錦尝抖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至喧半,卻和暖如春奴迅,著一層夾襖步出監(jiān)牢的瞬間青责,已是汗流浹背挺据。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脖隶,地道東北人扁耐。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像产阱,于是被迫代替她去往敵國(guó)和親婉称。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容