Android NDK 編程指南(二)

文章測(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)。首先我們看一張圖。

JNI.png

這張圖很明晰的表達(dá)出了JNINDK編程中所擔(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í)JNIString也提供專有的數(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ù),我們就可以讓JavaC/C++進(jìn)行數(shù)據(jù)通信。

因?yàn)?code>Java的String對(duì)象是不可變的,因此JNI并不提供任何修改Java中已經(jīng)存在String類型數(shù)據(jù)內(nèi)容的方法。

JNI同樣支持UnicodeUTF-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。

我們知道在javaString實(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ī)的處理需求了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,612評(píng)論 19 139
  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全稱 Java Native Interface,Java...
    cfanr閱讀 58,514評(píng)論 9 133
  • 賽場(chǎng)上你是為國(guó)爭(zhēng)光的戰(zhàn)士, 你自信,堅(jiān)強(qiáng); 跨過(guò)一座座的大山,解決一個(gè)個(gè)的難題, 終站上屬于你的領(lǐng)獎(jiǎng)臺(tái)。 賽場(chǎng)下你...
    梅思閱讀 282評(píng)論 1 5
  • 雪遇暖則化,雨遇云則起, 爐中暖正生,穹下云正集。 想你是濕,是冷,是多情。 予你暖熱,你蹤影暗匿, 予你歡鬧,你...
    張一秋閱讀 280評(píng)論 0 1
  • 落葉逝去,漸漸你我之間好似有一層薄薄的細(xì)紗間隔。雖朦朧如畫(huà),可怎視廬山。忽冷忽熱,由遠(yuǎn)及近,由清晰到模糊,漸...
    微初見(jiàn)閱讀 209評(píng)論 0 0

友情鏈接更多精彩內(nèi)容