Android-JNI開發(fā)系列《二》在jni層的線程中回調(diào)到j(luò)ava層

人間觀察

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

馬上國慶+中秋了?!?/p>

今天我們看一個(gè)比較常見的場景:
當(dāng)我們處理一個(gè)密集型計(jì)算數(shù)據(jù)(比如音視頻的軟編解碼處理,bitmap的特效處理等),這時(shí)候就需要用c/c++實(shí)現(xiàn)。當(dāng)在c/c++處理完后需要異步回調(diào)/通知到j(luò)ava中,這樣代碼看起來才很優(yōu)雅有氣質(zhì)。
如果你知道這個(gè)知識(shí)那就return吧?!?/p>

在Android中你可以用Thread+Handler很容易的來實(shí)現(xiàn),我相信你閉著眼都能寫了。但在jni層中不是這么簡單的,我們?nèi)绾螌?shí)現(xiàn)?

我們先看一下在jni中非子線程中如何回調(diào)再看下在子線程如何回調(diào)到j(luò)ava層中。

jni中非子線程回調(diào)到j(luò)ava方法中

和普通的在jni中調(diào)用java的實(shí)例方法沒啥區(qū)別,上代碼:

// java回調(diào)接口 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);
    // 獲取回調(diào)方法的id
    jmethodID mid = env->GetMethodID(cls, "onCall", "()V");
    // 調(diào)用java中的方法
    env->CallVoidMethod(call_back, mid);
}

總結(jié): 分三步

  1. 根據(jù)java的obj獲取jclass。jclass可以理解為java中的class對象(如果熟悉jni就沒啥問題)
  2. 根據(jù)1步中的jclass和方法名字和方法的簽名獲取該方法的jmethodID
  3. env調(diào)用java實(shí)例的方法。(當(dāng)前如果是靜態(tài)的只是調(diào)用的方法不一樣)

jni中的子線程回調(diào)到j(luò)ava方法

主要方法是JavaVM中的AttachCurrentThreadDetachCurrentThread兩個(gè)方法,這兩個(gè)是對應(yīng)的。
官方文檔:
官網(wǎng)doc地址

有關(guān)注釋

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虛擬機(jī)上
JNI接口指針(JNIEnv)僅在當(dāng)前線程中有效。
如果另一個(gè)線程需要訪問jvm,它必須首先調(diào)用AttachCurrentThread()將自己附加到 JVM并獲取JNI接口指針。
一旦連接到JVM上,本地線程(jni線程)的工作方式與在本地方法中運(yùn)行的普通Java線程一樣。
本機(jī)線程在調(diào)用DetachCurrentThread()來分離它自己之前一直連接到VM。

附加的線程應(yīng)該有足夠的堆棧空間來執(zhí)行合理數(shù)量的任務(wù)。
每個(gè)線程的堆棧空間分配是取決于操作系統(tǒng)。
例如,使用pthreads,可以在pthread_attr_t參數(shù)中為pthread_create指定堆棧大小。

而在調(diào)用JavaVM中的AttachCurrentThreadDetachCurrentThread我們需要拿到JavaVM *vm指針。怎么拿到這個(gè)呢?一種是調(diào)用JNI_CreateJavaVM加載并初始化Java虛擬機(jī),并返回指向JNI接口指針的指針。我們可以用另外一種jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)全局變量保存一下vm即可。
如果不了解JNI_OnLoad可以看上一篇文章 jni動(dòng)態(tài)庫的函數(shù)注冊

接下來,我們寫一個(gè)簡單的功能:在jni中創(chuàng)建一個(gè)線程實(shí)現(xiàn)一個(gè)寫入隨機(jī)字符串到文件(用來模擬線程任務(wù)的耗時(shí)),然后寫入完成后給java層一個(gè)回調(diào)告訴java層寫入成功。

// java回調(diào)接口 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) {
    // 創(chuàng)建一個(gè)jni中的全局引用
    gCallBackObj = env->NewGlobalRef(call_back);
    jclass cls = env->GetObjectClass(call_back);
    gCallBackMid = env->GetMethodID(cls, "onSuccess", "(Ljava/lang/String;)V");
    // 創(chuàng)建一個(gè)線程
    pthread_t pthread;
    jint ret = pthread_create(&pthread, nullptr, writeFile, nullptr);
    LOG_D("pthread_create ret=%d", ret);
}

這里簡單說一下線程的幾個(gè)參數(shù)

 pthread_create
 參數(shù)1 pthread_t* pthread 線程句柄
 參數(shù)2  pthread_attr_t const* 線程的一些屬性
 參數(shù)3 void* (*__start_routine)(void*) 線程具體執(zhí)行的函數(shù)
 參數(shù)4 void* 傳給線程的參數(shù)
 返回值 int  0 創(chuàng)建成功
/**
 * 相當(dāng)于java中線程的run方法
 * @return
 */
void *writeFile(void *args) {
    // 隨機(jī)字符串寫入
    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;
    // 將當(dāng)前線程添加到Java虛擬機(jī)上,返回一個(gè)屬于當(dāng)前線程的JNIEnv指針env
    if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
        jstring jstr = env->NewStringUTF("write success");
        // 回調(diào)到j(luò)ava層
        env->CallVoidMethod(gCallBackObj, gCallBackMid, jstr);
        // 刪除jni中全局引用
        env->DeleteGlobalRef(gCallBackObj);
        // 從Java虛擬機(jī)上分離當(dāng)前線程
        gvm->DetachCurrentThread();
    }
    return nullptr;
}

其實(shí)還是jni中非子線程回調(diào)到j(luò)ava方法中的三個(gè)步驟,只不是多了AttachCurrentThreadDetachCurrentThread的操作?;镜淖⑨屧诖a中體現(xiàn)了,另外關(guān)于文件的寫入,屬于linux下c的基本操作這里不多說了,不了解的可以看下有關(guān)知識(shí)。

備注:jni中有寫入文件的操作,記得加入Android 權(quán)限哦。

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

測試一下,結(jié)果:

// 看一下文件內(nèi)容,符合預(yù)期
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

// 回調(diào),符合預(yù)期onSuccess的回調(diào)運(yùn)行在非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

最后,祝大家中秋國慶做個(gè)三好學(xué)生(吃好喝好玩好)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容