JNI入門這篇文章就夠了(含demo)

mymdeep發表於2017-12-08

本來這篇文章想叫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地址

相關文章