1. 程式人生 > >Android進階之路——NDK(二)

Android進階之路——NDK(二)

  上一篇部落格介紹了NDK簡介和環境的搭建以及一個簡單的Demo,這篇準備總結一下JNI呼叫Java物件以及在JNI中開啟執行緒。
  ps:這裡說明一下,我是用Android Studio開發的,如果是用Eclipse開發的朋友,是不能直接匯入我的程式,而且專案的結構和我的是有區別的。

點選下載

一、JNI實現回撥

  通過JNI在Native層呼叫JAVA層的方法,來實現Native層向JAVA層傳遞訊息。

我的專案結構:

這裡寫圖片描述

  1. 首先在java層註冊native函式
public class MyJni {

    public static final String TAG = "MyNdkTest"
; /** * 載入動態庫 */ static { System.loadLibrary("MyJni"); } /** * 從JNI獲取字串 * * @return */ public static native String getStringFromNative(); /** * 初始化native中建立執行緒需要的變數 */ public native void nativeInitialize(); /** * 建立native中執行緒 */
public native void nativeThreadStart(); /** * 停止native中執行緒 */ public native void nativeThreadStop(); /** * 從native程式碼中回撥Java的方法入口 */ public native void nativeCallback(); /** * 從native程式碼中回撥(非靜態) * 在Jni中開啟執行緒 */ public void onNativeThreadCallback
(String str) { Log.i(TAG, "Thread ID = " + Thread.currentThread().getId()); Log.i(TAG, "onNativeThreadCallback = " + str); } /** * 從native程式碼中回撥Java(非靜態) */ public void onNativeCallback(String str) { Log.i(TAG, "Thread ID = " + Thread.currentThread().getId()); Log.i(TAG, "onNativeCallback = " + str); } /** * 從native程式碼中回撥(非靜態) * 在Jni中開啟執行緒 */ public static void onNativeStaticCallback(int count) { Log.i(TAG, "Thread ID = " + Thread.currentThread().getId()); } }

  如上面程式碼所示,Java層與JNI層的介面程式碼主要封裝在Native類中,該類定義了五個native函式,分別是從jni層獲取字串,完成jni庫的初始化,呼叫jni層開啟執行緒,呼叫jni層關閉執行緒等功能。並且提供一個回撥函式(一個為在開啟的執行緒中回撥,另一個是在jni開啟的執行緒中回撥),供jni層呼叫,並在回撥函式中列印執行緒的Id和傳遞過來的字串。這裡先不講解如何在JNI中建立執行緒。
  
2. jni中回撥java層的函式
  這裡建立標頭檔案的方法和Android Studio下配置NDK的環境已經在前一篇敘述過,這裡就不說了。在標頭檔案中定義了這五個函式,MyJni.c是實現五個native函式的主要類:

JNIEXPORT void JNICALL
Java_com_ndk_MyJni_nativeCallback(JNIEnv *env, jobject instance) {

    onNativeCallback(env, "主執行緒中回撥java函式");

}

  這裡只貼出了在主執行緒回撥java函式的程式碼,這裡程式碼很簡單,就是呼叫了onNativeCallback,然而這個函式實在哪裡實現的呢?大家可以再看一下我的專案截圖,就是在CallJava.c中實現的。程式碼中重要的部分我都有註釋。

#include "CallJava.h"
#include "com_ndk_MyJni.h"

/**
 * C回撥Java方法(非靜態)
 */
void onNativeCallback(JNIEnv *env, jstring str) {

    // 獲取類
    jclass gjclass = (*env)->FindClass(env, "com/ndk/MyJni");
    if (NULL == gjclass) {
        return;
    }

    // 例項化類物件
    jobject gjobject = getInstance(env, gjclass);
    if (NULL == gjobject) {
        (*env)->DeleteLocalRef(env, gjclass); // 刪除類指引
        LOGI("刪除類指引 !");
        return;
    }

    // 獲取物件callback方法
    jmethodID callback = (*env)->GetMethodID(env, gjclass, "onNativeCallback",
                                             "(Ljava/lang/String;)V");
    if (NULL == callback) {
        (*env)->DeleteLocalRef(env, gjclass); // 刪除類指引
        (*env)->DeleteLocalRef(env, gjobject); // 刪除類物件指引
        LOGI("刪除類物件指引 !");
        return;
    }
    // 呼叫非靜態int方法
    (*env)->CallVoidMethod(env, gjobject, callback, (*env)->NewStringUTF(env, str));
}

/**
 * 例項化類物件
 */
jobject getInstance(JNIEnv *env, jclass clazz) {
    // 獲取構造方法
    jmethodID constructor = (*env)->GetMethodID(env, clazz, "<init>", "()V");
    if (NULL == constructor) {
        return NULL;
    }
    // 例項化類物件
    return (*env)->NewObject(env, clazz, constructor);
}

  大家可以看到,在JNI中回撥Java層的函式需要四步:1、獲得一個Java類的class引用(*env)->FindClass(env, “com/ndk/MyJni”),第二個引數代表這個類的相對路徑。2、例項化該類,(*env)->GetMethodID(env, clazz, “”, “()V”),第二個是剛剛獲得的class引用,第三個是方法的名稱(這裡是建構函式),最後一個就是方法的簽名了(下一章節會詳細介紹)。3、獲取到呼叫該物件的方法。4、回撥該方法。
  這樣一個主執行緒的回撥就完成了,這裡大家在使用(*env)->GetMethodID時,注意第三個和第四個引數一定要和你java中函式名稱和引數型別及個數相對應,不然會報錯。
  

二、在JNI中開啟子執行緒

  • 在java層註冊native函式,與上一章節中的第一步一樣,這裡就不再贅述。
  • 在JNI中實現開啟執行緒的程式碼,也就是MyJni.c,上一章節中只是貼出了主執行緒回撥的一個函式,這裡將剩下的四個本地方法都貼出來。
#include "com_ndk_MyJni.h"

JNIEXPORT jstring JNICALL
Java_com_ndk_MyJni_getStringFromNative(JNIEnv *env, jclass type) {

    return (*env)->NewStringUTF(env, "I am from native");
}

/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    設定全域性變數
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeInitialize(JNIEnv *env,
                                                           jobject thiz) {

    //注意,直接通過定義全域性的JNIEnv和jobject變數,在此儲存env和thiz的值是不可以線上程中使用的
    //執行緒不允許共用env環境變數,但是JavaVM指標是整個jvm共用的,所以可以通過下面的方法儲存JavaVM指標,線上程中使用
    (*env)->GetJavaVM(env, &gJavaVM);

    //同理,jobject變數也不允許線上程中共用,因此需要建立全域性的jobject物件線上程中訪問該物件
    gJavaObj = (*env)->NewGlobalRef(env, thiz);
}
static void *native_thread_exec(void *arg) {

    JNIEnv *env;

    //從全域性的JavaVM中獲取到環境變數
    (*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL);

    //獲取Java層對應的類
    jclass javaClass = (*env)->GetObjectClass(env, gJavaObj);
    if (javaClass == NULL) {
        LOGI("Fail to find javaClass");
        return 0;
    }

    //獲取Java層被回撥的函式
    jmethodID javaCallback = (*env)->GetMethodID(env, javaClass, "onNativeThreadCallback", "(I)V");
    if (javaCallback == NULL) {
        LOGI("Fail to find method onNativeCallback");
        return 0;
    }

    LOGI("native_thread_exec loop enter");

    int count = 0;

    //執行緒迴圈
    while (!gIsThreadExit) {

        //回撥Java層的函式
        (*env)->CallVoidMethod(env, gJavaObj, javaCallback, count++);

        //休眠1秒
        sleep(1);
    }

    (*gJavaVM)->DetachCurrentThread(gJavaVM);

    LOGI("native_thread_exec loop leave");
}



/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    開啟執行緒
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStart(JNIEnv *env,
                                                            jobject thiz) {

    gIsThreadExit = 0;

    //通過pthread庫建立執行緒
    pthread_t threadId;
    if (pthread_create(&threadId, NULL, native_thread_exec, NULL) != 0) {
        LOGI("native_thread_start pthread_create fail !");
        return;
    }

    LOGI("native_thread_start success");
}

/*
 * Class:     com_ticktick_jnicallback_Native
 * Method:    NativeThreadStop
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStop(JNIEnv *env,
                                                           jobject thiz) {
    gIsThreadExit = 1;
    LOGI("native_thread_stop success");
}

  第一個本地方法:返回一個字串。
  第二個本地方法:是初始化一些全域性變數,在開啟執行緒時使用。
  第三個本地方法:是開啟執行緒。
  第四個本地方法:是關閉執行緒
  大部分程式碼是有註釋的,大家應該是可以看懂的。

三、方法的簽名

JNINativeMethod的定義如下:

typedef struct {
   const char* name;
   const char* signature;
   void* fnPtr;
} JNINativeMethod;

第一個變數name是Java中函式的名字。
第二個變數signature,用字串是描述了函式的引數和返回值
第三個變數fnPtr是函式指標,指向C函式。

其中比較難以理解的是第二個引數,例如
“()V”
“(II)V”
“(Ljava/lang/String;Ljava/lang/String;)V”

實際上這些字元是與函式的引數型別一一對應的。
“()” 中的字元表示引數,後面的則代表返回值。例如”()V” 就表示void Func();
“(II)V” 表示 void Func(int, int);

那其他情況呢?請查看下錶:
型別
符號
這裡寫圖片描述
稍稍補充一下:

1、方法引數或者返回值為java中的物件時,簽名中必須以“L”加上其路徑,不過此路徑必須以“/”分開,自定義的物件也使用本規則
比如說 java.lang.String為“java/lang/String”,com.nedu.jni.helloword.Student為”Lcom /nedu/jni/helloword/Student;”

2、方法引數或者返回值為陣列型別時,請前加上[

例如[I表示 int[],[[[D表示 double[][][],即幾維陣列就加幾個[
這裡寫圖片描述

四、總結

  1. 在JNI_OnLoad中,儲存JavaVM*,這是跨執行緒的,持久有效的,而JNIEnv*則是當前執行緒有效的。一旦啟動執行緒,用AttachCurrentThread方法獲得env。
  2. 通過JavaVM*和JNIEnv可以查詢到jclass。
  3. 把jclass轉成全域性引用,使其跨執行緒。
  4. 然後就可以正常地呼叫你想呼叫的方法了。
  5. 用完後,別忘了delete掉建立的全域性引用和呼叫DetachCurrentThread方法。