【Android 系統開發】_「核心技術」篇 -- JNI

開篇

核心源碼

關鍵類 路徑
MediaScanner.java frameworks/base/media/java/android/media/MediaScanner.java
android_media_MediaScanner.cpp frameworks/base/media/jni/android_media_MediaScanner.cpp
android_media_MediaPlayer.cpp frameworks/base/media/jni/android_media_MediaPlayer.cpp
AndroidRuntime.cpp frameworks/base/core/jni/AndroidRuntime.cpp

JNI概述

JNI是Java Native Interface的縮寫,中文譯爲「Java本地調用」,通俗地說,JNI是一種技術,經過這種技術能夠作到如下兩點:html

      ✨ Java程序中的函數能夠調用Native語言寫的函數,Native通常指的是C/C++編寫函數;
      ✨ Native程序中的函數能夠調用Java層的函數,也就是說在C/C++程序中能夠調用Java的函數;java

在平臺無關的Java中,爲何要建立一個與Native相關的JNI技術呢?這豈不是破壞了Java的平臺無關特性嗎?JNI技術的推出主要有如下幾個方面的考慮:
      ✨ 承載Java世界的虛擬機是用Native語言寫的,而虛擬機又運行在具體的平臺上,因此虛擬機自己沒法作到平臺無關。然而,有了JNI技術後,就能夠對Java層屏蔽不一樣操做系統平臺之間的差別了。這樣,就能實現Java自己的平臺無關特性。
      ✨ 在Java誕生以前,不少程序都是用Native語言寫的,隨後Java後來受到追捧,而且迅速發展,可是做爲一門高級語言,沒法將軟件世界完全的改變。那麼既然Native模塊實現了許多功能,那麼在Java中直接經過JNI技術去使用它們不久能夠了?android

因此,咱們能夠把JNI看做一座將Native世界和Java世界互聯起來的一座橋樑(特殊說明:JNI層的代碼也是用Native寫的哦!)。數據庫

原理圖以下:數組

流程圖.png

一概的講原理很枯燥,咱們直接以實際的代碼做爲範例來學習JNI的原理和實際使用!服務器

MediaScanner

若是你是作Android系統開發和維護工做的,那麼你確定聽過MediaScanner,那咱們就拿它來舉例,看看它和JNI之間是如何關聯的。函數

(MediaScanner是Android平臺中多媒體系統的重要組成部分,它的功能是掃描媒體文件,獲得諸如歌曲時長、歌曲做者等媒體信息,並將他們存入到媒體數據庫中,拱其餘應用程序使用。)工具

MediaScanner和它的JNI:源碼分析

流程圖.png

咱們簡單說明下這個流程圖:學習

      ✨ Java世界對應的是MediaScanner,而這個MediaScanner類有一些函數須要由Native層來實現(定義了一些Native函數,具體實現代碼在Native層)
      ✨ JNI層對應的是libmedia_jni.so。
             · media_jni是JNI庫的名字,其中下劃線前的「media」是Native層庫的名字,這裏就是libmedia庫。下劃線後的「jni」表示它是一個JNI庫。
             · Android平臺基本上都採用「lib模塊名_jni.so」來命名JNI庫。
      ✨ Native層對應的是libmedia.so,這個庫完成了實際的功能。
      ✨ MediaScanner將經過JNI庫libmedia_jni.so和Native層的libmedia.so交互。

源碼分析 - Java層

MediaScanner.java

咱們先來看看MediaScanner在Java層中關於JNI的代碼:

package android.media;

public class MediaScanner implements AutoCloseable {
    static {                        // static語句
        // 這個咱們以前說過,media_jni爲JNI庫的名字,實際加載動態庫的時候會將其拓展成libmedia_jni.so
        System.loadLibrary("media_jni");    
        native_init();              // 調用native_init函數
    }
    ... ...

    private native void processFile(String path, String mimeType, MediaScannerClient client);
    ... ...
    
    private static native final void native_init();  // 申明一個native函數。native爲Java的關鍵字,表示它由JNI層實現。

    ... ...
}

OK,以上代碼列出了兩個重要的要點:(1)加載JNI庫;(2)調用Java的native函數

加載JNI庫

咱們前面說到過,若是Java要調用native函數,就必須經過一個位於JNI層的動態庫來實現。那麼這個動態庫在何時、什麼地方加載?

原則上,在調用native函數以前,咱們能夠在任什麼時候候、任何地方去加載動態庫。但通常通行的作法就是在類的static語句中加載,調用System.loadLibrary方法就能夠了。

native函數

咱們發現native_init和processFile函數前面都有Java的關鍵字native,這個就表示函數將由JNI層來實現。

因此在Java層面去使用JNI只要作兩項工做:(1)加載對應的JNI庫;(2)申明由關鍵字native修飾的函數。

源碼分析 - JNI層

實現函數

接下來咱們看下Java層中定義的兩個native函數在JNI層的實現。

native_init的JNI層實現

static const char* const kClassMediaScanner =
        "android/media/MediaScanner";

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

processFile的JNI層實現

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    ... ...

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    ... ...

    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

這邊咱們來解答一個問題,咱們確實是知道MediaScanner的native函數是JNI層去實現的,可是系統是如何知道Java層的native_init函數對應的就是JNI層的android_media_MediaScanner_native_init函數呢?

註冊JNI函數

不知道你有沒有注意到native_init函數位於android.media這個包中,它的全路徑名應該是android.media.MediaScanner.native_init,而JNI層函數的名字是android_media_MediaScanner_native_init。

是否是很神奇?名字對應着,惟一的區別就是「.」這個符號變成了「_」。由於在Native語言中,符號「.」有着特殊的意義,因此JNI層須要把Java函數名稱(包括包名)中的「.」換成「_」。也就是經過這種方式,native_init找到了本身JNI層的本家兄弟android.media.MediaScanner.native_init。

咱們知道了Java層native函數對應JNI層的函數的原理,但有個問題,咱們知道是哪一個函數,可是想要把兩個函數關聯起來(也就是說去調用它)就涉及到JNI函數註冊的問題(不註冊,就沒有關聯,沒有關聯就沒法調用)。

靜態方法註冊

這種方法很簡單,很暴力!直接根據函數名來找對應的JNI函數,它須要Java的工具程序javah參與,總體流程以下:

      ✨ 先編寫Java代碼,而後編譯生成.class文件。
      ✨ 使用Java的工具程序javah,採用命令「javah -o output packagename.classname」,這樣它會生成一個叫output.h的JNI層頭文件。其中packagename.classname是Java代碼編譯後的class文件,而在生成的output.h文件裏,聲明瞭對應的JNI層函數,只要實現裏面的函數便可。

這個頭文件的名字通常都會使用packagename_class.h的樣式,例如MediaScanner對應的JNI層頭文件就是android_media_MediaScanner.h。

/* DO NOT EDIT THIS FILE - it is machine generated*/
  #include <jni.h>        // 必須包含這個頭文件,不然編譯通不過
/* Header for class android_media_MediaScanner */

#ifndef _Included_android_media_MediaScanner
#define _Included_android_media_MediaScanner
#ifdef _cplusplus
extern "C" {
#endif
... ...     // 略去一部份內容

// processFile對應的JNI函數
JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile(JNIEnv *, jobject, jstring, jstring, jobject);

... ...     // 略去一部份內容

// native_init對應的JNI函數
JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit(JNIEnv *, jclass);

#ifdef _cplusplus
}
#endif
#endif

從上面代碼中能夠發現,native_init和processFile的JNI層函數被聲明成:

// Java 層函數名中若是由一個「_」, 轉換成JNI後就變成了「l」
JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile
JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit

Ok,那麼靜態方法中native函數是如何找到對應的JNI函數的呢?

當Java層調用native_init函數時,它會從對應的JNI庫中尋找Java_android_media_MediaScanner_native_init函數,若是沒有,就會報錯。若是找到,則會爲這個native_init和Java_android_media_MediaScanner_native_init創建一個關聯關係,其實就是保存JNI層函數的函數指針。之後再調用native_init函數時,直接使用這個函數指針就能夠了,固然這項工做是由虛擬機完成的。

從這裏能夠看出,靜態方法就是根據函數名來創建Java函數與JNI函數之間的關聯關係的,並且它要求JNI函數的名字必須遵循特定的格式。

這種方法有三個弊端,以下:

      ✨ 須要編譯全部聲明瞭native函數的Java類,每一個所生成的class文件都得用javah生成一個頭文件;
      ✨ javah生成的JNI層函數名特別長,書寫起來很不方便;
      ✨ 初次調用native函數時須要根據函數名稱搜索對應的JNI函數來創建關聯關係,這樣會影響運行效率。

因此咱們是否有辦法克服以上三點弊端?咱們知道靜態方法是去動態庫裏找一遍,而後創建關聯關係,之後再根據這個函數指針去調用對應的JNI函數,那麼若是咱們直接讓native函數直接知道JNI層對應函數的函數指針,是不就Ok了?

這就是下面咱們要介紹的第二種方法:動態註冊法!

動態方法註冊

咱們知道Java native函數和JNI函數是一一對應的,這個就像咱們key-value同樣,那麼若是有一個結構來保存這種關聯關係,那麼經過這個結構直接能夠找到彼此的關聯,是否是就效率就高多了?

答案是確定的,動態註冊就是這麼幹的!在JNI技術中,用來記錄這種一一對應關係的,是一個叫 JNINativeMethod 的結構,其定義以下:

typedef struct {
    char *name;                      // Java中native函數的名字,不用攜帶包的路徑,例如:native_init
    char *signature;                 // Java中函數的簽名信息,用字符串表示,是參數類型和返回值類型的集合
    void *fnPtr;                     // JNI層對應函數的函數指針,注意它是 void* 類型
}JNINativeMethod;

下面咱們看看如何使用這個結構體,看下MediaScanner JNI層是如何作的。

// 定義一個JNINativeMethod數組,其成員就是MediaScanner中全部native函數的一一對應關係。
static const JNINativeMethod gMethods[] = {
    ... ...

    {
        "processFile",                                    // Java中native函數的函數名
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",  // processFile的簽名信息
        (void *)android_media_MediaScanner_processFile    // JNI層對應的函數指針
    },
    ... ...

    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },
    ... ...
    
};

是否是很一目瞭然?定義好了,不能直接用啊,固然須要註冊一下。

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
// 註冊JNINativeMethod數組
int register_android_media_MediaScanner(JNIEnv *env)
{
    // 調用AndroidRuntime的registerNativeMethods函數,第二個參數代表是Java中的哪一個類
    // 咱們在講解Zygote原理時,聊過建立Java虛擬機,註冊JNI函數的內容
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

AndroidRunTime類提供了一個registerNativeMethods函數來完成註冊的工做,下面來看下registerNativeMethods的實現:

/*
 * Register native methods using JNI.
 */
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    // 調用jniRegisterNativeMethods函數完成註冊
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

其實,jniRegisterNativeMethods是Android平臺中爲了方便JNI使用而提供的一個幫助函數,其代碼以下:

int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = (*env)->FindClass(env, className);
    ... ...
    // 其實是調用JNIEnv的RegisterNatives函數完成註冊的
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        return -1;
    }
}

我知道你看到這邊已經頭疼了,調用來調用去,看上去很麻煩,是否是?其實動態註冊的工做,只用兩個函數就能完成,以下:

(1)jclass clazz = (*env)->FindClass(env, className);
         env指向一個JNIEnv結構體,它很是重要,後面咱們會討論。classname爲對應的Java類名,因爲JNINativeMethod中使用的函數名並不是全路徑名,因此要指明是哪一個類。
(2)(*env)->RegisterNatives(env, clazz, gMethods, numMethods);
         調用JNIEnv的RegisterNatives函數,註冊關聯關係。

那麼,你如今知道了若是動態註冊了,可是有個問題,這些動態註冊的函數在何時和什麼地方被調用?

當Java層經過System.loadLibrary加載完JNI動態庫後,緊接着就會去查找該庫中一個叫JNI_OnLoad的函數。若是有,就調用它,而動態註冊的工做就是在這裏完成的。

JNI_OnLoad

動態庫是libmedia_jni.so,那麼JNI_OnLoad函數在哪裏實現的?若是你看的比較自信的話,我相信以前代碼中有段註釋你應該注意到了。

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp                    // 看這裏!看這裏!看這裏! 
int register_android_media_MediaScanner(JNIEnv *env)              // 這個代碼很熟悉吧?
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

因爲多媒體系統不少地方都使用了JNI,因此JNI_OnLoad被放到了android_media_MediaPlayer.cpp中,咱們看下代碼:

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    // 該函數的第一個參數類型爲JavaVM,這但是虛擬機在JNI層的表明哦,每一個Java進程只有一個這樣的JavaVM
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        goto bail;
    }
    ... ...

    if (register_android_media_MediaScanner(env) < 0) {
        ALOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }
    ... ...

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

數據類型轉換

在Java中調用native函數傳遞的參數是Java數據類型,那麼這些參數類型傳遞到JNI層會變成什麼類型?

Java數據類型分爲「基本數據類型」「引用數據類型」兩種,JNI層也是區別對待二者的。

基本數據類型的轉換

Java基本類型 Native類型 符號屬性 字長
boolean jboolean 無符號 8位
byte jbyte 無符號 8位
char jchar 無符號 16位
short jshort 有符號 16位
int jint 有符號 32位
long jlong 有符號 64位
float jfloat 有符號 32位
double jdoublt 有符號 64位

引用數據類型的轉換

Java引用類型 Native類型 Java引用類型 Native類型
All objects jobject char[] jcharArray
java.lang.Class 實例 jclass short[] jshortArray
java.lang.String 實例 jstring int[] jintArray
Object[] jobjectArray long[] jlongArray
boolean[] jbooleanArray float jfloatArray
byte[] jbyteArray double[] jdoubleArray
java.lang.Throwable 實例 jthrowable

咱們舉例說明,看下processFile函數:

private native void processFile                   (                           String  path,  String  mimeType,  MediaScannerClient client);
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path,  jstring mimeType,  jobject            client)

咱們發現:

      ✨ Java的String類型在JNI層對應爲jstring類型;
      ✨ Java的MediaScannerClient類型在JNI層對應爲jobject。

不知道你有沒有注意到一個問題,Java中的processFile中只有三個參數,爲何到了JNI層對應的函數卻有五個參數?

static void android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)

接下來咱們開始重點討論JNIEnv!!!

JNIEnv

JNIEnv的概念
      是一個與線程相關的表明JNI環境的結構體,該結構體表明瞭Java在本線程的執行環境。

JNIEnv的做用
      ✨ 調用 Java 函數 : JNIEnv 表明 Java 執行環境, 可以使用 JNIEnv 調用 Java 中的代碼
      ✨ 操做 Java 對象 : Java 對象傳入 JNI 層就是 Jobject 對象, 需要使用 JNIEnv 來操做這個 Java 對象

咱們來看一個有趣的現象

前面,咱們已經知道 JNIEnv 是一個與線程相關的變量,若是此時線程 A 有一個 JNIEnv 變量, 線程 B 也有一個JNIEnv變量,因爲線程相關,因此 A 線程不能使用 B 線程的 JNIEnv 結構體變量。
此時,一個java對象經過JNI調用動態庫中的一個send()函數向服務器發送消息,不等服務器消息到來就當即返回,同時把JNI接口的指針JNIEnv *env(虛擬機環境指針),和jobject obj保存在動態庫中的變量裏。一段時間後,動態庫中的消息接收線程接收到服務器發來的消息,並試圖經過保存過的env和obj來調用先前的java對象的方法(至關於JAVA回調方法)來處理此消息,此時程序忽然退出(崩潰)

爲何?

緣由:前臺JAVA線程發送消息,後臺線程處理消息,歸屬於兩個不一樣的線程,不能使用相同的JNIEnv變量。

怎麼解決?

還記得咱們前面介紹的JNI_OnLoad函數嗎?它的第一個參數是JavaVM,它是虛擬機在JNI層的表明!!!

// 全進程只有一個JavaVM對象,因此能夠在任何地方使用
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)

那麼也就是說,不論進程有多少線程(不論有多少JNIEnv),JavaVM倒是獨此一份!因此,咱們能夠利用一個機制:利用全局的 JavaVM 指針獲得當前線程的 JNIEnv 指針。

JavaVM和JNIEnv

      ✨ 調用JavaVM的AttachCurrentThread函數,就可獲得這個線程的JNIEnv結構體,這樣就能夠在後臺線程中回調Java函數了。
      ✨ 另外,在後臺線程退出前,須要調用JavaVM的DetachCurrentThread函數來釋放對應的資源。

經過JNIEnv操做jobject

前面介紹數據類型的時候,咱們知道Java的引用類型除了少數幾個外(Class、String和Throwable),最終在JNI層都會用jobject來表示對象的數據類型,那麼該如何操做這個jobject呢?

咱們先回顧下Java對象是由什麼組成的?固然是它的成員變量和成員函數了!那麼同理,操做jobject的本質就應當是操做這些對象的成員變量和成員函數!那麼jobject的成員變量和成員函數又是什麼?

取出jfieldID和jmethodID

在java中,咱們知道成員變量和成員函數都是由類定義的,他們是類的屬性,那麼在JNI規則中,也是這麼來定義的,用jfieldID定義Java類的成員變量,用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爲這個函數和變量的簽名信息(後面會說到)。

咱們來看看在MediaScanner中如何使用它們,直接看代碼:android_media_MediaScanner.cpp::MyMediaScannerClient構造函數

class MyMediaScannerClient : public MediaScannerClient
{
public:
    MyMediaScannerClient(JNIEnv *env, jobject client)... ...
    {
        // 先找到android.media.MediaScannerClient類在JNI層中對應的jclass實例
        jclass mediaScannerClientInterface =
                env->FindClass(kClassMediaScannerClient);

        if (mediaScannerClientInterface == NULL) {
            ALOGE("Class %s not found", kClassMediaScannerClient);
        } else {
            // 取出MediaScannerClient類中函數scanFile的jMethodID
            mScanFileMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "scanFile",
                                    "(Ljava/lang/String;JJZZ)V");

            // 取出MediaScannerClient類中函數handleStringTag的jMethodID
            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");

            ... ...
        }
    }
    ... ...
    jobject mClient;
    jmethodID mScanFileMethodID;
    jmethodID mHandleStringTagMethodID;
    ... ...
}

從上面的代碼中,將scanFile和handleStringTag函數的jMethodID保存在MyMediaScannerClient的成員變量中。爲何這裏要把它們保存起來呢?這個問題涉及到一個關於程序運行效率的知識點:

若是每次操做jobject前都要去查詢jmethodID或jfieldID,那麼將會影響程序運行的效率,因此咱們在初始化的時候能夠取出這些ID並保存起來以供後續使用。

使用jfieldID和jmethodID

咱們來看看android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile函數

virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }

        /*
         * 調用JNIEnv的CallVoidMethod函數
         * 注意CallVoidMethod的參數:
         *(1)第一個參數是表明MediaScannerClient的jobject對象
         *(2)第二個參數是函數scanFile的jmethodID,後面是Java中的scanFile的參數
         */
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);

        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }

經過JNIEnv輸出CallVoidMethod,再把jobject、jMethodID和對應的參數傳進去,JNI層就可以調用Java對象的函數了!

實際上JNIEnv輸出了一系列相似CallVoidMethod的函數,形式以下:

NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...)

其中type對應java函數的返回值類型,例如CallIntMethod、CallVoidMethod等。若是想調用Java中的static函數,則用JNIEnv輸出的CallStatic<Type>Method系列函數。

因此,咱們能夠看出,雖然jobject是透明的,但有了JNIEnv的幫助,仍是能輕鬆操做jobject背後的實際對象的。

jstring

這一節咱們單獨聊聊String。Java中的String也是引用類型,不過因爲它的使用頻率很高,因此在JNI規範中單首創建了一個jstring類型來表示Java中的String類型。
雖然jstring是一種獨立的數據累心,可是它並無提供成員函數以便操做。而C++中的string類是由本身的成員函數的。那麼該如何操做jstring呢?仍是得依靠JNIEnv提供幫助。

先看幾個有關jstring的函數:

      ✨ 調用JNIEnv的NewString(const jchar* unicodeChars, jsize len),能夠從Native的字符串獲得一個jstring對象。
      ✨ 調用JNIEnv的NewStringUTF(const char* bytes)將根據Native的一個UTF-8字符串獲得一個jstring對象。
      ✨ 上面兩個函數將本地字符串轉換成了Java的String對象,JNIEnv還提供了GetStringChars函數和GetStringUTFChars函數,它們能夠將Java String對象轉換成本地字符串。其中GetStringChars獲得一個Unicode字符串,而GetStringUTFChars獲得一個UTF-8字符串。
      ✨ 另外,若是在代碼中調用了上面幾個函數,在作完相關工做後,就都須要調用ReleaseStringChars函數或ReleaseStringUTFChars函數來對應地釋放資源,否定會致使JVM內存泄漏。

咱們看段代碼加深印象:

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    ... ...

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }
    ... ...
}

JNI類型簽名

咱們看下動態註冊中的一段代碼:

static const JNINativeMethod gMethods[] = {
    ... ...

    {
        "processFile",
        // processFile的簽名信息,這麼長的字符串,是什麼意思?
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processFile
    },
    ... ...
};

上面這段代碼咱們以前早就見過了,不過"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V"是什麼意思呢?

咱們前面提到過,這個是Java中對應函數的簽名信息,由參數類型和返回值類型共同組成,有人可能有疑問,這東西是幹嗎的?

咱們都知道,Java支持函數重載,也就是說,能夠定義同名但不一樣參數的函數。但僅僅根據函數名是無法找到具體函數的。爲了解決這個問題,JNI技術中就將參數類型和返回值類型的組合做爲一個函數的簽名信息,有了簽名信息和函數名,就能很順利地找到Java中的函數了。

JNI規範定義的函數簽名信息看起來很彆扭,不過習慣就行了。它的格式是:

(參數 1 類型標識參數 2 類型標識 ... 參數 n 類型標識) 返回值類型標識

咱們仍然拿processFile的例子來看下:

{
        "processFile",
        // Java中的函數定義爲 private native void processFile(String path, String mimeType, MediaScannerClient client);
        // 對應的JNI函數簽名以下:
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        // void類型對應的標示是V
        // 當參數的類型是引用類型時,其格式是「L包名」,其中包名中的「.」換成「/」,Ljava/lang/String表示是一個Java String類型
        (void *)android_media_MediaScanner_processFile
    },

【注意】:引用類型(除基本類型的數組外)的標識最後都有一個「;」。

函數簽名不只看起來麻煩,寫起來更麻煩,稍微寫錯一個標點都會致使註冊失敗,因此在具體編碼時,能夠定義字符串宏(這邊就很少作解釋了,能夠自行查詢瞭解便可)。

雖然函數簽名信息很容易寫錯,可是Java提供了一個叫javap的工具可以幫助咱們生成函數或變量的簽名信息,它的用法以下:

javap -s -p xxx

其中 xxx 爲編譯後的class文件,s表示輸出內部數據類型的簽名信息,p表示打印全部函數和成員的簽名信息,默認只會打印public成員和函數的簽名信息。

垃圾回收及異常處理

這部分我打算單獨放在一篇博文中探討,結果具體錯誤進行分析。

參考Blog

                  Java Native Interface (JNI)

相關文章
相關標籤/搜索