Android : C++調用Java

轉載請標明出處:https://blog.csdn.net/qq_29621351/article/details/79870319

    通過這段時間接觸JNI的過程,覺得JNI裏面的坑還是挺多的,有的地方理解的也不是很周到,如果發現理解錯的地方,請大家能夠指出,我會立即改正。

JNI可以理解爲三層:Java層,JNI層,C++層。

    Java層就是Java語言編寫的程序,C++層是純粹的C/C++語言,而JNI層是函數映射層類似於下面這種函數形式,這種形式的函數對應Java中的一個具體函數,如果對應Java中的具體函數是靜態函數則第二個參數就和下面一樣,如果是非靜態函數則第二個參數換爲jobject obj。

JNIEXPORT jint JNICALL someMethod(JNIEnv* env, jclass cls,...) // ...表示其它參數,而不是變長函數參數。

JNI層直接調用Java

    如果想在JNI層直接調用Java層是很方便的,JNIEnv* env這個類型的變量直接提供了調用Java函數的方式,JNIEnv的含義是Java虛擬機的執行環境,可以通過它來操縱Java層中的類方法、對象方法,而jclass表示Java中的類,JNI層映射函數傳入的jclass和jobject表示該方法的類實例或對象實例,簡單的調用方法如下所示:

jclass clazz = env -> FindClass("utils/LogWriter");
jmethodID mid = env -> GetStaticMethodID(clazz,"aMethod","(Ljava/lang/String;Ljava/lang/String;)V");
env -> CallStaticVoidMethod(clazz,mid, strArgs1,strArgs2);

步驟1:表示獲得Java虛擬機中的類,參數"utils/LogWriter"表示Java中一個自定義的類,也可以是Java類庫中的一個類,包名的使用類定義文件中開頭 package 後面的路徑加上"/類名"。簡單地說就是跟 import 這個類所帶有的參數是一樣的。

步驟2:在獲取Java中的一個類後,要繼續得到這個類的方法,通過得到的類clazz和環境變量env、方法名aMethod來得到方法的id,類型爲jmethodID,還要描述方法返回值和傳入參數,本例中的描述方式以"(Ljava/lang/String;Ljava/lang/String;)V"表示,讀者可自行查閱描述方式的規則。

步驟3:通過環境變量env調用Java的方法,CallStaticVoidMethod爲調用靜態方法的方式。strArgs1和strArgs2爲Java方法傳入參數,在傳入之前先自己設定其值。

主線程中的C++層調用Java

    對於C++來講在主線程中調用Java和在子線程中調用Java的方式不是完全一樣的,對C++層來說,一般不會有傳入的JNIEnv*,只能在需要調用Java的地方獲取,獲取時用到了C++中的一些函數,想要使用這些函數,先要導入"jni.h",

    既然沒有JNIEnv*的變量那就只能自己獲取,因爲它是調用Java的唯一途徑,獲取的方式是通過JavaVM* vm獲取的,JavaVM和JNIEnv一樣都是JNI機制中非常重要的一個變量,JavaVM表示Java虛擬機,但是JavaVM* vm是在什麼地方獲取的?這裏有一個誤區,網上的很多博客都說用JNI_CreateJavaVM函數,這在純粹的Java語言中是可以的,但是在Android中不能這麼使用,因爲Java語言在一個進程中可以創建多個虛擬機變量,而在Android虛擬機在一個進程中只能創建唯一一個虛擬機變量,並且因爲Android在運行的時候就已經創建了一個虛擬機變量,所以絕不會允許再創建一個。不能創建虛擬機變量但是可以設置指針指向已存在的虛擬機。我們可以使用

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)

    這個函數是一個系統調用,我們在Java中執行System.loadLibrary("xxx.so")函數時,該函數就會自動被調用,並且調用參數中有一個JavaVM*類型的指針。我們可以通過一個全局變量把它保存下來,並用它創建JNIEnv*,然後再使用JNIEnv*。這種方式的過程如下(注意這個函數一定要返回一個JNI版本的宏):

JavaVM* jvm = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if(vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    return JNI_VERSION_1_6;
}

然後在其它函數中獲取JNIEnv*變量

JNIEnv* env = nullptr;
if(jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
{
    return JNI_ERR;
}

    此函數雖然定義爲JNI層函數的模式,但是寫在C++層中也是沒有問題的。獲取JNIEnv*變量後我們就可以以JNI層一樣的方式來調用Java語言,還有就是這裏獲得JNIEnv*變量是通過jni.h中提供的GetEnv函數獲得的。

子線程中的C++層調用Java

    在子線程中調用Java,又是不一樣的情況。但無論如何,只要想調用Java,就絕對少不了JNIEnv*變量的存在,事實上,也正是因爲JNIEnv*變量的某種特性導致了在子線程環境中調用方式與主線程的不同。

    剛纔講到,在android中每個進程只能有一個虛擬機變量,所以不能再創建,但是可以定義多個虛擬機指針指向同一個虛擬機,JNIEnv*變量也有點相似,可以定義多個指針指向虛擬機環境,但是JNIEnv還有一個特性,它是附着在具體線程之上的,在主線程中獲取JNIEnv*變量是需要調用JavaVM調用GetEnv方法,但是在子線程中要使用AttachCurrentThread方法,否則無效。

    在子線程下獲取JNIEnv*變量後,如果你在C++中調用的Java本身的類庫,那可能不會有問題,但若調用自定義的類,會出現找不到類的錯誤,因爲Java虛擬機的類加載器並沒有把自定義的類加載給子線程,解決的方案是我們需要在主線程中找到這個類,並把它加載爲全局類,然後通過類獲得方法ID,之後在子線程中使用這個全局類,如下:

JavaVM* jvm = nullptr;
jclass clazz = nullptr;
jclass global_clazz = nullptr;
jmethodID mid_static_method;

/*  executed after System.loadLibrary("xxx.so") */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if (vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    clazz = env -> FindClass("utils/JJLogWriter");
    if (clazz == nullptr)
    {
        return JNI_ERR;
    }
    global_clazz = (jclass)env->NewGlobalRef(clazz);
    if (global_clazz == nullptr) 
    {
        return JNI_ERR;
    }
    mid_static_method = env -> GetStaticMethodID(global_clazz,"c","(Ljava/lang/String;Ljava/lang/String;)V");

    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    env -> DeleteGlobalRef(global_clazz);
//    env -> DeleteLocalRef(mid_static_method);
    return;
}

在這裏調用了一個stoJstring函數,它是將const char*類型先轉換爲jbytearray類型,又將jbytearray類型轉換爲jstring類型,這樣做是爲了避免emoji類型的字符使jni.h中提供的NewStringUTF函數崩潰。stoJstring函數的定義如下。

jstring stoJstring(JNIEnv* env, const char* pat)
{
    jclass strClass = env->FindClass("java/lang/String");

    jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");

    jbyteArray bytes = env->NewByteArray(strlen(pat));

    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);

    jstring encoding = env->NewStringUTF("utf-8");

    jstring finalJstring = (jstring)env->NewObject(strClass, ctorID, bytes, encoding);

    env -> DeleteLocalRef(strClass);

    env -> DeleteLocalRef(bytes);

    env -> DeleteLocalRef(encoding);

    return finalJstring;
}

 以下是我當時寫的一個demo,貼出來方便自己以後隨意查看

/*
 * created by txc
**/
JavaVM* jvm = nullptr;
jclass clazz = nullptr;
jclass global_clazz = nullptr;
jmethodID mid_static_method;
/*  executed after System.loadLibrary("xxx.so") */

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if (vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    JNIEnv* env = nullptr;
    
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    clazz = env -> FindClass("utils/JJLogWriter");
    if (clazz == nullptr)
    {
        return JNI_ERR;
    }
    global_clazz = (jclass)env->NewGlobalRef(clazz);
    if (global_clazz == nullptr) 
    {
        return JNI_ERR;
    }
    mid_static_method = env -> GetStaticMethodID(global_clazz,"c","(Ljava/lang/String;Ljava/lang/String;)V");

    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    env -> DeleteGlobalRef(global_clazz);
    return;
}

jstring stoJstring(JNIEnv* env, const char* pat)
{
    jclass strClass = env->FindClass("java/lang/String");

    jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");

    jbyteArray bytes = env->NewByteArray(strlen(pat));

    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);

    jstring encoding = env->NewStringUTF("utf-8");

    jstring finalJstring = (jstring)env->NewObject(strClass, ctorID, bytes, encoding);

    env -> DeleteLocalRef(strClass);

    env -> DeleteLocalRef(bytes);

    env -> DeleteLocalRef(encoding);

    return finalJstring;
}

void JJLog2Java(const char* location,const char* cstr)
{
    JNIEnv* env = NULL;
    jstring strArgs = NULL;
    jstring strLocation = NULL;
    if (jvm->AttachCurrentThread((void**)&env, NULL))
    {
        return;
    }
    strArgs = stoJstring(env,cstr);
    strLocation = env -> NewStringUTF(location);
    env -> CallStaticVoidMethod(global_clazz,mid_static_method, strLocation,strArgs);
    env -> DeleteLocalRef(strArgs);
    env -> DeleteLocalRef(strLocation);
    jvm->DetachCurrentThread();
}

    此外,在JNI程序的編寫中要時刻注意內存泄漏問題,不僅是C/C++語言本身的內存泄漏,JNI還有自己的內存管理方式,稍有不當就會造成內存泄漏,這裏推薦一篇JNI內存泄漏的文章,講得非常透徹。

JNI內存泄漏問題參考:https://www.ibm.com/developerworks/cn/java/j-lo-jnileak/

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章