Android JNI-在jni層的執行緒中回撥到java層

2020-09-28 09:10:28

人間觀察

忽有故人心上頭,回首山河已是秋。

馬上國慶+中秋了~~~

今天我們看一個比較常見的場景:
當我們處理一個密集型計算資料(比如音視訊的軟編解碼處理,bitmap的特效處理等),這時候就需要用c/c++實現。當在c/c++處理完後需要非同步回撥/通知到java中,這樣程式碼看起來才很優雅有氣質。
如果你知道這個知識那就return吧。~~

在Android中你可以用Thread+Handler很容易的來實現,我相信你閉著眼都能寫了。但在jni層中不是這麼簡單的,我們如何實現?

我們先看一下在jni中非子執行緒中如何回撥再看下在子執行緒如何回撥到java層中。

jni中非子執行緒回撥到java方法中

和普通的在jni中呼叫java的實體方法沒啥區別,上程式碼:

// java回撥介面 INativeListener.java
public interface INativeListener {
    void onCall();
}
public native void nativeCallBack(INativeListener callBack);
// jni_thread_callback.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeCallBack(JNIEnv *env, jobject thiz,
                                                           jobject call_back) {
    // 獲取java中的物件
    jclass cls = env->GetObjectClass(call_back);
    // 獲取回撥方法的id
    jmethodID mid = env->GetMethodID(cls, "onCall", "()V");
    // 呼叫java中的方法
    env->CallVoidMethod(call_back, mid);
}

總結: 分三步

  1. 根據java的obj獲取jclass。jclass可以理解為java中的class物件(如果熟悉jni就沒啥問題)
  2. 根據1步中的jclass和方法名字和方法的簽名獲取該方法的jmethodID
  3. env呼叫java範例的方法。(當前如果是靜態的只是呼叫的方法不一樣)

jni中的子執行緒回撥到java方法

主要方法是JavaVM中的AttachCurrentThreadDetachCurrentThread兩個方法,這兩個是對應的。
官方檔案:
官網doc地址

有關注釋

Attaching to the VM
The JNI interface pointer (JNIEnv) is valid only in the current thread. Should another thread need to access the Java VM, 
it must first call AttachCurrentThread() to attach itself to the VM and obtain a JNI interface pointer.
Once attached to the VM, a native thread works just like an ordinary Java thread running inside a native method. 
The native thread remains attached to the VM until it calls DetachCurrentThread() to detach itself.

The attached thread should have enough stack space to perform a reaonable amount of work. 
The allocation of stack space per thread is operating system-specific. For example, using pthreads,
the stack size can be specified in the pthread_attr_t argument to pthread_create.

譯一下:

依附到Java虛擬機器器上
JNI介面指標(JNIEnv)僅在當前執行緒中有效。
如果另一個執行緒需要存取jvm,它必須首先呼叫AttachCurrentThread()將自己附加到 JVM並獲取JNI介面指標。
一旦連線到JVM上,本地執行緒(jni執行緒)的工作方式與在本地方法中執行的普通Java執行緒一樣。
本機執行緒在呼叫DetachCurrentThread()來分離它自己之前一直連線到VM。

附加的執行緒應該有足夠的堆疊空間來執行合理數量的任務。
每個執行緒的堆疊空間分配是取決於作業系統。
例如,使用pthreads,可以在pthread_attr_t引數中為pthread_create指定堆疊大小。

而在呼叫JavaVM中的AttachCurrentThreadDetachCurrentThread我們需要拿到JavaVM *vm指標。怎麼拿到這個呢?一種是呼叫JNI_CreateJavaVM載入並初始化Java虛擬機器器,並返回指向JNI介面指標的指標。我們可以用另外一種jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)全域性變數儲存一下vm即可。
如果不瞭解JNI_OnLoad可以看上一篇文章 jni動態庫的函數註冊

接下來,我們寫一個簡單的功能:在jni中建立一個執行緒實現一個寫入隨機字串到檔案(用來模擬執行緒任務的耗時),然後寫入完成後給java層一個回撥告訴java層寫入成功。

// java回撥介面 INativeThreadListener.java
public interface INativeThreadListener {
    void onSuccess(String msg);
}
public native void nativeInThreadCallBack(INativeThreadListener listener);
JavaVM *gvm;
jobject gCallBackObj;
jmethodID gCallBackMid;

extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeInThreadCallBack(JNIEnv *env, jobject thiz,
                                                                   jobject call_back) {
    // 建立一個jni中的全域性參照
    gCallBackObj = env->NewGlobalRef(call_back);
    jclass cls = env->GetObjectClass(call_back);
    gCallBackMid = env->GetMethodID(cls, "onSuccess", "(Ljava/lang/String;)V");
    // 建立一個執行緒
    pthread_t pthread;
    jint ret = pthread_create(&pthread, nullptr, writeFile, nullptr);
    LOG_D("pthread_create ret=%d", ret);
}

這裡簡單說一下執行緒的幾個引數

 pthread_create
 引數1 pthread_t* pthread 執行緒控制程式碼
 引數2  pthread_attr_t const* 執行緒的一些屬性
 引數3 void* (*__start_routine)(void*) 執行緒具體執行的函數
 引數4 void* 傳給執行緒的引數
 返回值 int  0 建立成功
/**
 * 相當於java中執行緒的run方法
 * @return
 */
void *writeFile(void *args) {
    // 隨機字串寫入
    FILE *file;
    if ((file = fopen("/sdcard/thread_cb", "a+")) == nullptr) {
        LOG_E("fopen filed");
        return nullptr;
    }
    for (int i = 0; i < 10; ++i) {
        fprintf(file, "test %d\n", i);
    }
    fflush(file);
    fclose(file);
    LOG_D("file write done");

    // https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html
    JNIEnv *env = nullptr;
    // 將當前執行緒新增到Java虛擬機器器上,返回一個屬於當前執行緒的JNIEnv指標env
    if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
        jstring jstr = env->NewStringUTF("write success");
        // 回撥到java層
        env->CallVoidMethod(gCallBackObj, gCallBackMid, jstr);
        // 刪除jni中全域性參照
        env->DeleteGlobalRef(gCallBackObj);
        // 從Java虛擬機器器上分離當前執行緒
        gvm->DetachCurrentThread();
    }
    return nullptr;
}

其實還是jni中非子執行緒回撥到java方法中的三個步驟,只不是多了AttachCurrentThreadDetachCurrentThread的操作。基本的註釋在程式碼中體現了,另外關於檔案的寫入,屬於linux下c的基本操作這裡不多說了,不瞭解的可以看下有關知識。

備註:jni中有寫入檔案的操作,記得加入Android 許可權哦。

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

測試一下,結果:

// 看一下檔案內容,符合預期
tb8765ap1_bsp_1g:/ # cat /sdcard/thread_cb                                                                                                      
test 0
test 1
test 2
test 3
test 4
test 5
test 6
test 7
test 8
test 9

// 回撥,符合預期onSuccess的回撥執行在非ui執行緒中
2020-09-21 21:43:40.441 8004-8004/com.bj.gxz.jniapp D/JNI: JNI_OnLoad Call
2020-09-21 21:43:40.443 8004-8004/com.bj.gxz.jniapp D/JNI: onCall invoked,threadName:main
2020-09-21 21:43:40.443 8004-8004/com.bj.gxz.jniapp D/JNI: pthread_create ret=0
2020-09-21 21:43:40.447 8004-8067/com.bj.gxz.jniapp D/JNI: file write done
2020-09-21 21:43:40.448 8004-8067/com.bj.gxz.jniapp D/JNI: onSuccess invoked,msg:write success
2020-09-21 21:43:40.449 8004-8067/com.bj.gxz.jniapp D/JNI: onSuccess invoked,threadName:Thread-111

原始碼:https://github.com/ta893115871/JNIAPP

最後,祝大家中秋國慶做個三好學生(吃好喝好玩好)。