Android深刻理解JNI(二)類型轉換、方法簽名和JNIEnv

相關文章
Android深刻理解JNI系列html

前言

上一篇文章介紹了JNI的基本原理和註冊,這一篇接着帶領你們來學習JNI的數據類型轉換、方法簽名和JNIEnv。java

1.數據類型的轉換

首先給出上一篇文章中android_media_MediaRecorder.cpp中的android_media_MediaRecorder_start方法:
frameworks/base/media/jni/android_media_MediaRecorder.cppandroid

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_start方法有一個參數爲jobject類型,它是JNI層的數據類型,Java的數據類型到了JNI層就須要轉換爲JNI層的數據類型。Java的數據類型分爲基本數據類型和引用數據類型,JNI層對於這兩種類型也作了區分,咱們先來查看基本數據類型的轉換。git

1.1 基本數據類型的轉換

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 表明簽名格式,後文會介紹它。接着來看引用數據類型的轉換。數組

1.2 引用數據類型的轉換

Java Native Signature
全部對象 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

從上表可一看出,數組的JNI層數據類型須要以「Array」結尾,簽名格式的開頭都會有「[」。除了數組之外,其餘的引用數據類型的簽名格式都會以「;」結尾。
另外,引用數據類型還具備繼承關係,以下所示:微信

1337955954_3405.jpg
1337955954_3405.jpg

再來列舉MediaRecorder框架的Java方法:
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類型。post

2.方法簽名

前面表格已經列舉了數據類型的簽名格式,方法簽名就由簽名格式組成,那麼,方法簽名有什麼做用呢?咱們看下面的代碼。
frameworks/base/media/jni/android_media_MediaRecorder.cpp學習

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",
    (void *)android_media_MediaRecorder_native_setup},
   ...
};複製代碼

gMethods數組中存儲的是MediaRecorder的Native方法與JNI層方法的對應關係,
其中"()V"和 "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法簽名。咱們知道Java是有重載方法的,能夠定義方法名相同,但參數不一樣的方法,正由於如此,在JNI中僅僅經過方法名是沒法找到 Java中的具體方法的,JNI爲了解決這一問題就將參數類型和返回值類型組合在一塊兒做爲方法簽名。經過方法簽名和方法名就能夠找到對應的Java方法。
JNI的方法簽名的格式爲:

(參數簽名格式...)返回值簽名格式複製代碼

拿上面gMethods數組的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;」,後兩個參數的簽名爲「Ljava/lang/String;」,返回值類型void 的簽名爲「V」,組合起來就是上面的方法簽名。

若是咱們每次編寫JNI時都要寫方法簽名,也會是一件比較頭疼的事,幸虧Java提供了javap命令來自動生成方法簽名。咱們先寫一個簡單的MediaRecorder.java包含上面的native_setup方法:

public class MediaRecorder {
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    private native final void native_setup(Object mediarecorder_this, String clientName, String opPackageName) throws IllegalStateException;
}複製代碼

這個文件的在個人本地地址爲D:/Android/MediaRecorder.java,接着執行以下命令:

javac D:/Android/MediaRecorder.java複製代碼

執行命令後會生成MediaRecorder.class文件,最後使用javap命令:

javap -s -p D:/Android/MediaRecorder.class複製代碼

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

能夠很清晰的看到輸出的native_setup方法的簽名和此前給出的一致。

3.JNIEnv

JNIEnv 是一個指向所有JNI方法的指針,該指針只在建立它的線程有效,不能跨線程傳遞,所以,不一樣線程的JNIEnv是彼此獨立的,JNIEnv的主要做用有兩點:
1.調用Java的方法。
2.操做Java(獲取Java中的變量和對象等等)。

先來看JNIEnv的定義,以下所示。
libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的類型 
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的類型 
typedef const struct JNIInvokeInterface* JavaVM;
#endif複製代碼

這裏使用預約義宏__cplusplus來區分C和C++兩種代碼,若是定義了__cplusplus,則是C++代碼中的定義,不然就是C代碼中的定義。
在這裏咱們也看到了JavaVM,它是虛擬機在JNI層的表明,在一個虛擬機進程中只有一個JavaVM,所以,該進程的全部線程均可以使用這個JavaVM。經過JavaVM的AttachCurrentThread函數能夠獲取這個線程的JNIEnv,這樣就能夠在不一樣的線程中調用Java方法了。還要記得在使用AttachCurrentThread函數的線程退出前,務必要調用DetachCurrentThread函數來釋放資源。

jfieldID和jmethodID

在JNI中用jfieldID和jmethodID來表明Java類中的成員變量和方法,能夠經過JNIEnv的下面兩個方法來分別獲得:

jfieldID GetFieldID(jclass clazz,const char *name,const char *sig);
jmethodID GetFieldID(jclass clazz,const char *name,const char *sig);複製代碼

其中,jclass表明Java類,name表明成員方法或者成員變量的名字,sig爲這個方法和變量的簽名。
咱們來查看MediaRecorder框架的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");//1
    if (clazz == NULL) {
        return;
    }
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
    if (fields.context == NULL) {
        return;
    }
    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");//3
    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");//4
    if (fields.post_event == NULL) {
        return;
    }
}複製代碼

註釋1處,經過FindClass來找到Java層的MediaRecorder的Class對象,並賦值給jclass類型的變量clazz,所以,clazz就是Java層的MediaRecorder在JNI層的表明。註釋2和註釋3處的代碼用來找到Java層的MediaRecorder中名爲mNativeContext和mSurface的成員變量,並分別賦值給context和surface。註釋4出獲取Java層的MediaRecorder中名爲postEventFromNative的靜態方法,並賦值給post_event。其中fields的定義爲:

struct fields_t {
    jfieldID    context;
    jfieldID    surface;
    jmethodID   post_event;
};
static fields_t fields;複製代碼

將這些成員變量和方法賦值給jfieldID和jmethodID類型的變量主要是爲了效率考慮,若是每次調用相關方法時都要進行查詢方法和變量,顯然會效率很低,所以在MediaRecorder框架JNI層的初始化方法android_media_MediaRecorder_native_init中將這些jfieldID和jmethodID類型的變量保存起來,以供後續使用。

使用jfieldID和jmethodID

咱們保存了jfieldID和jmethodID類型的變量,接着怎麼使用它們呢,以下所示。
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);//1
}複製代碼

在註釋1處調用了JNIEnv的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);
    }
}複製代碼

這樣咱們就能在JNI層中訪問Java的靜態方法了。同理,若是想要訪問Java的方法則可使用JNIEnv的CallVoidMethod函數。
上面的例子是使用了jmethodID,接着來查看jfieldID:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz) {
    ALOGV("prepare");
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    jobject surface = env->GetObjectField(thiz, fields.surface);//1
    if (surface != NULL) {
        const sp<Surface> native_surface = get_surface(env, surface);
       ...
    }
    process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");複製代碼

在註釋1處調用了JNIEnv的GetObjectField函數,參數中的fields.surface用來保存Java層MediaRecorde中的成員變量mSurface,mSurface的類型爲Surface,這樣經過GetObjectField函數就獲得了mSurface在JNI層中對應的jobject類型變量surface 。

參考資料
《深刻理解Android卷一》
Android JNI原理分析
JNI總管:JNIEnv
Android Framework層JNI的使用淺析
JNI學習積累之二 ---- 數據類型映射、域描述符說明
JNI Tips


歡迎關注個人微信公衆號,第一時間得到博客更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,便可關注。

相關文章
相關標籤/搜索