Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊

劉望舒發表於2017-06-21

前言

JNI不僅僅在NDK開發中應用,它更是Android系統中Java與Native互動的橋樑,不理解JNI的話,你就只能停留在Java Framework層。這一個系列我們來一起深入學習JNI。

1.JNI概述

Android系統按語言來劃分的話由兩個世界組成,分別是Java世界和Native世界。那為什麼要這麼劃分呢?Android系統由Java寫不好嗎?除了效能的之外,最主要的原因就是在Java誕生之前,就有很多程式和庫都是由Native語言寫的,因此,重複利用這些Native語言編寫的庫是十分必要的,況且Native語言編寫的庫具有更好的效能。
這樣就產生了一個問題,Java世界的程式碼要怎麼使用Native世界的程式碼呢,這就需要一個橋樑來將它們連線在一起,而JNI就是這個橋樑。

Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊

通過JNI,Java世界的程式碼就可以訪問Native世界的程式碼,同樣的,Native世界的程式碼也可以訪問Java世界的程式碼。
為了講解JNI我們需要分析系統的原始碼,在即將出版的《Android進階之光》的最後一章中我拿MediaPlayer框架做了舉例,這裡換MediaRecorder框架來舉例,它和MediaPlayer框架的呼叫過程十分類似。

2.MediaRecorder框架概述

MediaRecorder我們應該都不陌生,它用於錄音和錄影。這裡不會主要介紹MediaRecorder框架,而是MediaRecorder框架中的JNI。

Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊

Java世界對應的是MediaRecorder.java,也就是我們應用開發中直接呼叫的類。JNI層對用的是libmedia_jni.so,它是一個JNI的動態庫。Native層對應的是libmedia.so,這個動態庫完成了實際的呼叫的功能。

3.Java層的MediaRecorder

我們先來檢視MediaRecorder.java的原始碼,擷取部分和JNI有關的部分如下所示。
frameworks/base/media/java/android/media/MediaRecorder.java

public class MediaRecorder{
static {
        System.loadLibrary("media_jni");//1
        native_init();//2
    }
...   
    private static native final void native_init();//3
...
    public native void start() throws IllegalStateException;
...    
}複製程式碼

在靜態程式碼塊中首先呼叫了註釋1處的程式碼,用來載入名為“media_jni“的動態庫,也就是libmedia_jni.so。接著呼叫註釋2處的native_init方法,註釋3處的native_init方法用native來修飾,說明它是一個native方法,表示由JNI來實現。MediaRecorder的start方法同樣也是一個native方法。
對於Java層來說只需要載入對應的JNI庫,接著宣告native方法就可以了,剩下的工作由JNI層來完成。

4.JNI層的MediaRecorder

MediaRecorder的JNI層由android_media_recorder.cpp實現,native方法native_init和start的JNI層實現如下所示。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;

    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
   ...
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }
}

static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
    ALOGV("start");
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}複製程式碼

android_media_MediaRecorder_native_init方法是native_init方法在JNI層的實現,android_media_MediaRecorder_start方法則是start方法在JNI層的實現。那麼,native_init方法是如何找到對應的android_media_MediaRecorder_native_init方法的呢?
這就需要了解JNI方法註冊的知識。

5.JNI方法註冊

JNI方法註冊分為靜態註冊和動態註冊,其中靜態註冊多用於NDK開發,而動態註冊多用於Framework開發。

靜態註冊

在AS中新建一個Java Library名為media,這裡仿照系統的MediaRecorder.java,寫一個簡單的MediaRecorder.java,如下所示。

package com.example;
public class MediaRecorder {
    static {
        System.loadLibrary("media_jni");
        native_init();
    }

    private static native final void native_init();
    public native void start() throws IllegalStateException;
}複製程式碼

接著進入專案的media/src/main/java目錄中執行如下命令:

javac com.example.MediaRecorder.java
javah com.example.MediaRecorder複製程式碼

第二個命令會在當前目錄中(media/src/main/java)生成com_example_MediaRecorder.h檔案,如下所示。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MediaRecorder */

#ifndef _Included_com_example_MediaRecorder
#define _Included_com_example_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_MediaRecorder
 * Method:    native_init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
  (JNIEnv *, jclass);//1

/*
 * Class:     com_example_MediaRecorder
 * Method:    start
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif複製程式碼

nativeinit方法被宣告為註釋1處的方法,格式為`Java包名類名方法名`,註釋1處的方法名多了一個“l”,這是因為nativeinit方法有一個“”,它會在轉換為JNI方法時變成“_l”。
其中JNIEnv 是一個指向全部JNI方法的指標,該指標只在建立它的執行緒有效,不能跨執行緒傳遞。
jclass是JNI的資料型別,對應Java的java.lang.Class例項。jobject同樣也是JNI的資料型別,對應於Java的Object。關於JNIEnv
以及JNI的資料型別會在本系列的後續文章中進行介紹。

當我們在Java中呼叫native_init方法時,就會從JNI中尋找Java_com_example_MediaRecorder_native_1init方法,如果沒有就會報錯,如果找到就會為native_init和Java_com_example_MediaRecorder_native_1init建立關聯,其實是儲存JNI的方法指標,這樣再次呼叫native_init方法時就會直接使用這個方法指標就可以了。
靜態註冊就是根據方法名,將Java方法和JNI方法建立關聯,但是它有一些缺點:

  • JNI層的方法名稱過長。
  • 宣告Native方法的類需要用javah生成標頭檔案。
  • 初次呼叫JIN方法時需要建立關聯,影響效率。

我們知道,靜態註冊就是Java的Native方法通過方法指標來與JNI進行關聯的,如果Native方法知道它在JNI中對應的方法指標,就可以避免上述的缺點,這就是動態註冊。

動態註冊

JNI中有一種結構用來記錄Java的Native方法和JNI方法的關聯關係,它就是JNINativeMethod,它在jni.h中被定義:

typedef struct {
    const char* name;//Java方法的名字
    const char* signature;//Java方法的簽名資訊
    void*       fnPtr;//JNI中對應的方法指標
} JNINativeMethod;複製程式碼

系統的MediaRecorder採用的就是動態註冊,我們來檢視它的JNI層是怎麼做的。
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static const JNINativeMethod gMethods[] = {
...
    {"start",            "()V",      (void *)android_media_MediaRecorder_start},//1
    {"stop",             "()V",      (void *)android_media_MediaRecorder_stop},
    {"pause",            "()V",      (void *)android_media_MediaRecorder_pause},
    {"resume",           "()V",      (void *)android_media_MediaRecorder_resume},
    {"native_reset",     "()V",      (void *)android_media_MediaRecorder_native_reset},
    {"release",          "()V",      (void *)android_media_MediaRecorder_release},
    {"native_init",      "()V",      (void *)android_media_MediaRecorder_native_init},
   ...
};複製程式碼

上面定義了一個JNINativeMethod型別的gMethods陣列,裡面儲存的就是MediaRecorder的Native方法與JNI層方法的對應關係,其中註釋1處"start"是Java層的Native方法,它對應的JNI層的方法為android_media_MediaRecorder_start。"()V"是start方法的簽名資訊,關於Java方法的簽名資訊後續的文章會介紹。
只定義JNINativeMethod 型別的陣列是沒有用的,還需要註冊它,註冊的方法為register_android_media_MediaRecorder:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

//JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaRecorder", gMethods, NELEM(gMethods));
}複製程式碼

register_android_media_MediaRecorder方法中return了AndroidRuntime的registerNativeMethods方法,如下所示。
frameworks/base/core/jni/AndroidRuntime.cpp

/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}複製程式碼

registerNativeMethods方法中又return了jniRegisterNativeMethods方法:
external/conscrypt/src/openjdk/native/JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
   ...
    if (env->RegisterNatives(c.get(), gMethods, numMethods) < 0) {//1
        char* msg;
        (void)asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        env->FatalError(msg);
    }
    return 0;
}複製程式碼

從註釋1處可以看出,最終呼叫的JNIEnv的RegisterNatives方法,JNIEnv在JNI中十分重要,後續文章會介紹它。

register_android_media_MediaRecorder方法最終會呼叫JNIEnv的RegisterNatives方法,但是register_android_media_MediaRecorder方法是在哪被呼叫的呢?答案在register_android_media_MediaRecorder方法的註釋上:JNI_OnLoad in android_media_MediaPlayer.cpp。這個JNI_OnLoad方法會在System.loadLibrary方法後呼叫,因為多媒體框架中的很多框架都要進行JNINativeMethod陣列註冊,因此,註冊方法就被統一定義在android_media_MediaPlayer.cpp中的JNI_OnLoad方法中,如下所示。
frameworks/base/media/jni/android_media_MediaPlayer.cpp

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto *bail;
    }
    assert(env != NULL);
    ...
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto *bail;
    }
    if (register_android_media_MediaRecorder(env) < 0) {//1
        ALOGE("ERROR: MediaRecorder native registration failed\n");
        goto *bail;
    }
  ...
   result = JNI_VERSION_1_4;
bail:
    return result;
}複製程式碼

在JNI_OnLoad方法中呼叫了整個多媒體框架的註冊JNINativeMethod陣列的方法,註釋1處的呼叫了register_android_media_MediaRecorder方法,同樣的,MediaPlayer框架的註冊JNINativeMethod陣列的方法register_android_media_MediaPlayer也被呼叫了。

關於動態註冊就講到這裡,更多深入JNI的知識請見本系列後續的文章。

參考資料
《深入理解Android卷I》


歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。

Android 深入理解 JNI(一)JNI 原理與靜態、動態註冊

相關文章