Android框架層基礎2————JNI原理

Android框架層基礎2————JNI原理

源碼基於Android8.0分析

一.JNI概述

JNI 即java Native Interface縮寫,即java本地調用。通過jni可以做到以下兩點:

  • java程序中的函數可以調用Native語言書寫的函數,Native一般指的c/c++編寫的函數
  • Native程序的函數可以調用java中的函數

在Android源碼中,jni大量的使用,同時也有很多的應用的場景,比如音視頻開發,熱修復,插件化,逆向開發,源碼調用。接下來先看看jni的使用

二.基於靜態註冊的JNI使用

關於Jni的使用詳細的步驟可以參考這篇博客

Android JNI學習(二)——實戰JNI之“hello world”

下面我簡單說一下jni的調用
具體的環境配置相關的,可以參考上面博客。

1.java中生明native方法

public class JniTest {
    static {
        System.loadLibrary("jni-test");
    }

    public static void main(String[] args) {
        JniTest jniTest = new JniTest();
        System.out.printf(jniTest.get());
        
    }

    public static native String get();

    public static native void set(String str);
}

上面的類中,先在靜態代碼塊中,加載了動態庫的過程,同時聲明瞭兩個native方法,get和set。

2.獲得jni的頭文件

  • 根據javac生成class文件
  • 在根據javah生成頭文件
  • javac 包名/JniTest.java(包名以/分割)
  • javah 包名.JniTest(包名以.分割)

生成的一個com_heshucheng_androidjni_JniTest頭文件

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

#ifndef _Included_com_heshucheng_androidjni_JniTest
#define _Included_com_heshucheng_androidjni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_heshucheng_androidjni_JniTest
 * Method:    get
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_heshucheng_androidjni_JniTest_get
  (JNIEnv *, jclass);

/*
 * Class:     com_heshucheng_androidjni_JniTest
 * Method:    set
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_heshucheng_androidjni_JniTest_set
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

說明

  • 函數名遵循下面規則:Java_包名_類名_方法
  • jstring是代表String類型的參數
  • JNIEnv:表示一個指向JNI環境的指針,可以通過它來訪問JNI提供的接口方法
  • jobject:表示Java對象中的this
  • JNIEXPORT 和 JNICALL:它兩是JNI中所定義的宏,可以在jni.h中查到定義

3.實現JNI方法

接下來就是實現jni方法,以C實現爲例.
創建一個子目錄,名稱隨意。將之前生成的頭文件複製到該目錄下。並創建test.c文件

#include "com_heshucheng_androidjni_JniText"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_com_heshucheng_androidjni_JniTest_get
  (JNIEnv  *env, jobject obj){
     printf("invoke get");
     return (*env)->NewStringUTF(env,"Hellow form JNI");

}

JNIEXPORT void JNICALL Java_com_heshucheng_androidjni_JniTest_set
  (JNIEnv *env, jclass obj, jstring,string){
    printf("invoke set");
    char* str = (char *)(* env)->GetStringUTFChars(env,string,NULL);
    printf("%s\n"str);
    (* env)->ReleaseStringUTFChars(env,string,str)  
    
  }

最後編譯so庫,並在java中調用。

4.小結

上面的這種方式我們稱爲靜態註冊。

這種情景下,當我們在java中調用native方法get時,就會從JNI中尋找Java_com_heshucheng_androidjni_JniTest_get函數,如果沒有就報錯,如果找到就和其建立聯繫,其實就是報存JNI函數指針,這樣再次調用native_init方法時直接使用這個函數指針就可以了。

靜態註冊就是個人那就方法名,將java的native方法通過方法指針和JNI進行關聯。如果java的Native方法知道它在JNI中的函數指針,就可以避免上述缺點。

三.基於動態註冊的android源碼

下面我就以AudioRecord源碼中的jni調用爲例,分析jni

1.源碼中jni使用

下面看看AudioRecord.java中的stop方法

    public void stop()
    throws IllegalStateException {
        if (mState != STATE_INITIALIZED) {
            throw new IllegalStateException("stop() called on an uninitialized AudioRecord.");
        }

        // stop recording
        synchronized(mRecordingStateLock) {
            handleFullVolumeRec(false);
            native_stop();
            mRecordingState = RECORDSTATE_STOPPED;
        }
    }

......
  private native final void native_stop();

這個native方法的實習是在android_media_AudioRecord中的android_media_AudioRecord_stop

目錄:framework/base/core/jni/android_media_AudioRecord.cpp

android_media_AudioRecord_stop(JNIEnv *env, jobject thiz)
{
    sp<AudioRecord> lpRecorder = getAudioRecord(env, thiz);
    if (lpRecorder == NULL ) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }

    lpRecorder->stop();
    //ALOGV("Called lpRecorder->stop()");
}

2.動態註冊的實現

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

typedf struct{
	coust char* name;//java方法的名字、	
	coust char* signture;//java方法的簽名信息
	void* fnPtr;//JNI中對應的方法指針
}

android_media_AudioRecord中有一個數組gMethods

目錄:framework/base/core/jni/android_media_AudioRecord.cpp

static const JNINativeMethod gMethods[] = {
    // name,               signature,  funcPtr
    {"native_start",         "(II)I",    (void *)android_media_AudioRecord_start},
    {"native_stop",          "()V",    (void *)android_media_AudioRecord_stop},
    {"native_setup",         "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILjava/lang/String;J)I",
                                      (void *)android_media_AudioRecord_setup},
    {"native_finalize",      "()V",    (void *)android_media_AudioRecord_finalize},
    {"native_release",       "()V",    (void *)android_media_AudioRecord_release},
    {"native_read_in_byte_array",
                             "([BIIZ)I",
                                     (void *)android_media_AudioRecord_readInArray<jbyteArray>},
    {"native_read_in_short_array",
                             "([SIIZ)I",
                                     (void *)android_media_AudioRecord_readInArray<jshortArray>},
    {"native_read_in_float_array",
                             "([FIIZ)I",
                                     (void *)android_media_AudioRecord_readInArray<jfloatArray>},
    {"native_read_in_direct_buffer","(Ljava/lang/Object;IZ)I",
                                       (void *)android_media_AudioRecord_readInDirectBuffer},
    {"native_get_buffer_size_in_frames",
                             "()I", (void *)android_media_AudioRecord_get_buffer_size_in_frames},
    {"native_set_marker_pos","(I)I",   (void *)android_media_AudioRecord_set_marker_pos},
    {"native_get_marker_pos","()I",    (void *)android_media_AudioRecord_get_marker_pos},
    {"native_set_pos_update_period",
                             "(I)I",   (void *)android_media_AudioRecord_set_pos_update_period},
    {"native_get_pos_update_period",
                             "()I",    (void *)android_media_AudioRecord_get_pos_update_period},
    {"native_get_min_buff_size",
                             "(III)I",   (void *)android_media_AudioRecord_get_min_buff_size},
    {"native_setInputDevice", "(I)Z", (void *)android_media_AudioRecord_setInputDevice},
    {"native_getRoutedDeviceId", "()I", (void *)android_media_AudioRecord_getRoutedDeviceId},
    {"native_enableDeviceCallback", "()V", (void *)android_media_AudioRecord_enableDeviceCallback},
    {"native_disableDeviceCallback", "()V",
                                        (void *)android_media_AudioRecord_disableDeviceCallback},
    {"native_get_timestamp", "(Landroid/media/AudioTimestamp;I)I",
                                       (void *)android_media_AudioRecord_get_timestamp},
};

gMethods數組存儲的是AudioRecord的Native方法與JNI層函數的對應關係,但是隻有這個gMethods數組還是不夠的,還需要進行註冊。

註冊的函數是:register_android_media_AudioRecord

目錄:framework/base/core/jni/android_media_AudioRecord.cpp

 int register_android_media_AudioRecord(JNIEnv *env)
{
    .....
    return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
}

繼續追蹤

目錄:framework/base/core/jni/core_jni_helpers.h

static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,
                                       const JNINativeMethod* gMethods, int numMethods) {
    int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);
    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
    return res;
}

在這其中返回了AndroidRuntime的registerNativeMethods函數,繼續追蹤

目錄:framework/base/core/jni/AndroidRuntime

    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

在registerNativeMethods函數中又返回了jniRegisterNativeMethods,他被定義再JNI類幫助類的JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);

    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp,
                     "Native registration unable to find class '%s'; aborting...",
                     className) == -1) {
            // Allocation failed, print default warning.
            msg = "Native registration unable to find class; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    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;
}

在這裏我們可以看見,最終調用了JNIENV的RegisterNatives來完成JNI的註冊。關於JNIENV在後面會有比較詳細的說明。

四.數據類型的轉化

上面我們解決了jni的註冊問題,接下來讓我們看看jni的數據轉換問題。在java中調用Native函數傳遞的是java類型參數,這些參數到jni層會轉爲不同的參數。

java的數據類型分爲基本數據類型和引用數據類型。jni對於兩者是區別對待的。

1.基本類型的轉換

基本類型轉換比較簡單。具體如下表所示,最後一列代表簽名格式,後面會介紹它。

Java Native類型 符號屬性 字長 簽名格式
boolean jboolean 無符號 8位 B
byte jbyte 無符號 8位 C
char jchar 無符號 16位 D
short jshort 有符號 16位 F
int jint 有符號 32位 I
long jlong 有符號 64位 S
float jfloat 有符號 32位 J
double jdouble 有符號 64位 Z
void void V

上面的基本類型,除了最後一行,其他只要在前面加上j即可

2.引用類型的轉換

java引用類型 native 簽名格式
All objects jobject L+classname +;
Class jclass Ljava/lang/Class;
String jstring Ljava/lang/String;
Throwable jthrowable Ljava/lang/Throwable;
Object[] jobjectArray [L+classname +;
boolean[] jbooleanArray [Z
byte[] jbyteArray [B
char[] jcharArray [C
short[] jshortArray [S
int[] jintArray [I
long[] jlongArray [J
float[] floatArray [F
double[] jdoubleArray [D

從上圖可以看出,所有的數組的JNI層數據類型需要以“Array”結尾,簽名格式的開頭都有“[”

引用數據類型也是具有繼承關係的,如下圖所示
在這裏插入圖片描述
我們以之前靜態註冊生成的頭文件類型分析

 Java_com_heshucheng_androidjni_JniTest_set
  (JNIEnv *env, jclass obj, jstring,string)

可以看出,java層的string 在jni層變成了就string類型

3.方法簽名

前面我們看每個了類型後面都有一個簽名格式,方法簽名就是由簽名格式組成的,那麼,方法簽名有什麼作用呢?我們回到前面的gMethods數組中

static const JNINativeMethod gMethods[] = {
...
  {"native_release",       "()V",    (void *)android_media_AudioRecord_release},
  {"native_read_in_byte_array", "([BIIZ)I",(void *)android_media_AudioRecord_readInArray<jbyteArray>},
 ...
 }

在gMethods數組中的, “()V"和”([BIIZ)I"就是簽名方法。

產生原因
我們都知道java是由重載方法的,可以定義方法名相同,反參數不同的方法,正因爲如此,jni中僅僅通過方法名時無法找到java中具體方法的,JNI爲了解決這一問題就將參數類型和返回值組合在一起作爲方法簽名。通過方法簽名和方法名就可以一起找到對應的java方法。、、

簽名格式

(參數簽名格式…)返回值簽名格式

在生成方法簽名的時候,java也提供了javap命令來自動生成方法簽名,具體使用如下

javap -s - p 路徑/類目.class

其中s表示輸出內部類型簽名,p表示打印所有方法和成員(默認打印public),最終會在cmd中輸出結果。

五.JNIEnv介紹

1.JNIEnv概述

JNIEnv是一個指向全部JNI方法的指針。該指針只在創建它的線程有效,不能跨進程傳遞,因此不同線程的JNIEnv是彼此獨立的,JNIEnv的主要作用有兩點:

  • 調用java方法
  • 操作java(獲取java中的變量和對象)

JNIEnv內部結構
在這裏插入圖片描述

2. JNIEnv的定義

下面我們來看看JNIEnv的定義:

目錄:libnativehelper/include_Jni/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; //c++中JNIEnv定義
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//c中的JNI類型
typedef const struct JNIInvokeInterface* JavaVM;
#endif

上面的代碼中,使用__cplusplus來區分c和c++兩種代碼,可以看到c++類型是_JNIEnv,c是JNINativeInterface。繼續用查看定義

目錄:libnativehelper/include_Jni/nativehelper/jni.h

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)
	...
	//尋找java指定名稱的類
    jclass FindClass(const char* name) 
    { return functions->FindClass(this, name); }

    //得到java中的方法
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }

	//得到java中的成員變量
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
...
}

_JNIEnv是一個結構體,它的內部又包含了JNINativeInterface,在_JNIEnv中定義了很多函數,上面列舉了三個常見的函數。同時也可以發現,無論,最終他們還是調用了JNINativeInterface中定義的函數。

來看看JNINativeInterface中的定義

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結構體對應的函數指針,上面只是給了3個函數對應的函數指針定義。通過這些函數指針的定義,就能夠定位到虛擬機中的JNI函數表,從而實現了JNI層可以調用Java世界的方法了。

如何在同一個進程,但是不同線程中調用java方法

在上面的源碼中,我們可以發現一個JavaVM,他是虛擬機在JNI層的代表,在一個虛擬機進程中只存在一個JavaVM,因此,該進程所以的線程都可以使用這個JavaVM,通過調用Java的AttchCurrent函數可以獲取這個線程的JNIEnv,這樣就可以在不同的線程中調用java方法了,還要記得在使用AttachCurrentThread函數的線程退出前,務必要調用DetachCurrentThread函數來釋放資源

3.jfieldID和jmethodID

前面我們知道,通過JNIEnv可以操作java對象和方法。那麼他是如果實現的?

我們知道,其實一個java對象實際上是由它的成員對象和成員函數來操作的。所以在JNI規則中,用jfieldID 和jmethodID 來表示Java類的成員變量和成員函數,它們通過JNIEnv的下面兩個函數可以得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

其中,jclass代表Java類,name表示成員函數或成員變量的名字,sig爲這個函數和變量的簽名信息。如前所示,成員函數和成員變量都是類的信息,這兩個函數的第一個參數都是jclass。

獲得jfieldID和jmethodID的過程

    jclass clazz;
	//Java層的MediaRecorder的Class對象
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
    //獲取jfieldID對象
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
    if (fields.context == NULL) {
        return;
    }
   
    //獲取jmethodID對象
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");//4
    if (fields.post_event == NULL) {
        return;
    }

當獲取成功之後就可以直接調用到java

//傳入了上面獲得的jmethodID對象
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);//1

六.JNI和垃圾回收

在java中,存在四種引用類型,強軟弱虛,這四種類型對虛擬機回收垃圾有不同程度的影響。同樣的,在JNI中,也存在不同的引用類型,即本地引用,全局引用,弱全局引用,下面分別來介紹他們

1.本地引用

JNIEnv提供的函數所提供的引用類型基本都是本地引用,因此本地引用也是JNI中最常見的引用類型,本地引用的特點如下:

  • 當Native函數返回時,這個本地引用就會自動被釋放
  • 只在創建它的線程有效,不能跨線程使用
  • 局部引用是JVM負責的引用類型,受JVM管控

2.全局引用

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

  • 在Native函數返回時不會被自動釋放掉,因此全局引用需要手動來進行釋放,並且不會被GC掉
  • 全局引用是可以跨線程使用的
  • 全局引用不受到JVM的管控

全局引用是通過JNIEnv的NewGlobalRef函數用來創建全局引用,調用JNIEnv的DeleteGlobalRef函數來釋放全局引用

3.弱全局引用

弱全局引用是一種特殊的全局引用,它和全局引用的特點相似,不同的是弱全局引用是可以被gc回收的,弱全局引用被GC回收後,弱全局引用被GC回收之後會指向NULL,所以在訪問弱全局引用之前,要首先判斷它是否被回收了,方法就是JNIEnv的isSameObject函數來判斷

弱全局引用是通過NewWeakGlobalRef函數創建的,DeleteWeakGlobalRef來進行釋放

七.JNI中的異常處理

NI中也有異常,不過它和C++、Java的異常不太一樣。當調用JNIEnv的某些函數出錯後,會產生一個異常,但這個異常不會中斷本地函數的執行,直到從JNI層返回到Java層後,虛擬機纔會拋出這個異常。雖然在JNI層中產生的異常不會中斷本地函數的運行,但一旦產生異常後,就只能做一些資源清理工作了(例如釋放全局引用,或者ReleaseStringChars)。如果這時調用除上面所說函數之外的其他JNIEnv函數,則會導致程序死掉。


 virtualbool scanFile(const char* path, long long lastModified,

long long fileSize)

 {

       jstring pathStr;

       //NewStringUTF調用失敗後,直接返回,不能再幹別的事情了。

        if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

       ......

}

JNI層函數可以在代碼中截獲和修改這些異常,JNIEnv提供了三個函數進行幫助:

  • ExceptionOccured函數,用來判斷是否發生異常。
  • ExceptionClear函數,用來清理當前JNI層中發生的異常。
  • ThrowNew函數,用來向Java層拋出異常。

異常處理是JNI層代碼必須關注的事情,讀者在編寫代碼時務小心對待。

八.參考資料

《Android藝術開發探索》
《深入理解Android 卷一》
《Android進階解密》

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