Android-JNI解析

目錄

JNI概述

MediaRecorder框架中的JNI

Java Framework層的MediaRecorder

JNI層的MediaRecorder

Native方法注冊

數據類型的轉換

方法簽名

解析JNIEnv

參考《Android進階解密》

JNI概述

JNI(Java Native Interface,Java本地接口),是Java與其他語言通信的橋梁。這不是Android系統所獨有的,而是Java所有,當出現一些用Java語言無法處理的任務時,就可以使用JNI技術來實現。

JNI不只是應用于Android開發,它有著非常廣泛的應用場景。JNI在Android中的應用主要有:音視頻開發、熱修復、插件話、逆向開發、系統源碼調用等等。為了方便使用JNI技術,Android提供了NDK這個工具集合,NDK開發是基于JNI的,它和JNI開發本質上并沒有區別,理解JNI原理,NDK開發也會很容易掌握。

Android系統按語言來劃分的話分為兩個層面:分別是Java層Native層。通過JNIJava層可以訪問Native層,同樣的Native層也可以訪問Java層。下面以MediaRecorder框架中的JNI舉例來理解系統中的JNI

MediaRecorder框架中的JNI

MediaRecorder是Android系統提供給我們用于錄音和錄像的框架。Java Framework層對應的是MediaRecorder.java,也就是我們平時開發在應用中直接調用的類。JNI層對應的是libmedia_jni.so,它是JNI的一個動態庫。Native層對應的是libmedia.so,這個動態庫完成來實際的功能。

Java Framework層的MediaRecorder

我們先看一下MediaRecorder.java的源碼:

frameworks/base/media/java/android/media/MediaRecorder.java

public class MediaRecorder{
    static {
        //加載名字為media_jni的動態庫
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    //JNI注冊
    private static native final void native_init();
    ......
    public native void start() throws IllegalStateException;
    ......
}

上述代碼指截取部分JNI相關的代碼:

  • 在靜態代碼塊中首先加載名字為media_jni的動態庫,也就是libmedia_jni.so
  • 然后接著調用了native_init ()方法,該方法會調用Native層方法,用來完成JNI的注冊。
  • start()方法也是一個Native方法。

對于Java Framework層來說只需要加載對應的JNI庫,接著聲明native方法就可以了,剩下的工作由JNI層來完成。

JNI層的MediaRecorder

MediaRecorderJNI層是由android_media_MediaRecorder.cpp實現,native方法:native_initstart的代碼實現如下:

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);
    if (mr == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    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層的實現。那么它們是如何找到對應的方法的呢?下面我們首先了解一下JNI方法注冊的知識。

Native方法注冊

Native方法注冊分為動態注冊靜態注冊,其中靜態注冊多用于NDK開發,而動態注冊多用于Framework開發。下面分別來看一下這兩種注冊方式。

靜態注冊

在Android Studio中新建一個Java Library,命名為media,仿照系統的MediaRecorder.java,代碼如下:


public class MediaRecorder {
    static{
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    public native void start() throws IllegalStateException;
}

編寫完成后,對MediaRecorder.java進行編譯和生成JNI方法:進入項目的media/src/main/java目錄中,執行以下命令:

javac com/example/media/MediaRecorder.java //編譯
javah com.example.media.MediaRecorder //生成頭文件

第二個命令會生成com_example_media_MediaRecorder.h文件,內容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_media_MediaRecorder */
#ifndef _Included_com_example_media_MediaRecorder
#define _Included_com_example_media_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_media_MediaRecorder
 * Method:    native_init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_native_1init
  (JNIEnv *, jclass);
/*
 * Class:     com_example_media_MediaRecorder
 * Method:    start
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_start
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

在Java中的native_init()方法被聲明為Java_com_example_media_MediaRecorder_native_1init方法,以“Java”開頭說明是在Java平臺中調用JNI方法的,后面的com_example_media_MediaRecorder_native_1init指的是包名 + 類名 + 方法名的格式。我們會發現還多了一個1 ,這是因為Java中的native_init方法包含了"_",轉換成JNI方法后變成了“_1”

此外方法還多了幾個參數:

  • JNIEnv:是Native層中Java環境的代表,通過該類型的指針就可以在Native層中訪問Java層的代碼,它只在創建它的線程中有效,不能跨線程傳遞。
  • jclass:是JNI的屬性類型,對應Java的java.lang.Class實例。
  • jobject:同樣也是JNI屬性類型,對應Java的Object。

當我們在Java中調用native_init()方法時,就會從JNI中尋找Java_com_example_media_MediaRecorder_native_1init函數,如果沒有就會報錯,如果有就會為native_initJava_com_example_media_MediaRecorder_native_1init建立關聯,其實就是報錯JNI函數指針。這樣再次調用的時候直接使用這個函數指針就可以了。

靜態注冊就是根據方法名,將Java方法和JNI函數建立關聯,這樣會有一些缺點:

  • JNI層函數名過長。
  • 聲明native方法的類需要用javah生成頭文件。
  • 初次調用native方法時需要建立關聯,影響效率。

動態注冊

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

//JNINativeMethod類型的數組,數組名字為gMethods
static const JNINativeMethod gMethods[] = {
    ......
    {"start",         "()V", (void *)android_media_MediaRecorder_start},
    {"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,里面存儲的是native方法于JNI層函數的對應關系。只定義是沒有用的,還需要注冊它,注冊的函數為:register_android_media_MediaRecorder:

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaRecorder", gMethods, NELEM(gMethods));
}

通過該方法的注釋我們知道該方法是在JNI_OnLoad函數中調用的。這個函數會在System.loadLibrary函數后調用,因為多媒體框架中很多框架都要進行JNINativeMethod類型的數組注冊,因此函數注冊被統一定義在android_media_MediaPlayer.cppJNI_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) {
        ALOGE("ERROR: MediaRecorder native registration failed\n");
        goto bail;
    }
    ......
    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

register_android_media_MediaRecorder方法中返回了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);
}

在該方法中又返回了jniRegisterNativeMethods函數,該函數被定義在JNI的幫助類JNIHelp.cpp中:
libnativehelper/JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    ......
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    return 0;
}

從上面代碼可以看出最終調用的是JNIEnvRegisterNatives函數來完成JNI注冊的。
動態注冊要比靜態注冊復雜一些。但是它解決來靜態注冊的缺點,可以說一勞永逸。

動態注冊是直接存儲Java的native方法與它對應的JNI中的函數指針。

數據類型的轉換

Java層的數據類型到來JNI層就需要轉換為JNI層的數據類型。Java的數據類型分為基本數據類型引用數據類型,JNI層對于這兩種數據類型也做來區分,下面就分別來看一下。

基本數據類型的轉換

Java Native Signature
byte jbyte B
char jchar C
double jdouble D
float jfloat F
int jint I
short jshort S
long jlong J
boolean jboolean Z
void void V

除了最后一個void,其他的數據類型只需要在前面加上“j”就可以了。Signature表示的是簽名格式。

引用數據類型轉換

Java Native Signature
Object jobject L + classname + ;
Class jclass Ljava/lang/Class;
String jstring Ljava/lang/String;
Throwable jthrowable Ljava/lang/Throwable;
object[] jobjectArray [L + classname + ;
byte[] jbyteArray [B
char[] jcharArray [C
double[] jdoubleArray [D
float[] jfloatArray [F
int[] jintArray [I
short[] jshortArray [S
long[] jlongArray [J
boolean[] jbooleanArray [Z
繼承關系

下面以MediaRecorder為例看一下類型的轉換:
frameworks/base/media/java/android/media/MediaRecorder.java

private native void _setOutputFile(FileDescriptor fd, long offset, long length)
    throws IllegalStateException, IOException;

_setOutputFile方法對應的JNI層的方法為:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_setOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
    ......
}

對比以上兩個方法可以看到** FileDescriptor被轉換成了 jobject long被轉換成了 jlong**。

方法簽名

方法簽名是由簽名格式組成的,上面在介紹數據類型轉換的時候每種數據類型都給出了對應的簽名格式。那么方法簽名有什么用呢?我們先看一下方法簽名是什么樣子的:
``

static const JNINativeMethod gMethods[] = {
    ......
    {"native_init",  "()V", (void *)android_media_MediaRecorder_native_init},
    {"native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
    ......
};

其中“()V”和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法簽名。Java中的方法是有重載的,可以定義同名的方法,但參數不同。正因為如此,在JNI中通過方法名是無法找到Java中對應的具體方法的,JNI為了解決這一問題就將參數類型和返回值類型組合在一起作為方法簽名。通過方法簽名和方法名就可以找到對應的Java方法。
JNI方法簽名的格式為:

(參數1簽名格式參數2簽名格式...)返回值簽名格式

native_setup函數為例,它在Java中的定義如下:

private native final void native_setup(Object mediarecorder_this,
            String clientName, String opPackageName) throws IllegalStateException;

它在JNI中的方法簽名為:

(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V

native_setup函數的第一個參數的簽名為:"Ljava/lang/Object;",第2和第3個參數的簽名為:“Ljava/lang/String;”,返回值的簽名格式為:“V”。通過參數的簽名格式我們可以找到java中對應的參數類型。

通過Java提供的javap命令可以自動生成方法簽名。

解析JNIEnv

JNIEnvNative層Java環境的代表,通過JNIEnv *指針就可以在Native層中訪問Java層中的代碼。它只在創建它的線程中有效,不能跨線程傳遞,因此不同線程的JNIEnv是彼此獨立的。
JNIEnv的主要作用:

  • 調用Java的方法。
  • 操作Java中的變量和對象等。

JNIEnv的定義如下:
libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
//C++中JNIEnv類型
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
//C語言中JNIEnv類型
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

這里使用預定義宏__cplusplus來區分C語言和C++兩種代碼,如果定義了__cplusplus(編譯的是C++源文件),就使用C++代碼中的定義,否則就是C語言的定義。JavaVM:它是虛擬機在JNI層的代表,在一個虛擬機進程中只有一個JavaVM,因此,該進程的所有線程都共享這個JavaVM。通過JavaVMAttachCurrentThread函數可以獲取這個線程的JNIEnv,這樣就可以在不同的線程中調用Java方法了。

在使用AttachCurrentThread函數的線程退出前,務必要調用DetachCurrentThread函數來釋放資源。
在C++中JNIEnv的類型是_JNIEnv,下面我們看一下它是如何定義的:
libnativehelper/include/nativehelper/jni.h

struct _JNIEnv {
    const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
    ......
     jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    ......
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
    ......
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
    ......
}

從上面代碼可以看到_JNIEnv是一個結構體,其中內部又包含了JNINativeInterface。在_JNIEnv中定義了很多的函數。這里只貼出了比較常用的3個函數,FindClass函數用來找到Java中指定名稱的類,GetMethodID函數用來獲取Java中的方法,GetFieldID函數用來獲取Java中的成員變量。這3個函數都調用了JNINativeInterface中定義的函數,因此可以看出,無論是C語言還是C++,JNIEnv的類型都和JNINativeInterface有關系,下面看一下它的定義:
libnativehelper/include/nativehelper/jni.h

struct JNINativeInterface {
    ......
    jclass    (*FindClass)(JNIEnv*, const char*);
    ......
    jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    ......
    jfieldID  (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
    ......
}

JNINativeInterface同樣也是一個結構體,在它里面定義了很多和JNIEnv結構體對應的函數指針。通過這些函數指針的定義,就能夠定義到虛擬機中的JNI函數表,從而實現了JNI層在虛擬機中的函數調用,這樣JNI就可以調用Java層的方法了。

在C語法中,JNIEnv是一個結構體指針:struct JNINativeInterface* JNIEnv

jfieldID和jmethodID

_JNIEnv結構體中定義了很多的函數,這些函數都會有不同的返回值,如下所示:

struct _JNIEnv {
    const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
    ......
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
    ......
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
    ......
}

這個兩個函數的返回值分別為:jmethodIDjfieldID,分別用來代表Java類中的方法和成員變量。jclass代表Java類,name:代表方法名或者成員變量的名字,sig:代表這個方法或者成員變量的簽名。接下來我們看一下這兩個函數在MediaRecorder框架中的使用:
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.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
    if (fields.surface == NULL) {
        return;
    }
    jclass surface = env->FindClass("android/view/Surface");
    if (surface == NULL) {
        return;
    }
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }
}

在上述函數的開始處,通過FindClass函數來找到Java層的MediaRecorder的Class對象,并賦值給jclass類型的變量clazz,所以,clazz就是Java層MediaRecorderJNI層的代表。緊接著找到Java層的MediaRecorder中名字為mNativeContextmSurface的成員變量,并分別賦值給fieldscontextsurface,最后找到名字為postEventFromNative的靜態方法,并賦值給fieldspost_event。其中fields的定義如下:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

struct fields_t {
    jfieldID    context;
    jfieldID    surface;
    jmethodID   post_event;
};
static fields_t fields;

android_media_MediaRecorder_native_init函數中將Java層中的成員變量和方法賦值給了jfieldIDjmethodID保存起來,這樣不用每次調用的時候都去查詢。下面看一下它們是如何使用的:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
    ALOGV("JNIMediaRecorderListener::notify");
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
}

調用CallStaticVoidMethod函數時傳入的參數就包含了fields.post_event,該參數代表的是Java層MediaRecorder的靜態方法postEventFromNative,下面看一下該方法的實現:
frameworks/base/media/java/android/media/MediaRecorder.java

private static void postEventFromNative(Object mediarecorder_ref,
                                        int what, int arg1, int arg2, Object obj){
    MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
    if (mr == null) {
        return;
    }
    if (mr.mEventHandler != null) {
        Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
        mr.mEventHandler.sendMessage(m);
    }
}

在該方法中創建了一個消息,然后通過mEventHandler來發送處理,這樣就會切換到應用程序的主線程中。該方法是通過JNIEnvCallStaticVoidMethod函數來調用的,也就是說通過它可以訪問Java層的靜態方法,同理,通過CallVoidMethod函數可以訪問Java層的非靜態方法。

引用類型

和Java的引用類型一樣,JNI也有引用類型,它們分別是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)

本地引用

JNIEnv提供的函數所返回的引用基本上都是本地引用,因此本地引用也是JNI中最常見的引用類型。本地引用的特點主要有以下幾點:

  • 當native函數返回時,這個本地引用就會被自動釋放。
  • 只在創建它的線程有效,不能跨線程使用。
  • 局部引用是JVM負責的引用類型,受JVM管理。
    下面通過一個示例來說明:
    frameworks/base/media/jni/android_media_MediaRecorder.cpp
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
    ......
}

FindClass函數返回的clazz就是本地引用,它會在android_media_MediaRecorder_native_init函數調用返回后自動釋放,我們也可以調用JNIEnvDeleteLocalRef函數來手動刪除本地引用,該函數的應用場景主要是在native函數返回之前占用了大量內存,需要手動刪除本地引用。

全局引用

全局引用和本地引用幾乎是相反的,它主要有以下幾個特點:

  • 在native函數返回時不會被自動釋放,因此全局引用需要手動進行釋放,并且不會被GC回收。
  • 全局引用是可以跨線程使用的。
  • 全局引用不受JVM管理。
    JNIEnvNewGlobalRef函數用來創建全局引用,調用DeleteLocalRef函數來釋放全局引用。
    下面通過一個示例來看一下全局引用的使用:
    ``
JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{
    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == NULL) {
        ALOGE("Can't find android/media/MediaRecorder");
        jniThrowException(env, "java/lang/Exception", NULL);
        return;
    }
    mClass = (jclass)env->NewGlobalRef(clazz);
    mObject  = env->NewGlobalRef(weak_thiz);
}

clazz是本地引用,在下面通過NewGlobalRef函數將它變成了全局引用mClass,該全局引用是在JNIMediaRecorderListener析構函數中釋放,這里就不貼出源碼了。

弱全局引用

弱全局引用是一種特殊的全局引用,它和全局引用的特點相似,不同的是弱全局引用是可以被GC回收的,被回收后會指向NULL。通過JNINewWeakGlobalRef函數來創建弱全局引用,調用DeleteWeakGlobalRef函數來釋放弱全局引用,由于它可能被GC回收,因此在使用之前要先判斷它是否被回收了,通過IsSameObject函數來判斷。

Kotlin實戰

Flutter實戰

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。