一.JNI與NDK的關系
1.什么是JNI杏瞻?
JNI(Java Native Interface癌压,Java本地接口)蛹批,用于打通Java層與Native層撰洗。 這不是Android系統(tǒng)所獨有的,而是Java所有腐芍。
??Java語言是跨平臺的語言差导,而這跨平臺的背后都是依靠Java虛擬機,虛擬機采用C/C++編寫猪勇,適配各個系統(tǒng)设褐,通過JNI為上層Java提供各種服務,保證跨平臺性。通俗地說助析,JNI是一種技術犀被,通過這種技術可以做到以下兩點: Java程序中的函數(shù)可以調(diào)用Native語言寫的函數(shù); Native程序中的函數(shù)可以調(diào)用Java層的函數(shù)外冀。
1.1.1 正常情況下的Android框架
最頂層是 Android的應用程序代碼, 是純Java代碼, 中間有一層的Framework框架層, 通過Framework進行系統(tǒng)調(diào)用底層的庫 和 linux 內(nèi)核;
1.1.2.使用JNI時的Android框架
繞過Framework提供的調(diào)用底層的代碼, 直接調(diào)用自己寫的C++代碼,該代碼最終會編譯成為一個動態(tài)的“.so庫(第二張圖的Native Libs)”寡键,該動態(tài)庫可以通過NDK提供的函數(shù)等工具,調(diào)用底下的C層Native Lib(上圖第三層)
2.什么是NDK?
NDK(英語:native development kit,原生開發(fā)工具包),是一種基于原生程序接口的軟件開發(fā)工具雪隧。通過此工具開發(fā)的程序直接以本地語言運行西轩,而非虛擬機。因此只有java等基于虛擬機運行的語言的程序才會有原生開發(fā)工具包脑沿。
??上面我們說過遭商,JNI是Java的一種特性,因此即便沒有NDK捅伤,我們?nèi)稳豢梢杂肅艸來寫我們的應用,那么為什么還要NDK呢巫玻?因為在此之前丛忆,在Android SDK文檔里,找不到任何JNI方面的幫助仍秤。即使第三方應用開發(fā)者使用JNI完成了自己的C++動態(tài)鏈接庫開發(fā)熄诡,但是so如何和應用程序一起打包成apk并發(fā)布?這里面也存在技術障礙诗力。
- 因此凰浮,NDK提供了一系列的工具,幫助開發(fā)者快速開發(fā)C(或C++)的動態(tài)庫苇本,并能自動將so和java應用一起打包成apk袜茧。這些工具對開發(fā)者的幫助是巨大的。
- NDK集成了交叉編譯器瓣窄,并提供了相應的mk文件隔離CPU笛厦、平臺、ABI等差異俺夕,開發(fā)人員只需要簡單修改mk文件(指出“哪些文件需要編譯”裳凸、“編譯特性要求”等),就可以創(chuàng)建出so劝贸。
二.JNI函數(shù)的動態(tài)注冊方式
1.JNI_OnLoad
在JNI中有一組特殊的函數(shù):
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);
這一組函數(shù)的作用就是負責Java方法和本地C函數(shù)的鏈接姨谷,其中JNI_OnLoad方法是在動態(tài)庫被加載時調(diào)用(調(diào)用“System.loadLibrary("so庫名字");的時候”),而JNI_OnUnload則是在本地庫被卸載時調(diào)用映九。所以這兩個函數(shù)就是一個本地庫最重要的兩個生命周期方法梦湘。
2.JNIEnv和JavaVM的區(qū)別
JNI_OnLoad方法在動態(tài)注冊時的全部代碼如下:
jint JNI_OnLoad(JavaVM* vm, void* resered){
JNIEnv* env = NULL;
if((*vm).GetEnv((void**) &env, JNI_VERSION_1_6)!=JNI_OK){
return JNI_ERR;
}
if(!registerNatives(env)){
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
注意到其中兩個類:JavaVM 和 JNIEnv
2.2.1 JavaVM 和 JNIEnv
JavaVM代表java的虛擬機。在java里,每一個process可以產(chǎn)生多個java vm對象践叠,但是在android上言缤,每一個process只有一個Dalvik虛擬機對象,也就是在android進程中是通過有且只有一個虛擬器對象來服務所有java和c/c++代碼禁灼,這個對象是線程共享的管挟。
??JNIEnv(JNI Interface Pointer)是提供JNI Native函數(shù)的基礎環(huán)境,線程相關弄捕,不同線程的JNIEnv相互獨立僻孝。
從上圖可知,JNIEnv實際上就是提供了一些JNI系統(tǒng)函數(shù)守谓。通過這些函數(shù)可以做到:
- 調(diào)用Java的函數(shù)穿铆。
- 操作jobject對象等很多事情。
如果說到這里還不明白的話斋荞,沒關系荞雏,我們之后會講JNIEnv這個指針的具體用法。
2.2.2 Java和Android中JavaVM對象有區(qū)別
在java里平酿,每一個process可以產(chǎn)生多個java vm對象凤优,但是在android上,每一個process只有一個Dalvik虛擬機對象蜈彼,也就是在android進程中是通過有且只有一個虛擬器對象來服務所有java和c/c++代碼筑辨。
Android的dex字節(jié)碼和c/c++的.so同時運行Dalvik虛擬機之內(nèi),共同使用一個進程空間幸逆。之所以可以相互調(diào)用棍辕,也是因為有Dalvik虛擬機*,Dalvik虛擬機說白了也是一個Android定制版的JVM还绘,谷歌對他做了很多優(yōu)化和調(diào)整(比如將JVM的機制堆棧尋址改為了基于寄存器)楚昭,因此它也是用C實現(xiàn)的,它的底層調(diào)的是第一張圖的第三層(系統(tǒng) Libs)拍顷。
當java代碼需要c/c++代碼時哪替,在Dalvik虛擬機加載進.so庫時,會先調(diào)用JNI_Onload()菇怀,此時就會 把JAVA VM對象的指針存儲于c層jni組件的全局環(huán)境中凭舶,在Java層調(diào)用C層的本地函數(shù)時,調(diào)用c本地函數(shù)的線程必然通過Dalvik虛擬機來調(diào)用c層的本地函數(shù)爱沟,此時帅霜,Dalvik虛擬機會為本地的C組件實例化一個JNIEnv指針,該指針指向Dalvik虛擬機的具體的函數(shù)列表
當JNI的c組件調(diào)用Java層的方法或者屬性時呼伸,也需要通過JNIEnv指針來進行調(diào)用身冀。當本地c/c++想獲得當前線程所要使用的JNIEnv時钝尸,可以使用Dalvik虛擬機對象的JavaVM jvm->GetEnv()返回當前線程所在的JNIEnv*(上面代碼也展示了):
if((*vm).GetEnv((void**) &env, JNI_VERSION_1_6)!=JNI_OK){
其中JNI_VERSION_1_6為JNI版本,這個值可以通過jint GetVersion(JNIEnv *env);
來獲取當前的JNI版本搂根,返回值是宏定義的常量珍促,我們可以使用獲取到的值與下列宏進行匹配來知道當前的版本:
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
3.動態(tài)注冊
2.3.1 JNINativeMethod
首先我們來說說JNINativeMethod,就是我們動態(tài)注冊時建立的函數(shù)映射表剩愧,將Java代碼中的native函數(shù)名和native函數(shù)對應起來:
static JNINativeMethod methods[] = {
{
"nativeBaseDataType", //Java代碼中的native函數(shù)的名字
"(SIJFDCZ)V", //方法的簽名信息,主要是參數(shù)+返回值猪叙,V表示返回類型為void
(void*)baseDataType //函數(shù)指針,指向一個native中定義的C++函數(shù)
}
}
上面建立了兩個函數(shù)對應關系仁卷,也就是JNINativeMethod數(shù)組中的兩個結(jié)構(gòu)體穴翩,每個結(jié)構(gòu)體有三個成員,第一個為Java代碼中的native函數(shù)的名字锦积,第三個為native中對應被調(diào)用的函數(shù)名字芒帕,我們來看看這兩個函數(shù):
java代碼中:
public static native void nativeBaseDataType(
short s, int i, long l, float f, double d, char c, boolean b );
C++代碼中:
void baseDataType(JNIEnv* jniEnv,jobject jobj,jshort s,
jint i, jlong l, jfloat f, jdouble d, jchar c, jboolean b){
ELOG("LearnJNI.cpp --> baseDataType:%d,%f,%c,%d",i,d,c,b);
}
這個方法展示了Java層向C++層傳遞基本類型數(shù)據(jù)》峤椋可以看到JNINativeMethod數(shù)組中結(jié)構(gòu)體的第一個元素和第三個元素就是這兩個地方方法的名字(其中第三個元素前面必須加上(void*))
我們再來看看結(jié)構(gòu)體中第二個元素背蟆,(SIJFDCZ)V
,這就是Java代碼中對應的JNI函數(shù)的簽名哮幢,也就是參數(shù)類型+返回值類型,只是這個類型有一定的對應關系:
因此public static native void nativeBaseDataType(short s, int i, long l, float f, double d, char c, boolean b );
對應(SIJFDCZ)V
淆储;public static native String nativeReturnString();
對應()Ljava/lang/String;
??同時應當注意,jni函數(shù)簽名中家浇。參數(shù)類型之間不用“;”隔開(除了String類型的標識為“ Ljava/lang/String; ”自帶“;”),直接連在一起就可以了碴裙,同時數(shù)組標識是在元素類型前面加“[”钢悲,如int[]標識為“[I”.
2.3.2 RegisterNatives
接著2中的那段代碼,看看“registerNatives()”:
static int registerNatives(JNIEnv *env) {
const char *className = "com/example/dell/growup/LearnJNI"; //指定注冊的類
return registerNativeMethods(env, className, methods, sizeof(methods) / sizeof(methods[0]));
}
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods,
int numMethods) {
jclass clazz;
clazz = (*env).FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env).RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
可以看到舔株,這里JNIEnv env排上用場了莺琳,首先className
中我們指定了注冊native函數(shù)的包名+類名,接著我們調(diào)用JNIEnv的FindClass*方法clazz = (*env).FindClass(className);
來獲取注冊native函數(shù)的類载慈,之后通過JNIEnv調(diào)用JNI函數(shù) (*env).RegisterNatives(clazz, gMethods, numMethods)
來注冊上面JNINativeMethod中定義的對應函數(shù)惭等。
當Java層通過System.loadLibrary加載完JNI動態(tài)庫后,緊接著會查找該庫中一個叫JNI_OnLoad的函數(shù)办铡,如果有辞做,就調(diào)用它,而動態(tài)注冊的工作就是在這里完成的寡具。
??所以秤茅,如果想使用動態(tài)注冊方法,就必須要實現(xiàn)JNI_OnLoad函數(shù)童叠,在這個函數(shù)中會完成動態(tài)注冊的工作框喳。靜態(tài)注冊則沒有這個要求,一般我們可以自己實現(xiàn)這個JNI_OnLoad函數(shù),并在其中做一些初始化工作(即使是靜態(tài)注冊)五垮。
我們再貼一遍JNI_OnLoad()函數(shù)代碼:
//該函數(shù)的第一個參數(shù)類型為JavaVM,是虛擬機在JNI層的代表乍惊,每個Java進程只有一個
jint JNI_OnLoad(JavaVM* vm, void* resered){
JNIEnv* env = NULL;
if((*vm).GetEnv((void**) &env, JNI_VERSION_1_6)!=JNI_OK){
return JNI_ERR;
}
if(!registerNatives(env)){
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
三.JNIEnv的各種操作
1.基本類型數(shù)據(jù)處理
其實基本類型處理上面我們已經(jīng)展示過了,這里再貼一遍代碼:
java代碼中:
public static native void nativeBaseDataType(
short s, int i, long l, float f, double d, char c, boolean b);
C++代碼中:
void baseDataType(JNIEnv* jniEnv,jobject jobj,
jshort s,jint i, jlong l, jfloat f, jdouble d, jchar c, jboolean b){
ELOG("LearnJNI.cpp --> baseDataType:%d,%f,%c,%d",i,d,c,b);
}
可以看到放仗,java中的基本數(shù)據(jù)類型润绎,在native中基本上就是在前面加了個“j”,實際上也就是在“jni.h”(NDK提供的jni開發(fā)類)中給java中的基本類型用結(jié)構(gòu)體包裝類一下:
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
從Java層往C++層傳遞基本類型數(shù)據(jù)是很容易的,只要上面函數(shù)的映射關系建立好了匙监,就跟普通函數(shù)傳參一樣凡橱。
2.String字符串處理
這里我們展示一個從java向C++傳基本類型、String亭姥、數(shù)組稼钩,并從C++中像向Java中傳遞String的例子:
Java代碼中:
void returnString(){
int i =3;
char[] c = {'J','N','I'};
String s = "learn";
String string = nativeReturnJavaString(i,s,c);
Log.e("returnString",string);
}
public static native String nativeReturnJavaString(int i, String s, char[] c);
C++代碼:
static JNINativeMethod methods[] = {
{
"nativeReturnJavaString",
"(ILjava/lang/String;[C)Ljava/lang/String;", //注意多種數(shù)據(jù)類型簽名時中間沒有分號或逗號
(void*)returnJavaString
}
}
jstring returnJavaString(JNIEnv *jniEnv, jobject jobj, jint i, jstring j_str, jcharArray j_char){
const char* c_str = NULL;
jchar* j_charArray = NULL;
jint arr_len;
jint str_len;
char buff[120] = {0};
jboolean isCopy;
arr_len = (*jniEnv).GetArrayLength(j_char);// 獲取char數(shù)組長度
str_len = (*jniEnv).GetStringLength(j_str);// 獲取String長度
j_charArray = (jchar*)malloc(sizeof(jchar)* arr_len); // 根據(jù)數(shù)組長度和數(shù)組元素的數(shù)據(jù)類型申請存放java數(shù)組元素的緩沖區(qū)
memset(j_charArray, 0,sizeof(jchar)* arr_len ); // 初始化緩沖區(qū)
(*jniEnv).GetCharArrayRegion(j_char, 0, arr_len, j_charArray); // 拷貝Java數(shù)組中的所有元素到緩沖區(qū)中
c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
sprintf(buff, "%s", c_str); //sprintf(s, "%d", 123); //把整數(shù)123打印成一個字符串保存在s中
for(int j=0; j<i; j++){
buff[str_len+j] = (char) j_charArray[j];
//ELOG("LearnJNI.cpp --> returnJavaString:%c",buff[str_len+j]);
}
free(j_charArray); // 釋放存儲數(shù)組元素的緩沖區(qū)
(*jniEnv).ReleaseStringUTFChars(j_str,c_str);
return jniEnv->NewStringUTF(buff);
}
我們知道,Java中String是一個對象达罗,他申明的對象是存放在JVM內(nèi)部數(shù)據(jù)結(jié)構(gòu)中的(堆坝撑,棧,靜態(tài)區(qū))粮揉。 JNI把Java中的所有對象當作一個C指針(對象的引用)傳遞到本地方法中巡李,這個指針指向JVM中的內(nèi)部數(shù)據(jù)結(jié)構(gòu)(即這個對象存放的地址),而內(nèi)部的數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中的存儲方式是不可見的扶认。只能從JNIEnv指針指向的函數(shù)表中選擇合適的JNI函數(shù)來操作JVM中的數(shù)據(jù)結(jié)構(gòu)侨拦。
訪問java.lang.String對應的JNI類型jstring時,沒有像訪問基本數(shù)據(jù)類型一樣直接使用辐宾,因為它在Java是一個引用類型狱从,所以在本地代碼中只能通過GetStringUTFChars這樣的JNI函數(shù)來訪問字符串的內(nèi)容。
3.2.1 const char* GetStringUTFChars(env, j_str, &isCopy)
env:JNIEnv函數(shù)表指針
j_str:jstring類型(Java傳遞給本地代碼的字符串指針)
isCopy:取值JNI_TRUE和JNI_FALSE叠纹,如果值為JNI_TRUE季研,表示返回JVM內(nèi)部源字符串的一份拷貝,并為新產(chǎn)生的字符串分配內(nèi)存空間誉察。如果值為JNI_FALSE与涡,表示返回JVM內(nèi)部源字符串的指針,意味著可以通過指針修改源字符串的內(nèi)容持偏,不推薦這么做驼卖,因為這樣做就打破了Java字符串不能修改的規(guī)定。但我們在開發(fā)當中鸿秆,并不關心這個值是多少款慨,通常情況下這個參數(shù)填NULL即可。
因為Java默認使用Unicode編碼谬莹,而C/C++默認使用UTF編碼檩奠,所以在本地代碼中操作字符串的時候桩了,必須使用合適的JNI函數(shù)把jstring轉(zhuǎn)換成C風格的字符串。JNI支持字符串在Unicode和UTF-8兩種編碼之間轉(zhuǎn)換埠戳,GetStringUTFChars可以把一個jstring指針(指向JVM內(nèi)部的Unicode字符序列)轉(zhuǎn)換成一個UTF-8格式的C字符串井誉。
3.2.2 jstring NewStringUTF(const char* bytes)
通過調(diào)用NewStringUTF函數(shù),會構(gòu)建一個新的java.lang.String字符串對象整胃。這個新創(chuàng)建的字符串會自動轉(zhuǎn)換成Java支持的Unicode編碼颗圣。
3.2.3 void ReleaseStringUTFChars(jstring string, const char* utf)
在調(diào)用GetStringUTFChars函數(shù)從JVM內(nèi)部獲取一個字符串之后,JVM內(nèi)部會分配一塊新的內(nèi)存屁使,用于存儲源字符串的拷貝在岂,以便本地代碼訪問和修改。即然有內(nèi)存分配蛮寂,用完之后馬上釋放.通過調(diào)用ReleaseStringUTFChars函數(shù)通知JVM這塊內(nèi)存已經(jīng)不使用了蔽午,你可以清除了。注意:這兩個函數(shù)是配對使用的酬蹋,用了GetXXX就必須調(diào)用ReleaseXXX及老,而且這兩個函數(shù)的命名也有規(guī)律,除了前面的Get和Release之外范抓,后面的都一樣骄恶。
3.訪問數(shù)組
??訪問數(shù)組的例子上面已經(jīng)展示過了,這里主要說明一下幾個訪問數(shù)組相關的函數(shù)的用法匕垫。
3.3.1 jsize GetArrayLength(jarray array)
??獲取數(shù)組的長度僧鲁,返回值為jint類型,我們獲取數(shù)組的類型之后就可以調(diào)用malloc函數(shù)動態(tài)的分配內(nèi)存:
jint arr_len;
arr_len = (*jniEnv).GetArrayLength(j_char);
jchar* j_charArray = NULL;
j_charArray = (jchar*)malloc(sizeof(jchar)* arr_len);
memset(j_charArray, 0,sizeof(jchar)* arr_len );
??我們需要一個jchar* 類型的指針指向我們剛分配的內(nèi)存(的首地址)象泵,以便之后調(diào)用 memset函數(shù)的時候找到這塊內(nèi)存并將其中的字節(jié)初始化為0(申請的內(nèi)存中可能有之前殘余的值)寞秃,我們來看看這個memset函數(shù):
memset() 函數(shù)用來將指定內(nèi)存的前n個字節(jié)設置為特定的值(經(jīng)常用于初始化剛剛申請的內(nèi)存),其原型為:
void * memset( void * ptr, int value, size_t num );
參數(shù)說明:
ptr 為要操作的內(nèi)存的指針单芜。
value 為要設置的值。你既可以向 value 傳遞 int 類型的值犁柜,也可以傳遞 char 類型的值洲鸠,
int 和 char 可以根據(jù) ASCII 碼相互轉(zhuǎn)換。
num 為 ptr 的前 num 個字節(jié)馋缅,size_t 就是unsigned int扒腕。
3.3.2 void GetCharArrayRegion(jcharArray array, jsize start, jsize len,jchar* buf)
...
j_charArray = (jchar*)malloc(sizeof(jchar)* arr_len);
memset(j_charArray, 0,sizeof(jchar)* arr_len ); // 初始化緩沖區(qū)
(*jniEnv).GetCharArrayRegion(j_char, 0, arr_len, j_charArray); // 拷貝Java數(shù)組中的所有元素到緩沖區(qū)中
c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
sprintf(buff, "%s", c_str); //sprintf(s, "%d", 123); //把整數(shù)123打印成一個字符串保存在s中
??GetCharArrayRegion
將Java數(shù)組j_char,拷貝到我們剛剛申請的內(nèi)存j_charArray中萤悴,之后我們調(diào)用c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
將java層傳來的 j_str(String類型)轉(zhuǎn)換成一個C風格字符串(c_str
)瘾腰,然后sprintf(buff, "%s", c_str);
是將作為C風格的字符串儲存在buff數(shù)組中。