一.JNI與NDK的關系
1.什么是JNI?
JNI(Java Native Interface,Java本地接口),用于打通Java層與Native層。 這不是Android系統所獨有的,而是Java所有。
??Java語言是跨平臺的語言,而這跨平臺的背后都是依靠Java虛擬機,虛擬機采用C/C++編寫,適配各個系統,通過JNI為上層Java提供各種服務,保證跨平臺性。通俗地說,JNI是一種技術,通過這種技術可以做到以下兩點: Java程序中的函數可以調用Native語言寫的函數; Native程序中的函數可以調用Java層的函數。
1.1.1 正常情況下的Android框架
最頂層是 Android的應用程序代碼, 是純Java代碼, 中間有一層的Framework框架層, 通過Framework進行系統調用底層的庫 和 linux 內核;
1.1.2.使用JNI時的Android框架
繞過Framework提供的調用底層的代碼, 直接調用自己寫的C++代碼,該代碼最終會編譯成為一個動態的“.so庫(第二張圖的Native Libs)”,該動態庫可以通過NDK提供的函數等工具,調用底下的C層Native Lib(上圖第三層)
2.什么是NDK?
NDK(英語:native development kit,原生開發工具包),是一種基于原生程序接口的軟件開發工具。通過此工具開發的程序直接以本地語言運行,而非虛擬機。因此只有java等基于虛擬機運行的語言的程序才會有原生開發工具包。
??上面我們說過,JNI是Java的一種特性,因此即便沒有NDK,我們任然可以用C艸來寫我們的應用,那么為什么還要NDK呢?因為在此之前,在Android SDK文檔里,找不到任何JNI方面的幫助。即使第三方應用開發者使用JNI完成了自己的C++動態鏈接庫開發,但是so如何和應用程序一起打包成apk并發布?這里面也存在技術障礙。
- 因此,NDK提供了一系列的工具,幫助開發者快速開發C(或C++)的動態庫,并能自動將so和java應用一起打包成apk。這些工具對開發者的幫助是巨大的。
- NDK集成了交叉編譯器,并提供了相應的mk文件隔離CPU、平臺、ABI等差異,開發人員只需要簡單修改mk文件(指出“哪些文件需要編譯”、“編譯特性要求”等),就可以創建出so。
二.JNI函數的動態注冊方式
1.JNI_OnLoad
在JNI中有一組特殊的函數:
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);
這一組函數的作用就是負責Java方法和本地C函數的鏈接,其中JNI_OnLoad方法是在動態庫被加載時調用(調用“System.loadLibrary("so庫名字");的時候”),而JNI_OnUnload則是在本地庫被卸載時調用。所以這兩個函數就是一個本地庫最重要的兩個生命周期方法。
2.JNIEnv和JavaVM的區別
JNI_OnLoad方法在動態注冊時的全部代碼如下:
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可以產生多個java vm對象,但是在android上,每一個process只有一個Dalvik虛擬機對象,也就是在android進程中是通過有且只有一個虛擬器對象來服務所有java和c/c++代碼,這個對象是線程共享的。
??JNIEnv(JNI Interface Pointer)是提供JNI Native函數的基礎環境,線程相關,不同線程的JNIEnv相互獨立。
從上圖可知,JNIEnv實際上就是提供了一些JNI系統函數。通過這些函數可以做到:
- 調用Java的函數。
- 操作jobject對象等很多事情。
如果說到這里還不明白的話,沒關系,我們之后會講JNIEnv這個指針的具體用法。
2.2.2 Java和Android中JavaVM對象有區別
在java里,每一個process可以產生多個java vm對象,但是在android上,每一個process只有一個Dalvik虛擬機對象,也就是在android進程中是通過有且只有一個虛擬器對象來服務所有java和c/c++代碼。
Android的dex字節碼和c/c++的.so同時運行Dalvik虛擬機之內,共同使用一個進程空間。之所以可以相互調用,也是因為有Dalvik虛擬機*,Dalvik虛擬機說白了也是一個Android定制版的JVM,谷歌對他做了很多優化和調整(比如將JVM的機制堆棧尋址改為了基于寄存器),因此它也是用C實現的,它的底層調的是第一張圖的第三層(系統 Libs)。
當java代碼需要c/c++代碼時,在Dalvik虛擬機加載進.so庫時,會先調用JNI_Onload(),此時就會 把JAVA VM對象的指針存儲于c層jni組件的全局環境中,在Java層調用C層的本地函數時,調用c本地函數的線程必然通過Dalvik虛擬機來調用c層的本地函數,此時,Dalvik虛擬機會為本地的C組件實例化一個JNIEnv指針,該指針指向Dalvik虛擬機的具體的函數列表
當JNI的c組件調用Java層的方法或者屬性時,也需要通過JNIEnv指針來進行調用。當本地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.動態注冊
2.3.1 JNINativeMethod
首先我們來說說JNINativeMethod,就是我們動態注冊時建立的函數映射表,將Java代碼中的native函數名和native函數對應起來:
static JNINativeMethod methods[] = {
{
"nativeBaseDataType", //Java代碼中的native函數的名字
"(SIJFDCZ)V", //方法的簽名信息,主要是參數+返回值,V表示返回類型為void
(void*)baseDataType //函數指針,指向一個native中定義的C++函數
}
}
上面建立了兩個函數對應關系,也就是JNINativeMethod數組中的兩個結構體,每個結構體有三個成員,第一個為Java代碼中的native函數的名字,第三個為native中對應被調用的函數名字,我們來看看這兩個函數:
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++層傳遞基本類型數據。可以看到JNINativeMethod數組中結構體的第一個元素和第三個元素就是這兩個地方方法的名字(其中第三個元素前面必須加上(void*))
我們再來看看結構體中第二個元素,(SIJFDCZ)V
,這就是Java代碼中對應的JNI函數的簽名,也就是參數類型+返回值類型,只是這個類型有一定的對應關系:
因此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函數簽名中。參數類型之間不用“;”隔開(除了String類型的標識為“ Ljava/lang/String; ”自帶“;”),直接連在一起就可以了,同時數組標識是在元素類型前面加“[”,如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函數的包名+類名,接著我們調用JNIEnv的FindClass*方法clazz = (*env).FindClass(className);
來獲取注冊native函數的類,之后通過JNIEnv調用JNI函數 (*env).RegisterNatives(clazz, gMethods, numMethods)
來注冊上面JNINativeMethod中定義的對應函數。
當Java層通過System.loadLibrary加載完JNI動態庫后,緊接著會查找該庫中一個叫JNI_OnLoad的函數,如果有,就調用它,而動態注冊的工作就是在這里完成的。
??所以,如果想使用動態注冊方法,就必須要實現JNI_OnLoad函數,在這個函數中會完成動態注冊的工作。靜態注冊則沒有這個要求,一般我們可以自己實現這個JNI_OnLoad函數,并在其中做一些初始化工作(即使是靜態注冊)。
我們再貼一遍JNI_OnLoad()函數代碼:
//該函數的第一個參數類型為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.基本類型數據處理
其實基本類型處理上面我們已經展示過了,這里再貼一遍代碼:
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中的基本數據類型,在native中基本上就是在前面加了個“j”,實際上也就是在“jni.h”(NDK提供的jni開發類)中給java中的基本類型用結構體包裝類一下:
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++層傳遞基本類型數據是很容易的,只要上面函數的映射關系建立好了,就跟普通函數傳參一樣。
2.String字符串處理
這里我們展示一個從java向C++傳基本類型、String、數組,并從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;", //注意多種數據類型簽名時中間沒有分號或逗號
(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數組長度
str_len = (*jniEnv).GetStringLength(j_str);// 獲取String長度
j_charArray = (jchar*)malloc(sizeof(jchar)* arr_len); // 根據數組長度和數組元素的數據類型申請存放java數組元素的緩沖區
memset(j_charArray, 0,sizeof(jchar)* arr_len ); // 初始化緩沖區
(*jniEnv).GetCharArrayRegion(j_char, 0, arr_len, j_charArray); // 拷貝Java數組中的所有元素到緩沖區中
c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
sprintf(buff, "%s", c_str); //sprintf(s, "%d", 123); //把整數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); // 釋放存儲數組元素的緩沖區
(*jniEnv).ReleaseStringUTFChars(j_str,c_str);
return jniEnv->NewStringUTF(buff);
}
我們知道,Java中String是一個對象,他申明的對象是存放在JVM內部數據結構中的(堆,棧,靜態區)。 JNI把Java中的所有對象當作一個C指針(對象的引用)傳遞到本地方法中,這個指針指向JVM中的內部數據結構(即這個對象存放的地址),而內部的數據結構在內存中的存儲方式是不可見的。只能從JNIEnv指針指向的函數表中選擇合適的JNI函數來操作JVM中的數據結構。
訪問java.lang.String對應的JNI類型jstring時,沒有像訪問基本數據類型一樣直接使用,因為它在Java是一個引用類型,所以在本地代碼中只能通過GetStringUTFChars這樣的JNI函數來訪問字符串的內容。
3.2.1 const char* GetStringUTFChars(env, j_str, &isCopy)
env:JNIEnv函數表指針
j_str:jstring類型(Java傳遞給本地代碼的字符串指針)
isCopy:取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內部源字符串的一份拷貝,并為新產生的字符串分配內存空間。如果值為JNI_FALSE,表示返回JVM內部源字符串的指針,意味著可以通過指針修改源字符串的內容,不推薦這么做,因為這樣做就打破了Java字符串不能修改的規定。但我們在開發當中,并不關心這個值是多少,通常情況下這個參數填NULL即可。
因為Java默認使用Unicode編碼,而C/C++默認使用UTF編碼,所以在本地代碼中操作字符串的時候,必須使用合適的JNI函數把jstring轉換成C風格的字符串。JNI支持字符串在Unicode和UTF-8兩種編碼之間轉換,GetStringUTFChars可以把一個jstring指針(指向JVM內部的Unicode字符序列)轉換成一個UTF-8格式的C字符串。
3.2.2 jstring NewStringUTF(const char* bytes)
通過調用NewStringUTF函數,會構建一個新的java.lang.String字符串對象。這個新創建的字符串會自動轉換成Java支持的Unicode編碼。
3.2.3 void ReleaseStringUTFChars(jstring string, const char* utf)
在調用GetStringUTFChars函數從JVM內部獲取一個字符串之后,JVM內部會分配一塊新的內存,用于存儲源字符串的拷貝,以便本地代碼訪問和修改。即然有內存分配,用完之后馬上釋放.通過調用ReleaseStringUTFChars函數通知JVM這塊內存已經不使用了,你可以清除了。注意:這兩個函數是配對使用的,用了GetXXX就必須調用ReleaseXXX,而且這兩個函數的命名也有規律,除了前面的Get和Release之外,后面的都一樣。
3.訪問數組
??訪問數組的例子上面已經展示過了,這里主要說明一下幾個訪問數組相關的函數的用法。
3.3.1 jsize GetArrayLength(jarray array)
??獲取數組的長度,返回值為jint類型,我們獲取數組的類型之后就可以調用malloc函數動態的分配內存:
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* 類型的指針指向我們剛分配的內存(的首地址),以便之后調用 memset函數的時候找到這塊內存并將其中的字節初始化為0(申請的內存中可能有之前殘余的值),我們來看看這個memset函數:
memset() 函數用來將指定內存的前n個字節設置為特定的值(經常用于初始化剛剛申請的內存),其原型為:
void * memset( void * ptr, int value, size_t num );
參數說明:
ptr 為要操作的內存的指針。
value 為要設置的值。你既可以向 value 傳遞 int 類型的值,也可以傳遞 char 類型的值,
int 和 char 可以根據 ASCII 碼相互轉換。
num 為 ptr 的前 num 個字節,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 ); // 初始化緩沖區
(*jniEnv).GetCharArrayRegion(j_char, 0, arr_len, j_charArray); // 拷貝Java數組中的所有元素到緩沖區中
c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
sprintf(buff, "%s", c_str); //sprintf(s, "%d", 123); //把整數123打印成一個字符串保存在s中
??GetCharArrayRegion
將Java數組j_char,拷貝到我們剛剛申請的內存j_charArray中,之后我們調用c_str = (*jniEnv).GetStringUTFChars(j_str, &isCopy);
將java層傳來的 j_str(String類型)轉換成一個C風格字符串(c_str
),然后sprintf(buff, "%s", c_str);
是將作為C風格的字符串儲存在buff數組中。