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

前言

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

1.JNI概述

Android系統按語言來劃分的話由兩個世界組成,分別是Java世界和Native世界。那爲何要這麼劃分呢?Android系統由Java寫很差嗎?除了性能的以外,最主要的緣由就是在Java誕生以前,就有不少程序和庫都是由Native語言寫的,所以,重複利用這些Native語言編寫的庫是十分必要的,何況Native語言編寫的庫具備更好的性能。
這樣就產生了一個問題,Java世界的代碼要怎麼使用Native世界的代碼呢,這就須要一個橋樑來將它們鏈接在一塊兒,而JNI就是這個橋樑。
java


經過JNI,Java世界的代碼就能夠訪問Native世界的代碼,一樣的,Native世界的代碼也能夠訪問Java世界的代碼。
爲了講解JNI咱們須要分析系統的源碼,在即將出版的《Android進階之光》的最後一章中我拿MediaPlayer框架作了舉例,這裏換MediaRecorder框架來舉例,它和MediaPlayer框架的調用過程十分相似。

2.MediaRecorder框架概述

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


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方法註冊的知識。post

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目錄中執行以下命令:spa

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相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,便可關注。

相關文章
相關標籤/搜索