本來這篇文章想叫JNI使用詳解或者使用全解的,但是想了想,這篇文章的內容應該只算基礎教學。所以改成這個名字,既成為了標題黨,也算是客觀。
準備工作
這篇文章直接進入正題,所謂的ndk下載工程建立我就不多說了,如果有疑問的可以參考我之前的一篇文章Android Studio中jni的使用。 在app的build.gradle中:
defaultConfig {
applicationId "umeng.testjni"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
ndk {
moduleName "JniTest"
ldLibs "log", "z", "m"
abiFilters "armeabi", "armeabi-v7a", "x86"
}
}
複製程式碼
其中moduleName是生成的.so檔案的名字,如果設定成JniTest,生成的.so檔案會是libJniTest.so,ldLibs是依賴的庫。 開啟gradle.properties檔案,增加配置:
android.useDeprecatedNdk = true
複製程式碼
local.properties增加ndk的配置路徑:
ndk.dir=/Users/xxxx/xxxx/sdk/android-ndk-r10e
sdk.dir=/Users/xxxxx/Library/Android/sdk
複製程式碼
程式碼工作
工程中新建一個介面類JniInterface
:
public class JniInterface {
static {
System.loadLibrary("JniTest");
}
public static native String sayHello();
}
複製程式碼
待會我們會一個一個的在這個類中新增方法。 其中 System.loadLibrary("JniTest");是載入.so檔案,sayHello是c++的方法名字。 這時開啟命令列,切到當前應用工程的目錄下(嚴格來說不是工程目錄下,而是java程式碼目錄下,即app/src/main/java),輸入如下命令:
javah -jni xxxx.com.jnitech.JniInterface
複製程式碼
其中 xxxx.com.jnitech.JniInterface是我們剛剛編寫的檔案,這時會在對應的路徑下生成一個 xxxx.com.jnitech.JniInterface.h檔案
在main目錄下建立jni資料夾,新建main.c實現剛才生成的標頭檔案中的方法:
#include <jni.h>
/* Header for class umeng_com_jnitech_JniInterface */
#include <stddef.h>
#ifndef _Included_umeng_com_jnitech_JniInterface
#define _Included_umeng_com_jnitech_JniInterface
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: umeng_com_jnitech_JniInterface
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_sayHello
(JNIEnv *env, jclass object){
return (*env)->NewStringUTF(env,"hello umeng");
}
#ifdef __cplusplus
}
#endif
#endif
複製程式碼
此時執行即可編譯.so檔案,在build/intermediates/ndk目錄下可以找到對應檔案。 如果文章就這樣結束了,大家一定覺得很水,所以這僅僅是一個開始。
呼叫C的方法
上面的例子是一個在Java中呼叫C中字串的方法。下面要實現一個Java呼叫C方法的例子。
找到標頭檔案umeng_com_jnitech_JniInterface.h
:
JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sum
(JNIEnv *, jclass,jint,jint);
複製程式碼
新增了這個宣告之後,需要去.c檔案中進行實現:
JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sum
(JNIEnv *env, jclass object, jint a, jint b){
return (a+b);
}
複製程式碼
這個方法可以看出是傳入兩個int值,做一個加法,再將值返回的操作。 在Java中也需要宣告一下:
public static native int sum(int a,int b);
複製程式碼
然後呼叫即可:
Toast.makeText(MainActivity.this,"3+4="+JniInterface.sum(3,4),Toast.LENGTH_SHORT).show();
複製程式碼
這裡要做一下詳細說明,java中的int對應到C中就是jint,這是一個原始型別的轉化問題,除此還有:
Java型別 | 本地型別(JNI) | 描述 |
---|---|---|
boolean(布林型) | jboolean | 無符號8個位元 |
byte(位元組型) | jbyte | 有符號8個位元 |
char(字元型) | jchar | 無符號16個位元 |
short(短整型) | jshort | 有符號16個位元 |
int(整型) | jint | 有符號32個位元 |
long(長整型) | jlong | 有符號64個位元 |
float(浮點型) | jfloat | 32個位元 |
double(雙精度浮點型) | jdouble | 64個位元 |
void(空型) | void | N/A |
陣列操作
上面的方法是傳入兩個int值,如果是陣列如何操作,這裡注意,傳入的型別要是基礎資料型別,要想傳入一個ArrayList肯定是不可以的。所以我們就用最基礎的String陣列。 標頭檔案增加方法:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_testArray
(JNIEnv *, jclass,jobjectArray,jobjectArray);
複製程式碼
實現:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_testArray
(JNIEnv *env, jclass object, jobjectArray a, jobjectArray b){
jsize count1 = (*env)->GetArrayLength(env,a);
jsize count2 = (*env)->GetArrayLength(env,b);
jstring aa = (jstring) (*env)->GetObjectArrayElement(env,a, count1-1);
jstring bb = (jstring) (*env)->GetObjectArrayElement(env,b, count2-1);
return (*env)->NewStringUTF(env,"陣列收到");
}
複製程式碼
增加java的介面
public static native String testArray(String[] a,String[] b);
複製程式碼
C呼叫Java中的String型別
上面提到了如何從Java中呼叫C的String,如果C需要呼叫Java的型別該如何處理呢? 如在Java中有如下方法:
public String helloNoStatic(){
return "這個字串來自java非靜態,穿越c,展示在這裡";
}
複製程式碼
C中應該如何呼叫呢?答案是反射,反射的方法與Java反射的方法類似。 標頭檔案:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callNonStaticJavaString
(JNIEnv *, jclass);
複製程式碼
實現:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callNonStaticJavaString
(JNIEnv *env, jclass object){
jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
if(dpclazz==0){
return;
}
jmethodID method1 = (*env)->GetMethodID(env,dpclazz,"helloNoStatic","()Ljava/lang/String;");
if(method1==0){
return;
}
jstring result = (jstring)(*env)->CallObjectMethod(env,object,method1);
return result;
}
複製程式碼
如果是一個靜態方法:
public static String hello(){
return "這個字串來自java靜態,穿越c,展示在這裡";
}
複製程式碼
該如何實現呢? 與靜態類似,只是C中呼叫的方法不同:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_callJavaStringSum
(JNIEnv *env, jclass object,jstring a,jstring b){
jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
if(dpclazz==0){
return;
}
jmethodID method1 = (*env)->GetStaticMethodID(env,dpclazz,"sumStringCallBackJava","(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
if(method1==0){
return;
}
jstring result = (jstring)(*env)->CallStaticObjectMethod(env,object,method1,a,b);
// return (*env)->NewStringUTF(env,"hello umeng");
return result;
}
複製程式碼
看過程式碼的同學,可能會疑問"()Ljava/lang/String;
這是什麼意思,這是域描述符,這裡詳細介紹一下:
括號內為呼叫方法的引數,括號後面的是返回值。
域描述符對應如下:
域 | JAVA語言 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
String | Ljava/lang/String; |
int[ ] | [I |
float[ ] | [F |
String[ ] | [Ljava/lang/String; |
Object[ ] | [Ljava/lang/Object; |
int [ ][ ] | [[I |
float[ ][ ] | [[F |
C中呼叫Java相加的方法
Java中:
public static int sumCallBackJava(int a,int b){
Log.e("xxxxxx","a+b="+a+b);
return a+b;
}
複製程式碼
標頭檔案:
JNIEXPORT jstring JNICALL Java_umeng_com_jnitech_JniInterface_sumCallBack
(JNIEnv *, jclass,jint,jint);
複製程式碼
實現:
JNIEXPORT jint JNICALL Java_umeng_com_jnitech_JniInterface_sumCallBack
(JNIEnv *env, jclass object, jint a, jint b){
jclass dpclazz = (*env)->FindClass(env,"umeng/com/jnitech/JniInterface");
if(dpclazz==0){
return 0;
}
jmethodID method1 = (*env)->GetStaticMethodID(env,dpclazz,"sumCallBackJava","(II)I");
if(method1==0){
return 0;
}
jint result = (*env)->CallStaticIntMethod(env,object,method1,a,b);
// return (*env)->NewStringUTF(env,"hello umeng");
return result;
}
複製程式碼
JNI函式操作查詢
函式 | Java | 本地型別 | 說明 |
---|---|---|---|
GetBooleanArrayElements | jbooleanArray | jboolean | ReleaseBooleanArrayElements 釋放 |
GetByteArrayElements | jbyteArray | jbyte | ReleaseByteArrayElements 釋放 |
GetCharArrayElements | jcharArray | jchar | ReleaseShortArrayElements 釋放 |
GetShortArrayElements | jshortArray | jshort | ReleaseBooleanArrayElements 釋放 |
GetIntArrayElements | jintArray | jint | ReleaseIntArrayElements 釋放 |
GetLongArrayElements | jlongArray | jlong | ReleaseLongArrayElements 釋放 |
GetFloatArrayElements | jfloatArray | jfloat | ReleaseFloatArrayElements 釋放 |
GetDoubleArrayElements | jdoubleArray | jdouble | ReleaseDoubleArrayElement 釋放 |
GetObjectArrayElement | 自定義物件 | object | |
SetObjectArrayElement | 自定義物件 | object | |
GetArrayLength | 獲取陣列大小 | ||
NewArray | 建立一個指定長度的原始資料型別的陣列 | ||
GetPrimitiveArrayCritical | 得到指向原始資料型別內容的指標,該方法可能使垃圾回收不能執行,該方法可能返回陣列的拷貝,因此必須釋放此資源。 | ||
ReleasePrimitiveArrayCritical | 釋放指向原始資料型別內容的指標,該方法可能使垃圾回收不能執行,該方法可能返回陣列的拷貝,因此必須釋放此資源。 | ||
NewStringUTF | jstring型別的方法轉換 | ||
GetStringUTFChars | jstring型別的方法轉換 | ||
DefineClass | 從原始類資料的緩衝區中載入類 | ||
FindClass 該函式用於載入本地定義的類。它將搜尋由CLASSPATH 環境變數為具有指定名稱的類所指定的目錄和 zip檔案 | |||
GetObjectClass | 通過物件獲取這個類。該函式比較簡單,唯一注意的是物件不能為NULL,否則獲取的class肯定返回也為NULL | ||
GetSuperclass | 獲取父類或者說超類 。 如果 clazz 代表類class而非類 object,則該函式返回由 clazz 所指定的類的超類。 如果 clazz指定類 object 或代表某個介面,則該函式返回NULL | ||
IsAssignableFrom | 確定 clazz1 的物件是否可安全地強制轉換為clazz2 | ||
Throw | 丟擲 java.lang.Throwable 物件 | ||
ThrowNew | 利用指定類的訊息(由 message 指定)構造異常物件並丟擲該異常 | ||
ExceptionOccurred | 確定是否某個異常正被丟擲。在平臺相關程式碼呼叫 ExceptionClear() 或 Java 程式碼處理該異常前,異常將始終保持丟擲狀態 | ||
ExceptionDescribe | 將異常及堆疊的回溯輸出到系統錯誤報告通道(例如 stderr)。該例程可便利除錯操作 | ||
ExceptionClear | 清除當前丟擲的任何異常。如果當前無異常,則此例程不產生任何效果 | ||
FatalError | 丟擲致命錯誤並且不希望虛擬機器進行修復。該函式無返回值 | ||
NewGlobalRef | 建立 obj 引數所引用物件的新全域性引用。obj 引數既可以是全域性引用,也可以是區域性引用。全域性引用通過呼叫DeleteGlobalRef() 來顯式撤消。 | ||
DeleteGlobalRef | 刪除 globalRef 所指向的全域性引用 | ||
DeleteLocalRef | 刪除 localRef所指向的區域性引用 | ||
AllocObject | 分配新 Java 物件而不呼叫該物件的任何建構函式。返回該物件的引用。clazz 引數務必不要引用陣列類。 | ||
getObjectClass | 返回物件的類 | ||
IsSameObject | 測試兩個引用是否引用同一 Java 物件 | ||
NewString | 利用 Unicode 字元陣列構造新的 java.lang.String 物件 | ||
GetStringLength | 返回 Java 字串的長度(Unicode 字元數) | ||
GetStringChars | 返回指向字串的 Unicode 字元陣列的指標。該指標在呼叫 ReleaseStringchars() 前一直有效 | ||
ReleaseStringChars | 通知虛擬機器平臺相關程式碼無需再訪問 chars。引數chars 是一個指標,可通過 GetStringChars() 從 string 獲得 | ||
NewStringUTF | 利用 UTF-8 字元陣列構造新 java.lang.String 物件 | ||
GetStringUTFLength | 以位元組為單位返回字串的 UTF-8 長度 | ||
GetStringUTFChars | 返回指向字串的 UTF-8 字元陣列的指標。該陣列在被ReleaseStringUTFChars() 釋放前將一直有效 | ||
ReleaseStringUTFChars | 通知虛擬機器平臺相關程式碼無需再訪問 utf。utf 引數是一個指標,可利用 GetStringUTFChars() 獲得 | ||
NewObjectArray | 構造新的陣列,它將儲存類 elementClass 中的物件。所有元素初始值均設為 initialElement | ||
SetArrayRegion | 將基本型別陣列的某一區域從緩衝區中複製回來的一組函式 | ||
GetFieldID | 返回類的例項(非靜態)域的屬性 ID。該域由其名稱及簽名指定。訪問器函式的GetField 及 SetField系列使用域 ID 檢索物件域。GetFieldID() 不能用於獲取陣列的長度域。應使用GetArrayLength()。 | ||
GetField | 該訪問器例程系列返回物件的例項(非靜態)域的值。要訪問的域由通過呼叫GetFieldID() 而得到的域 ID 指定。 | ||
SetField | 該訪問器例程系列設定物件的例項(非靜態)屬性的值。要訪問的屬性由通過呼叫SetFieldID() 而得到的屬性 ID指定。 | ||
GetStaticFieldID GetStaticField SetStaticField | 同上,只不過是靜態屬性。 | ||
GetMethodID | 返回類或介面例項(非靜態)方法的方法 ID。方法可在某個 clazz 的超類中定義,也可從 clazz 繼承。該方法由其名稱和簽名決定。 GetMethodID() 可使未初始化的類初始化。要獲得建構函式的方法 ID,應將 作為方法名,同時將void (V) 作為返回型別。 | ||
CallVoidMethod | 同上 | ||
CallObjectMethod | 同上 | ||
CallBooleanMethod | 同上 | ||
CallByteMethod | 同上 | ||
CallCharMethod | 同上 | ||
CallShortMethod | 同上 | ||
CallIntMethod | 同上 | ||
CallLongMethod | 同上 | ||
CallFloatMethod | 同上 | ||
CallDoubleMethod | 同上 | ||
GetStaticMethodID | 呼叫靜態方法 | ||
CallMethod | 同上 | ||
RegisterNatives | 向 clazz 引數指定的類註冊本地方法。methods 引數將指定 JNINativeMethod 結構的陣列,其中包含本地方法的名稱、簽名和函式指標。nMethods 引數將指定陣列中的本地方法數。 | ||
UnregisterNatives | 取消註冊類的本地方法。類將返回到連結或註冊了本地方法函式前的狀態。該函式不應在常規平臺相關程式碼中使用。相反,它可以為某些程式提供一種重新載入和重新連結本地庫的途徑。 |
總結
JNI中使用的幾種常見方法暫時介紹到這裡,有什麼問題,也歡迎大家給我留言。 demo地址