文章測(cè)試案例提交到Github:learnNdk
有了第一篇內(nèi)容的基礎(chǔ)之后,我們開(kāi)始正式學(xué)習(xí)JNI。如果前面一片文章你已經(jīng)寫(xiě)出來(lái)了一個(gè)demo,但是還有很多有疑問(wèn)的地方,沒(méi)關(guān)系,你可以把你的疑問(wèn)記下來(lái),在接下來(lái)的學(xué)習(xí)中,我們將會(huì)慢慢解開(kāi)這些疑問(wèn)。首先我們看一張圖。

這張圖很明晰的表達(dá)出了JNI在NDK編程中所擔(dān)任的角色,以及JNI在和java虛擬機(jī)的關(guān)系。很顯然它屬于java虛擬機(jī)的一部分。
我們知道在Java中一般有兩種類型的方法,一個(gè)是instance方法,一個(gè)是類方法,在JNI對(duì)應(yīng)的函數(shù)里面一般至少都會(huì)有兩個(gè)參數(shù),一個(gè)是JNIEnv,一個(gè)是jobject或者jclass,其中第二個(gè)參數(shù)的不同,就是對(duì)應(yīng)著Java中的方法是所屬于某個(gè)對(duì)象還是所屬于這個(gè)類。這兩個(gè)參數(shù)會(huì)在JNIEnv方法調(diào)用的時(shí)候有些地方會(huì)用到。
我們使用NDK編程的目的其實(shí)就是為了用C/C++代碼來(lái)幫助我們實(shí)現(xiàn)java里不好實(shí)現(xiàn)或者不方便實(shí)現(xiàn)的內(nèi)容,問(wèn)題說(shuō)的通俗一點(diǎn)NDK編程其實(shí)就是java如何與C/++進(jìn)行數(shù)據(jù)通信。
通過(guò)上圖我們了解到,java想要和C/C++進(jìn)行數(shù)據(jù)通信,需要經(jīng)過(guò)JNI層進(jìn)行橋梁轉(zhuǎn)換,也就是說(shuō)我們的java層數(shù)據(jù)想要傳遞到C/C++層,首先要經(jīng)過(guò)JNI層轉(zhuǎn)換后才能到C/C++層。同理,C/C++層數(shù)據(jù)想要傳遞給java層也是如此。
不同語(yǔ)言層級(jí)之間進(jìn)行數(shù)據(jù)交互,必然涉及到數(shù)據(jù)類型的轉(zhuǎn)換。不對(duì)等的數(shù)據(jù)類型是無(wú)法進(jìn)行數(shù)據(jù)交互的,即使可以,也容易導(dǎo)致bug甚至錯(cuò)誤的發(fā)生。
下面我們就來(lái)看看這個(gè)三個(gè)層級(jí)之間數(shù)據(jù)類型是如何轉(zhuǎn)換的。
基本數(shù)據(jù)類型轉(zhuǎn)換
我們知道在java里數(shù)據(jù)類型分成:基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。
基本數(shù)據(jù)類型有8種分別是:boolean,byte,char,short,int,long,float,double。
三者的對(duì)應(yīng)關(guān)系如下表:
| JavaType | JNIType | C/C++ Type |
|---|---|---|
| boolean | jboolean | uint8_t(unsigned char) |
| byte | jbyte | int8_t (signed char) |
| char | jchar | uint16_t (unsigned short) |
| short | jshort | int16_t (short) |
| int | jint | int32_t (int) |
| long | jlong | int64_t (long) |
| float | jfloat | float |
| double | jdouble | double |
上面的基本類型數(shù)據(jù)在同等對(duì)應(yīng)之間是可以直接轉(zhuǎn)換的,舉一個(gè)例子:我們以int類型進(jìn)行舉例,其他類型類比如此就行了,還是我們的add函數(shù)。
java中的原型:
public native int add(int a ,int b);
這個(gè)java方法向JNI層傳遞了兩個(gè)int類型的參數(shù),同時(shí)需要從JNI層,返回一個(gè)int類型的參數(shù)。這個(gè)參數(shù)傳遞到JNI層是怎樣轉(zhuǎn)換的呢?記住,我們并不能直接傳遞到C/C++層,總是從Java->JNI->C/C++。雖然很多JNI的代碼放在C/C++文件,但是這部分代碼卻屬于JNI層。
接下來(lái)我們就來(lái)看看JNI層的代碼
JNIEXPORT jint JNICALL
Java_com_sivin_ndkdemo_NormalJni_add(JNIEnv *env, jobject instance, jint a ,jint b)
從個(gè)函數(shù)中我們發(fā)現(xiàn)int類型的參數(shù)轉(zhuǎn)成了jint類型的參數(shù),同時(shí)返回的類型也是jint類型。
那么如何在將jni層的數(shù)據(jù)傳遞geiC/C++層呢,我們來(lái)看看代碼實(shí)現(xiàn)
extern "C"
int add(int a ,int b){
return a+b;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_sivin_opengles_1andorid_MainActivity_add(JNIEnv *env, jobject instance, jint a,
jint b) {
jint sum = add(a,b);
return sum;
}
為了明顯的突出三個(gè)層級(jí)之間的關(guān)系,我們特意在native文件里寫(xiě)了一個(gè)C語(yǔ)言實(shí)現(xiàn)的函數(shù),從函數(shù)中我們可以看到jint類型直接轉(zhuǎn)換成C層的int類型,反之亦然,C層的返回的int類型也直接轉(zhuǎn)換成jint然后又返回給了java層的int類型。
我們想強(qiáng)調(diào)的一點(diǎn)是上面的類型轉(zhuǎn)是java基本數(shù)據(jù)類型才能這樣做,其他的數(shù)據(jù)類型是如何轉(zhuǎn)換的呢?
引用數(shù)據(jù)類型轉(zhuǎn)換
清楚java語(yǔ)言的都知道在java中,除了基本數(shù)據(jù)類型,剩下就是引用數(shù)據(jù)類型:
Refrenece 數(shù)據(jù)類型
Java的引用數(shù)據(jù)類型并不像原始數(shù)據(jù)類型一樣轉(zhuǎn)換到JNI層之后可以直接被C/C++使用,它需要經(jīng)過(guò)再次的變換,使之可以與C/C++進(jìn)行數(shù)據(jù)交互,JNI提供了一系列的API來(lái)幫助我們完成這些變換,這些API通過(guò)JNIEnv獲取,并調(diào)用。
JAVA中的都有哪些引用數(shù)據(jù)類型呢?
- object
- class
- throwable
- String
- Arrays
- NIO Buffers
- Fields
- Methods
上面我們可以完全用object代替所有,但是這里我們并不打算這樣做,上面的object僅代表普通的java對(duì)象。因?yàn)樵?code>JNI中不同的引用數(shù)據(jù)類型對(duì)應(yīng)著不同的JNI數(shù)據(jù)類型。具體對(duì)應(yīng)我們看下表:
JNI引用數(shù)據(jù)類型對(duì)應(yīng)表
| JavaType | JNIType |
|---|---|
| java.lang.Class | jclass |
| java.lang.Throwable | jthrwoable |
| java.lang.String | jstring |
| other objects | jobject |
| object[ ] | jobjectArray |
| 基本數(shù)據(jù)類型[ ] (例如:int[ ]) | j基本數(shù)據(jù)類型Array (例如:jintArray) |
| other arrays | jarray |
看上面的這個(gè)表,不懂的人看的是一頭霧水,最顯而易見(jiàn)的疑惑是,怎么沒(méi)有C/C++對(duì)應(yīng)關(guān)系。沒(méi)關(guān)系,我們下面的學(xué)習(xí)就知道了。上面的這個(gè)表我們就大致看一下,有一個(gè)整體感知就行了,下面我們就來(lái)具體的針對(duì)每一個(gè)對(duì)應(yīng)關(guān)系進(jìn)行解釋說(shuō)明。
首先我們就來(lái)從最基本的String類型說(shuō)起,有人怕是要問(wèn),為什么從String而不是object。我想說(shuō)問(wèn)的好,解釋一下,很簡(jiǎn)單因?yàn)樗S枚姨厥?,同時(shí)JNI為String也提供專有的數(shù)據(jù)類型映射和一些處理函數(shù)。后面學(xué)習(xí),我們會(huì)知道普通的object類型的映射還需要用到其他的知識(shí),而string則相對(duì)更集中一些,同時(shí)處理字符串應(yīng)該是每一個(gè)編程語(yǔ)言的一個(gè)很重要的任務(wù)。因此我們首先從String類說(shuō)起。
String 操作
首先我們回顧一下java String類的一些基本知識(shí),首先java.lang.String類使用了final修飾,不能被繼承。即雙引號(hào)括起的字符串,如"abc",都是作為String類的實(shí)例實(shí)現(xiàn)的。String是常量,其對(duì)象一旦構(gòu)造就不能再被改變,換句話說(shuō),String對(duì)象是不可變的,每一個(gè)看起來(lái)會(huì)修改String值的方法,實(shí)際上都是創(chuàng)造了一個(gè)全新的String對(duì)象,而最初的String對(duì)象則絲毫未動(dòng)。String對(duì)象具有只讀特性,指向它的任何引用都不可能改變它的值,因此,也不會(huì)對(duì)其他的引用有什么影響。但是字符串引用可以重新賦值。
基本的java String的一些基礎(chǔ)知識(shí)我們就說(shuō)這么多,如果對(duì)這方面還有疑問(wèn)的建議好好復(fù)習(xí)一下這方面的知識(shí)。
我們通過(guò)上面的表可以知道java中的String類型的數(shù)據(jù)傳遞到JNI層后,就轉(zhuǎn)變成了jstring數(shù)據(jù)類型。但是有一個(gè)問(wèn)題,我們并不知道jstring數(shù)據(jù)類型該如何在C/C++中處理,記住我們的主線,總是java層--> JNI層-->C/C++層,然后在反過(guò)來(lái)。那么jstring是如何傳遞到C/C++層的呢?
我們知道在C里面我們處理字符串使用過(guò)char *或者char [ ],當(dāng)然在C++里面還有string類,這些可以向基本類型一樣直接進(jìn)行轉(zhuǎn)換嗎?答案當(dāng)然是不能。
既然不能直接轉(zhuǎn)換,肯定有轉(zhuǎn)換的方式,否則我們就沒(méi)法繼續(xù)編程了。是的,在前面我們提到過(guò),每一個(gè)JNI函數(shù)都有一個(gè)JNIEnv *的參數(shù)。這個(gè)參數(shù)里可以得到很多函數(shù)指針,通過(guò)這些函數(shù),我們就可以讓Java和C/C++進(jìn)行數(shù)據(jù)通信。
因?yàn)?code>Java的String對(duì)象是不可變的,因此JNI并不提供任何修改Java中已經(jīng)存在String類型數(shù)據(jù)內(nèi)容的方法。
JNI同樣支持Unicode和UTF-8編碼的String,并且提供了兩組方法集,來(lái)處理這些編碼的字符串.
我們來(lái)看看那個(gè)方法能將jstring-->轉(zhuǎn)換到C/C++層可用的數(shù)據(jù),我們打開(kāi)jni.h頭文件,找到JNIEnv結(jié)構(gòu)體??纯蠢锩嬗袥](méi)有相關(guān)的方法,怎么找?很簡(jiǎn)單,想要將jstring變換到C/C++一定是通過(guò)一個(gè)函數(shù),我們先看返回值,看看有沒(méi)有返回char *相關(guān)的函數(shù):
找了一圈,我們發(fā)現(xiàn)了下面這個(gè)相關(guān)的函數(shù)。
//將jvm內(nèi)Unicode字符轉(zhuǎn)換成UTF-8的字符串
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
在解釋這個(gè)函數(shù)之前,我們先來(lái)補(bǔ)充一個(gè)知識(shí)點(diǎn),在java中字符串在內(nèi)存中是采用unicode編碼方式存放的:任何一個(gè)字符對(duì)應(yīng)兩個(gè)字節(jié)的定長(zhǎng)編碼。即任何一個(gè)字符(無(wú)論中文還是英文)都算一個(gè)字符長(zhǎng)度,占用兩個(gè)字節(jié)。。UTF-8字符串使用一種向上兼容7-bit ASCII字符串的編碼協(xié)議。UTF-8字符串很像NULL結(jié)尾的C字符串,在包含非ASCII字符的時(shí)候依然如此。所有的7-bitASCII字符的值都在1~127之間,這些值在UTF-8編碼中保持原樣。一個(gè)字節(jié)如果最高位被設(shè)置了,意味著這是一個(gè)多字節(jié)字符(16-bitUnicode值)。
一般情況下我們使用GetStringUTFChars函數(shù)進(jìn)行轉(zhuǎn)換成C的字符串,細(xì)心的你可能在尋找的時(shí)候還會(huì)發(fā)現(xiàn)另外一個(gè)函數(shù)GetStringChars(this, string, isCopy),并且看到它的的返回值是jchar類型,這個(gè)函數(shù)返回的字符是Unicode編碼的,一個(gè)字符占用兩個(gè)字節(jié),對(duì)應(yīng)到C/C++就是short,對(duì)應(yīng)到jni就是jchar由于jchar是一個(gè)16位的short類型,無(wú)法直接轉(zhuǎn)換成C類型的字符串。因此我們一般不使用這個(gè)函數(shù)。
/**
*jstring:從java層傳遞轉(zhuǎn)換過(guò)來(lái)的string類型數(shù)據(jù)
*jbooean:表示當(dāng)我們調(diào)用這個(gè)函數(shù)時(shí)將jstring轉(zhuǎn)成成C字符串,是內(nèi)存的直接指向還是,復(fù)制了一份,這里我們一般不關(guān)心它是怎么來(lái)的,因此我們一般在開(kāi)發(fā)的過(guò)程中可以直接傳遞一個(gè)NULL或者nullptr
*/
const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
需要注意一點(diǎn)是,調(diào)用這個(gè)函數(shù)我們將會(huì)得到一個(gè)C/C++可以處理的字符串,究竟這個(gè)函數(shù)是將字符串的值復(fù)制一份到C/C++,還是直接將內(nèi)存地址指向,是由java虛擬機(jī)實(shí)現(xiàn)機(jī)制決定的,但是作為開(kāi)發(fā)者,我們應(yīng)該遵循一個(gè)規(guī)則我們總應(yīng)該認(rèn)為他是復(fù)制一份數(shù)據(jù)到C/C++,這樣做至少不會(huì)有錯(cuò)。既然我們認(rèn)為是復(fù)制一份數(shù)據(jù)到C/C++,那么這份內(nèi)存空間就應(yīng)該需要我們自己管理了,否則可能會(huì)引發(fā)內(nèi)存泄露,因此我們?cè)诓恍枰@份內(nèi)存數(shù)據(jù)之后應(yīng)該將這份內(nèi)存釋放掉。
如何釋放內(nèi)存呢,是不是像C/C++一樣使用free或者delete呢?因?yàn)槲覀儾磺宄?code>JNI到C/C++是如何轉(zhuǎn)換的,因此如果直接使用free顯然是不合適的,同樣JNI為我們提供相關(guān)的釋放這段字符串內(nèi)存的方法。
/**
*jstring :是從java層傳遞過(guò)來(lái)的jstring
*char * :由jstring轉(zhuǎn)換成的字符串
ReleaseStringUTFChars(jstring ,const char *);
轉(zhuǎn)換和釋放都已經(jīng)說(shuō)完了,剩下的我們就可以利用C/C++相關(guān)的東西來(lái)處理我們的業(yè)務(wù)了。在處理完成之后,我們想將我們處理的結(jié)果在返回給java層。這一步如何實(shí)現(xiàn)呢?
顯然我們需要將char *數(shù)據(jù)轉(zhuǎn)換成jstring然后JNI就會(huì)將jstring傳遞到j(luò)ava層轉(zhuǎn)換成String。
我們知道在java層String實(shí)例在Java中可以通過(guò)new 的方式被實(shí)例化出來(lái),那么在JNI層中是否有方式也能創(chuàng)造一個(gè)jstring然后傳遞到java層呢?顯然是有的,同樣我們可以查閱JNIEnv *,w其中NewString和一個(gè)NewStringUTF函數(shù),這個(gè)兩個(gè)函數(shù)的區(qū)別和上面說(shuō)的一樣,這里我們使用newStringUTF這個(gè)函數(shù),同樣有一個(gè)問(wèn)題,我們可以用C/C++分配的char *來(lái)創(chuàng)造jstring對(duì)象,那么這個(gè)塊內(nèi)存空間是否需要釋放呢?是return前釋放還是return后釋放呢?顯然我們不能在return前釋放,因?yàn)獒尫帕藘?nèi)存,我們java層如何處理呢?。在return后釋放?,這個(gè)更不現(xiàn)實(shí)了,這段代碼就不會(huì)執(zhí)行。那么該怎么辦呢?答案是,不用管理這塊內(nèi)存,因?yàn)槲覀冝D(zhuǎn)成jstring之后傳遞給了java虛擬機(jī),這塊內(nèi)存空間就由java虛擬機(jī)自己管理了。
示例代碼如下:
jstring javaString;
javaString = (*env)->NewStringUTF(env, "Hello World!");
return javaString
這個(gè)方法傳入一個(gè)c類型的字符串,返回一個(gè)Java類型的字符,由于可能會(huì)由于內(nèi)存空間不足,因此,這個(gè)函數(shù)將會(huì)返回NULL阻止Native code繼續(xù)運(yùn)行,同時(shí)會(huì)拋出一個(gè)異常.
這樣我們就把一個(gè)字符串處理流程就講解完了,當(dāng)然還有很多其他的細(xì)節(jié)我們沒(méi)有講到,如unicode字符串等,這里我們后續(xù)在補(bǔ)充,因?yàn)樗€涉及到字符編碼問(wèn)題。這里我們知道有這么回事就行了。但是這已經(jīng)可以滿足我們常規(guī)的處理需求了。