Android進階之路——NDK(二)
上一篇部落格介紹了NDK簡介和環境的搭建以及一個簡單的Demo,這篇準備總結一下JNI呼叫Java物件以及在JNI中開啟執行緒。
ps:這裡說明一下,我是用Android Studio開發的,如果是用Eclipse開發的朋友,是不能直接匯入我的程式,而且專案的結構和我的是有區別的。
點選下載
一、JNI實現回撥
通過JNI在Native層呼叫JAVA層的方法,來實現Native層向JAVA層傳遞訊息。
我的專案結構:
- 首先在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[][][],即幾維陣列就加幾個[
四、總結
- 在JNI_OnLoad中,儲存JavaVM*,這是跨執行緒的,持久有效的,而JNIEnv*則是當前執行緒有效的。一旦啟動執行緒,用AttachCurrentThread方法獲得env。
- 通過JavaVM*和JNIEnv可以查詢到jclass。
- 把jclass轉成全域性引用,使其跨執行緒。
- 然後就可以正常地呼叫你想呼叫的方法了。
- 用完後,別忘了delete掉建立的全域性引用和呼叫DetachCurrentThread方法。