Jni 線程JNIEnv,JavaVM,JNI_OnLoad(GetEnv返回NULL?FindClass返回NULL?)


此文章是關於NDK線程的第二篇理論知識筆記。主要有兩個點,如下:

1.pthread_create(Too many arguements, expected 1) ?
2.線程中如何獲取JNIEnv?GetEnv返回NULL?
3.FindClass返回NULL ?
首先我們在主頁MainActivity的代碼如下:

public class MainActivity extends Activity {
 
    static {
        try {
            System.loadLibrary("native-lib");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    public native void nativeThreadEnvTest();
 
    public static String getUuid() {
        // 提供給nativeThreadEnvTest使用
        return UUID.randomUUID().toString();
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) ...
}
 nativeThreadEnvTest打算實現這樣一個功能:for循環調用MainActivity.getUuid方法,打印出5串不同的UUID。聽上去很簡單,邏輯代碼如下:

#include <unistd.h>
#include <pthread.h>
#include <assert.h>
 
void *pthread_run(void *arg) {
    JNIEnv *env = NULL;
    // get env ?
    char *name = (char *) arg;
    for (int i = 0; i < 5; ++i) {
        //char* uuid_cstr = ...
        LOGI("%s, No:%d, uuid:%s", name, i, uuid_cstr);
        sleep(1);
    }
 
    pthread_exit((void *) 0);
}
 
JNIEXPORT void JNICALL
Java_org_zzrblog_MainActivity_nativeThreadEnvTest(JNIEnv *env, jobject thiz) {
    pthread_t tid;
    pthread_create(&tid, NULL, pthread_run, (void *) "pthread1");
 
    //void* reval;
    //pthread_join(tid, &reval);
}

此時第一個坑點可能就會出現了:pthread_create報出錯誤提示 Too many arguements, expected 1(黑人三問號)

ctrl+左鍵,跳轉到頭文件pthread.h的定義,明明就是四個參數的啊?

rebuild,重啟AS,各種大法都沒解啊,怎么辦? 這里給出可行科學的解決方案,在頭文件添加如下宏定義就OJBK了!

#ifndef _PTHREAD_H_
#define _PTHREAD_H_
 
#include ...
// 添加宏定義 _Nonnull
#ifndef _Nonnull
#define _Nonnull
#endif

那么我們繼續功能實現,在線程執行函數pthread_run中,想要調用MainActivity.getUuid方法,必須得有env啊。那么我們怎么獲取env?可能就有大兄弟立馬說:在nativeThreadEnvTest傳入的env時NewGlobalRef啊,這樣就可以全局使用了!這好像確實是一個解決思路,好像還蠻好使的(因為兄弟你見識得太少了)。但是!BUT!However! 嚴謹的說,這種做法是不可取的。為什么?引用Google官方翻譯:

由於VM通常是多執行緒(Multi-threading)的執行環境。每一個執行緒在呼叫native函數時,所傳遞進來的JNIEnv指標值都是不同的。為了配合這種多執行緒的環境,C組件開發者在撰寫native函數時,可藉由JNIEnv指標值之不同而避免執行緒的資料沖突問題,才能確保所寫的native函數能安全地在Android的多執行緒VM里安全地執行。基於這個理由,當在呼叫C組件的函數時,都會將JNIEnv指標值傳遞到下一級函數使用。

看起來好像很抽象,似懂非懂的。但是我們必須知道:VM是多執行緒(Multi-threading) ,每個JNIEnv都是不同的!特別是在不同線程,都是獨自維護各自獨立的JNIEnv。  那么問題又回到最初的?怎么正確的獲取線程安全的JNIEnv? 

此時我們引入函數JNI_OnLoad 和 結構體JavaVM,在頭文件 jni.h 有它的定義:

/*
 * Prototypes for functions exported by loadable shared libs.  These are
 * called by JNI, not provided by JNI.
 */
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);

從注釋可知,JNI_OnLoad是由系統JNI回調的,並不由得開發者亂用,而且也不由JNI默認提供。不重寫這個方法系統就默認進行配置。在虛擬機VM加載c組件的時候(so)就會調用組件加載接口JNI_OnLoad(),在JNI_OnLoad()函數里,就透過VM之指標而取得JNIEnv之指標值,並存入env指標變數里。

這里的JavaVM就是虛擬機VM在JNI中的表示,一個進程JVM中只有一個JavaVM對象,這個對象是線程共享的。換言之這個JavaVM是能全局安全使用的,而且也只能在JNI_OnLoad的回調進行強引用賦值。 有了這個JavaVM,我們再調用AttachCurrentThread 附加當前線程到虛擬機VM當中,並返回線程對應的JNIEnv,我們就能愉快的擼碼了!

說到AttachCurrentThread,不能不提起JavaVM的另外一個接口 GetEnv,看上去GetEnv不就是獲取env的方法嗎?這么解釋吧,只有先AttachCurrentThread到JavaVM,分配到了獨立的JNIEnv之后,GetEnv第二個參數二級指針返回的env才有值。就是說JavaVM->GetEnv獲取的是,此線程有效的env。JavaVM->AttachCurrentThread是向虛擬機分配線程獨立的env。    所以一般在線程執行函數第一句是AttachCurrentThread,隨后就能用GetEnv了。

理論知識介紹到這里,我們繼續測試功能函數,現在代碼應該是長這樣的:

JavaVM *javaVM;
 
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
    LOGW("%s\n", "JNI_OnLoad startup ...");
    javaVM = vm;
    JNIEnv *env = NULL;
    jint result;
 
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) == JNI_OK) {
        LOGI("Catch JNI_VERSION_1_6\n");
        result = JNI_VERSION_1_6;
    }
    else if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) == JNI_OK) {
        LOGI("Catch JNI_VERSION_1_4\n");
        result = JNI_VERSION_1_4;
    }
    else {
        LOGI("Default JNI_VERSION_1_2\n");
        result = JNI_VERSION_1_2;
    }
 
    assert(env != NULL);
    // 動態注冊native函數 ...
    return result;
}
 
void *pthread_run(void *arg) {
    JNIEnv *env = NULL;
    // (*javaVM)->AttachCurrentThread(javaVM,&env,NULL)
    // (*javaVM)->GetEnv(javaVM, (void **)&env, JNI_VERSION_1_6)
    if ( (*javaVM)->AttachCurrentThread(javaVM,&env,NULL) != JNI_OK) {
        LOGE("javaVM->Env Error!\n");
        pthread_exit((void *) -1);
    }
 
    assert(env != NULL);
   
    // 自定義的類型 jclass
    jclass clazz = (*env)->FindClass(env, "org/zzrblog/MainActivity");
    jmethodID getUuid_mid = (*env)->GetStaticMethodID(env, clazz, "getUuid","()Ljava/lang/String;");
 
    char *name = (char *) arg;
    for (int i = 0; i < 5; ++i) {
        jobject uuid_jstr = (*env)->CallStaticObjectMethod(env, clazz, getUuid_mid);
        char* uuid_cstr = (char *) (*env)->GetStringUTFChars(env, uuid_jstr, NULL);
        LOGI("%s, No:%d, uuid:%s", name, i, uuid_cstr);
        sleep(1);
    }
 
    (*env)->ReleaseStringUTFChars(env, sys_uuid_jstr, sys_uuid_cstr);
    (*javaVM)->DetachCurrentThread(javaVM);
    pthread_exit((void *) 0);
}

我們在JNI_OnLoad函數全局引用JavaVM對象,然后就是模板代碼了,告訴系統虛擬機用哪個版本的JNI。此時調用JavaVM->GetEnv獲取的env是主線程的。所以我們能獲取成功。

然后我們進入線程執行函數,使用AttachCurrentThread請求分配當前線程安全的env,之后我們使用FindClass / GetStaticMethodID / CallStaticObjectMethod 等JNI API進行Java層的MainActivity.getUuid 靜態方法的調用。

一切看着都是那么順利,然后運行demo瞬間報錯(奸笑.jpg)一堆紅通通的錯誤啊!

為什么會找不到 org.zzrblog.MainActivity?此問題更好的體現了JNIEnv的線程獨立性問題了!如果FindClass用的是主線程env就不會報錯了。如果FindClass的是系統的UUID類也不會報錯了。但是現實生活沒有那么多如果!問題的原因就是只有主線程的env才有包含我們自定義(自己開發)的類類型,而 AttachCurrentThread的線程安全env只加載了系統的類類型,並不包含自定義的類類型。
所以問題的原因找到了,怎么解?既然env不是線程安全,不能直接引用。那么我們可以引用其他線程共享的調用對象啊,再通過GetObjectClass獲取jclass。不明白的同學看如下代碼:

jobject jMainActivity;
 
JNIEXPORT void JNICALL
Java_org_zzrblog_MainActivity_nativeThreadEnvTest(JNIEnv *env, jobject thiz) {
    if(jMainActivity == NULL) {
        //調用對象,創建全局引用
        jMainActivity = (*env)->NewGlobalRef(env, thiz);
    }
 
    pthread_t tid;
    pthread_create(&tid, NULL, pthread_run, (void *) "pthread1");
    //void* reval;
    //pthread_join(tid, &reval);
}

現在知道native方法的 static 和 非static的時候,傳入的第二個參數的意義和真實的用處了吧?

非static的時候,傳入jobject類型的thiz,就相當於MainActivity.this了。

staic的時候,傳入jcalss類型的clazz,就相當於MainActivity.class了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM