AndroidNDK開發系列教程6:JNI函式註冊(JNI_OnLoad)

乾初發表於2018-02-07

在使用native方法前都會先載入該native方法的so檔案,通常在一個類的靜態程式碼塊中進行載入,當然也可以在建構函式,或者呼叫前載入。jvm在載入so時都會先呼叫so中的JNI_OnLoad函式,如果你沒有重寫該方法,那麼系統會給你自動生成一個。JNI_OnLoad方法的呼叫順序可以參考我的另一篇博文:JNI_OnLoad呼叫時機,下面我們可以在該方法中對自己的函式進行註冊。這就很爽了,jni預設的那個方法命名又臭又長,改的時候不注意還可能該錯。現在我們可以定義自己的函式名稱,只需要在JNI_OnLoad中註冊下對應的對映。在Google官網也有介紹:https://developer.android.com/training/articles/perf-jni.html

1. JNI_OnLoad簡介

在編寫JNI方法時有兩種方法:一種是標準的通過javah生成標頭檔案,然後自己實現對應的cpp檔案,這種辦法也是官方推薦的。還有一種方法是在JNI_OnLoad函式中進行函式對映,將java裡面的方法對映到自己實現的方法。

當Android的DVM(Virtual Machine)執行到C元件裡的System.loadLibrary()函式時,首先會去執行C元件裡的JNI_OnLoad()函式。它的用途有二:
1. 告訴VM此C元件使用那一個JNI版本。
如果你的*.so檔沒有提供JNI_OnLoad()函式,VM會預設該*.so檔是使用最老的JNI 1.1版本。
由於新版的JNI做了許多擴充,如果需要使用JNI的新版功能,
例如JNI 1.4的java.nio.ByteBuffer,就必須藉由JNI_OnLoad()函式來告知VM。
2. 由於VM執行到System.loadLibrary()函式時,就會立即先呼叫JNI_OnLoad(), 所以C元件的開發者可以藉由JNI_OnLoad()來進行C元件內的初期值之設定(Initialization) 。
3. 在so被成功解除安裝時,會回撥另一個JNI方法:JNI_UnOnLoad。這兩個方法宣告如下:

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

其中第一個引數vm表示DVM虛擬機器,該vm在應用程式中僅有一個,可以儲存在native的靜態變數中,供其他函式或其他執行緒使用。其返回值表示當前需要native library需要的版本。

2. 舉個例子

首先在Java中寫好native方法:

    //JNI_OnLoads使用例項
    public native void jniOnLoadTest();
    public native String jniOnload1(Person person);

然後編寫對應的native方法

//空方法可以不用傳任何欄位
//也可以傳這兩個引數:void onLoadTest(JNIEnv*env,jobject obj);兩個引數含義和用javah生成的一致。
void onLoadTest() {
    LOGE("調到我啦");
}
//如果有引數,那麼需要加上前面兩個引數,不然會導致引數不對應。引數含義和javah生成的標頭檔案中引數含義一致。
jstring onloadTest1(JNIEnv *env, jobject instance, jobject obj) {
    jclass pCls = env->GetObjectClass(obj);
    jfieldID nameFid = env->GetFieldID(pCls, "name", "Ljava/lang/String;");
    jstring name = (jstring) env->GetObjectField(obj, nameFid);
    char *cname = jstringToChar(env, name);
    char *tmp = new char[100];
    sprintf(tmp, "我來自Native,我叫:%s", cname);
    jstring result = charTojstring(env, tmp);
    return result;
}

然後在JNI_OnLoad中註冊改函式對映

//註冊函式對映
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv *pEnv = NULL;
    //獲取環境
    jint ret = vm->GetEnv((void**) &pEnv, JNI_VERSION_1_6);
    if (ret != JNI_OK) {
        LOGE("jni_replace JVM ERROR:GetEnv");
        return -1;
    }
    //在{}裡面進行方法對映編寫,第一個是java端方法名,第二個是方法簽名,第三個是c語言形式簽名(括號內表示方法返回值)
    JNINativeMethod g_Methods[] = {{"jniOnLoadTest", "()V", (void*) onLoadTest},
                                   {"jniOnload1", "(Lzqc/com/example/Person;)Ljava/lang/String;", (jstring*)onloadTest1}
    };
    jclass cls = pEnv->FindClass("zqc/com/example/NativeTest");
    if (cls == NULL) {
        LOGE("FindClass Error");
        return -1;
    }
    //動態註冊本地方法
    ret = pEnv->RegisterNatives(cls, g_Methods,sizeof(g_Methods) / sizeof(g_Methods[0]));
    if (ret != JNI_OK) {
        LOGE("Register Error");
        return -1;
    }
    //返回java版本
    return JNI_VERSION_1_6;
}

其中JNINativeMethod的結構如下:

typedef struct {  
    const char* name;     // java層對應的方法名稱  
    const char* signature;// 該方法的返回值型別和引數型別  
    void*       fnPtr;    // native中對應的函式指標  
} JNINativeMethod;  
    //註冊本地方法,第一個是方法對應的類,第二個是方法對映,第三個是對映方法的個數
    jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
        jint nMethods)
    { return functions->RegisterNatives(this, clazz, methods, nMethods); }

通過以上方法就可以實現方法對映,而不用遵循原有的命名規則。

3. 總結

JNI_OnLoad是載入so時最先呼叫的方法,而且該方法會把JavaVM* vm指標傳過來,這樣在native就可以儲存該指標,該指標在整個應用程式中僅有一個,可以跨執行緒使用。我們通過在該方法中註冊函式對映,當然也可以在該方法中做其他操作。比如我們可以在該方法中進行版本校驗,也可以校驗當前呼叫該so的應用是否合乎要求。


相關文章