Android JNI 中的執行緒操作

glumes發表於2018-07-16

學習一下如何在 Native 程式碼中使用執行緒。

Native 中支援的執行緒標準是 POSIX 執行緒,它定義了一套建立和操作執行緒的 API 。

我們可以在 Native 程式碼中使用 POSIX 執行緒,就相當於使用一個庫一樣,首先需要包含這個庫的標頭檔案:

#include <pthread.h>
複製程式碼

這個標頭檔案中定義了很多和執行緒相關的函式,這裡就暫時使用到了其中部分內容。

建立執行緒

POSIX 建立執行緒的函式如下:

int pthread_create(
	pthread_t* __pthread_ptr, 
	pthread_attr_t const* __attr, 
	void* (*__start_routine)(void*), 
	void* arg);
複製程式碼

它的引數對應如下:

  • __pthread_ptr 為指向 pthread_t 型別變數的指標,用它代表返回執行緒的控制程式碼。

  • __attr 為指向 pthread_attr_t 結構的指標,可以通過該結構來指定新執行緒的一些屬性,比如棧大小、排程優先順序等,具體看 pthread_attr_t 結構的內容。如果沒有特殊要求,可使用預設值,把該變數取值為 NULL 。

  • 第三個引數為該執行緒啟動程式的函式指標,也就是執行緒啟動時要執行的那個方法,類似於 Java Runnable 中的 run 方法,它的函式簽名格式如下:

void* start_routine(void* args)
複製程式碼

啟動程式將執行緒引數看成 void 指標,返回 void 指標型別結果。

  • 第四個引數為執行緒啟動程式的引數,也就是函式的引數,如果不需要傳遞引數,它可以為 NULL 。

pthread_create 函式如果執行成功了則返回 0 ,如果返回其他錯誤程式碼。

接下來,我們可以體驗一下 pthread_create 方法建立執行緒。

首先,建立執行緒啟動時執行的函式:

void *printThreadHello(void *);
void *printThreadHello(void *) {
    LOGD("hello thread");
    // 切記要有返回值
    return NULL;
}
複製程式碼

要注意執行緒啟動函式是要有返回值的,沒有返回值就直接崩潰了。

另外這個函式一旦 pthread_create 呼叫了,它就會立即執行。

接下來建立執行緒:

JNIEXPORT void JNICALL
Java_com_glumes_cppso_jnioperations_ThreadOps_createNativeThread(JNIEnv *, jobject) {
    pthread_t handles; // 執行緒控制程式碼
    int result = pthread_create(&handles, NULL, printThreadHello, NULL);
    if (result != 0) {
        LOGD("create thread failed");
    } else {
        LOGD("create thread success");
    }
}
複製程式碼

由於沒有給該執行緒設定屬性,並且執行緒執行函式也不需要引數,就都直接設定為了 NULL,那麼上面那段程式就可以執行了,並且 printThreadHello 函式是執行在新的執行緒中的。

將執行緒附著在 Java 虛擬機器上

在上面的執行緒啟動函式中,只是簡單的執行了列印 log 的操作,如果想要執行和 Java 相關的操作,比如從 JNI 呼叫 Java 的函式等等,那就需要用到 Java 虛擬機器環境了,也就是用到 JNIEnv 指標,畢竟所有的呼叫函式都是以它開頭的。

pthread_create 建立的執行緒是一個 C++ 中的執行緒,虛擬機器並不能識別它們,為了和 Java 空間互動,需要先把 POSIX 執行緒附著到 Java 虛擬機器上,然後就可以獲得當前執行緒的 JNIEnv 指標,因為 JNIEnv 指標只是在當前執行緒中有效。

通過 AttachCurrentThread 方法可以將當前執行緒附著到 Java 虛擬機器上,並且可以獲得 JNIEnv 指標。

AttachCurrentThread 方法是由 JavaVM 指標呼叫的,它代表的是 Java 虛擬機器介面指標,可以在 JNI_OnLoad 載入時來獲得,通過全域性變數儲存起來

static JavaVM *gVm = NULL;
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    gVm = vm;
    return JNI_VERSION_1_6;
}
複製程式碼

當通過 AttachCurrentThread 方法將執行緒附著當 Java 虛擬機器上後,還需要將該執行緒從 Java 虛擬機器上分離,通過 DetachCurrentThread 方法,這兩個方法是要同時使用的,否則會帶來 BUG 。

具體使用如下:

首先在 Java 中定義在 C++ 執行緒中回撥的方法,主要就是列印執行緒名字:

    private void printThreadName() {
        LogUtil.Companion.d("print thread name current thread name is " + Thread.currentThread().getName());
            Thread.sleep(5000);
    }
複製程式碼

然後定義 Native 執行緒中執行的方法:

void *run(void *);
void *run(void *args) {
    JNIEnv *env = NULL;
    // 將當前執行緒新增到 Java 虛擬機器上
    // 假設有引數傳遞
    ThreadRunArgs *threadRunArgs = (ThreadRunArgs *) args;
    if (gVm->AttachCurrentThread(&env, NULL) == 0) {
    // 回撥 Java 的方法,列印當前執行緒 id ,發現不是主執行緒就對了
        env->CallVoidMethod(gObj, printThreadName);
        // 從 Java 虛擬機器上分離當前執行緒
        gVm->DetachCurrentThread();  
    }
    return (void *) threadRunArgs->result;
}
複製程式碼

最後建立執行緒並執行方法:

		// 建立傳遞的引數
	    ThreadRunArgs *threadRunArgs = new ThreadRunArgs();
        threadRunArgs->id = i;
        threadRunArgs->result = i * i;
        // 執行執行緒
        int result = pthread_create(&handles, NULL, run, (void *) threadRunArgs);
複製程式碼

通過這樣的呼叫,就可以在 Native 執行緒中呼叫 Java 相關的函式了。

等待執行緒返回結果

前面提到線上程執行函式中必須要有返回值,最開始只是返回了一個空指標 NULL ,並且在某個方法裡面開啟了新執行緒,新執行緒執行後,該方法也就立即返回退出,執行完了。

現在,還可以在該方法裡等待執行緒執行完畢後,拿到執行緒執行完的結果之後再推出。

通過 pthread_join 方法可以等待執行緒終止。

int pthread_join(pthread_t __pthread, void** __return_value_ptr);
複製程式碼

其中:

  • __pthread 代表建立執行緒的控制程式碼
  • __return_value_ptr 代表執行緒執行函式返回的結果

使用如下:

	for (int i = 0; i < num; ++i) {
        pthread_t pthread;
        // 建立執行緒,
        int result = pthread_create(&handles[i], NULL, run, (void *) threadRunArgs);
        }
    }
    for (int i = 0; i < num; ++i) {
        void *result = NULL; // 執行緒執行返回結果
        // 等待執行緒執行結束
        if (pthread_join(handles[i], &result) != 0) {
            throwByName(env, runtimeException, "Unable to join thread");
        } else {
	        LOGD("return value is %d",result);
        }
    }
複製程式碼

如果 pthread_join 返回為 0 代表執行成功,非 0 則執行失敗。

具體的程式碼示例可以參考我的 Github 專案,歡迎 Star 。

github.com/glumes/Andr…

JNI 系列文章:

  1. Android JNI 中的引用管理
  2. Android JNI 呼叫時的異常處理
  3. Android JNI 基本操作

歡迎關注微信公眾號:【紙上淺談】,獲得最新文章推送~~~

掃碼關注

相關文章