深入理解Android-JNI的理解

理解JNI需要理解以下問題:

  • JNI的認識
  • JNI庫的加載、相關native函數分析和總結【藉助於MediaScanner】
  • JNI函數註冊
  • JNIEnv的認識

JNI的認識

JNI是Java Native Interface的縮寫,它提供了若干的API實現了Java和其他語言的通信(主要是C&C++)。在安卓中,主要做到以下兩點:

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

  • Native函數也可以反過來調用Java層函數

Java平臺中,爲什麼需要創建一個與Native相關的JNI技術呢?是不是破壞了Java的平臺無關特性,其實主要考慮到如下方面:

  • Java世界的虛擬機使用Native語音寫的,虛擬機又運行在具體的平臺上,所有虛擬機是無法做到平臺無關的。但是,JNI技術可以針對Java層屏蔽不同操作系統之間的差異,這樣就能夠實現平臺無關特性。
  • C/C++語音已經有了很多成熟的模塊,Java只需要直接調用即可。還有一些追求效率和速度的場合,需要Native語音參與的。
    在Android平臺上,JNI就是一座將Native世界和Java世界的天塹變通途的橋。
    這裏寫圖片描述

本文從源碼中的MediaScanner中來初步窺視JNI用法
這裏寫圖片描述
上圖可以看到Java層 、JNI層、Native層之間的架構,JNI是中間層【這裏區分了libmedia_jni.so、libmedia.so:平常開發直接用一個.so 文件。這裏區分爲了說明JNI層和Native層】

  • Java世界對於的是MediaScanner,他內部的一些函數是需要由Native層來實現的。
  • JNI層對於的是libmedia_jni.so 。Android平臺基本上都是採用“lib模塊名_jni.so”的命名方式。
  • Native層對應的是libmedia.so 這個庫完成了實際的功能。
  • MediaScanner將通過JNI層的libmedia_jni.so和Native層的libmedia.so交互

源碼位置:android\frameworks\base\media\java\android\media\MediaScanner.java


public class MediaScanner
{
//1、靜態代碼塊
    static {
        System.loadLibrary("media_jni");      //導入庫
        native_init();                        //初始化操作,是一個native方法
    }

   //2、普通方法【非native方法】
   @Override
        public void scanFile(String path, long lastModified, long fileSize,
                boolean isDirectory, boolean noMedia) {
            // This is the callback funtion from native codes.
            // Log.v(TAG, "scanFile: "+path);
            doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
        }
//3、native 方法【它的具體實現實在JNI層完成的】
    private native void processDirectory(String path, MediaScannerClient client);
    private native void processFile(String path, String mimeType, MediaScannerClient client);
    public native void setLocale(String locale);

    public native byte[] extractAlbumArt(FileDescriptor fd);

    private static native final void native_init();
    private native final void native_setup();
    private native final void native_finalize();

}

上面可以看到三點:1)加載JNI庫 2)初始化Native層 3)聲明native方法,具體實現再JNI層實現,應用層只需要聲明native方法,程序中直接使用

JNI庫的加載

加載jni庫其實很簡單,如上面MediaScanner源碼中,靜態代碼塊中直接調用System.loadLibrary方法就可以了。其參數就是動態庫的名字,即media_jni.系統會根據不同的平臺拓展成動態庫文件,Linux系統會拓展成libmedia_jni.so,而在Windows平臺會拓展成media_jni.dll

通過MediaScanner.java 的native函數刨根問底,追一下native_init() 和 processFile(String path, String mimeType, MediaScannerClient client)

源碼位置:android\frameworks\base\media\jni\android_media_MediaScanner.cpp


// This function gets a field ID, which in turn causes class initialization.
// It is called from a static block in MediaScanner, which won't run until the
// first time an instance of this class is used.
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    jclass clazz = env->FindClass(kClassMediaScanner);
    fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
    if (fields.context == NULL) {
        return;
    }
}
//native 方法processFile的實現
static void android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
   ......
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }
    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
......
    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

有興趣好好看一下源碼,肯定會有很多疑問,比如:JNIEnv 、FindClass、jclass、ReleaseStringUTFChars 等,都是什麼,都在執行什麼任務。

不要急,且看如下內容!

JNI函數註冊

所謂JNI函數註冊就是將JNI層的native層的native函數和JNI層對於的實現函數關聯起來,有了這種關聯,在調用Java層測native函數時候,就能夠順利到JNI層對應的函數執行了。JNI函數註冊有兩種,我們主要分析第二種。

  • 靜態註冊

    靜態註冊就是直接在Java文件裏寫個native方法 然後再c/c++文件中實現這個方法就行了!流程如下:
    1)編寫 java 代碼;
    2)利用 javah 指令生成對應的 .h 文件;
    3)對 .h 中的聲明進行實現;
    比如如下實現:

JNIEXPORT jstring JNICALL
Java_com_example_efan_jni_1learn2_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

那麼有哪些弊端呢:
1)編寫不方便,JNI 方法名字必須遵循規則且名字很長;
2)編寫過程步驟多,不方便;
3)程序運行效率低,因爲初次調用native函數時需要根據根據函數名在JNI層中搜索對應的本地函數,然後建立對應關係,這個過程比較耗時

  • 動態註冊
    原理:
    利用 RegisterNatives 方法來註冊 java 方法與 JNI 函數的一一對應關係
    實現流程:
    1) 利用結構體 JNINativeMethod 數組記錄 java 方法與 JNI 函數的對應關係;
    2)實現 JNI_OnLoad 方法,在加載動態庫後,執行動態註冊;
    3)調用 FindClass 方法,獲取 java 對象;
    4)調用 RegisterNatives 方法,傳入 java 對象,以及 JNINativeMethod 數組,以及註冊數目完成註冊;
    優點:
    1)流程更加清晰可控;
    2)效率更高;
    靜態改動態註冊,只需要改JNI層代碼:
jstring stringFromJNI(JNIEnv *env, jobject thiz){
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


static const JNINativeMethod gMethods[] = {
        {"stringFromJNI", "()Ljava/lang/String;", (jstring*)stringFromJNI}
};


JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){

    __android_log_print(ANDROID_LOG_INFO, "native", "Jni_OnLoad");
    JNIEnv* env = NULL;
    if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) //從JavaVM獲取JNIEnv,一般使用1.4的版本
        return -1;
    jclass clazz = env->FindClass("com/example/efan/jni_learn2/MainActivity");
    if (!clazz){
        __android_log_print(ANDROID_LOG_INFO, "native", "cannot get class: com/example/efan/jni_learn2/MainActivity");
        return -1;
    }
    if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0])))
    {
        __android_log_print(ANDROID_LOG_INFO, "native", "register native method failed!\n");
        return -1;
    }
    return JNI_VERSION_1_4;
}

如上是一個簡易的動態註冊流程。
Java native函數和JNI函數式一一對應的,其實在在動態註冊中直接用一個數據結構來保存這種關聯關係的,用一個JNINativeMethod 的結構。

typedef struct {
const char* name;         //name是Java中函數的名字
const char* signature;    //signature,用字符串是描述了函數的參數和返回值 
void* fnPtr;              //fnPtr是函數指針,指向C函數。
} JNINativeMethod;

咋們再分析下MediaScanner
源碼位置:master\android\frameworks\base\media\jni\android_media_MediaScanner.cpp


static JNINativeMethod gMethods[] = {
    {
        "processDirectory",
        "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processDirectory
    },

    {
        "processFile",
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void *)android_media_MediaScanner_processFile
    },

    {
        "setLocale",
        "(Ljava/lang/String;)V",
        (void *)android_media_MediaScanner_setLocale
    },

    {
        "extractAlbumArt",
        "(Ljava/io/FileDescriptor;)[B",
        (void *)android_media_MediaScanner_extractAlbumArt
    },

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

    {
        "native_setup",
        "()V",
        (void *)android_media_MediaScanner_native_setup
    },

    {
        "native_finalize",
        "()V",
        (void *)android_media_MediaScanner_native_finalize
    },
};

其中:processDirectory 、 processFile 、 setLocale 、 extractAlbumArt 、 native_init 、native_setup 、 native_finalize 方法不就是Java中native的放方法名麼。

AndroidRunTime 類提供了一個registerNativeMethods函數完成註冊工作的,如下源碼:
\master\android\frameworks\base\core\jni\AndroidRuntime.cpp


/*
 * Register native methods using JNI.
 */
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);  //就是這個方法
}

其中的jniRegisterNativeMethods是Android平臺中爲了方便JNI使用而提供的一個幫助函數。【RndroidRunTime源碼可以看一看,有很多看了就非常熟悉的內容】

\master\android\libnativehelper\JNIHelp.cpp


extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    scoped_local_ref<jclass> c(env, findClass(env, className));
   ---
   //下面的RegisterNatives 纔是重點方法:真正實現註冊的地方
   //調用JNIEnv 的RegiisterNatives 函數,註冊關聯關係
   //e 是env指針   c.get()得到jclass  gMethods是native的函數數組  
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
      ---
    }
    return 0;
}

上面就完成了動態註冊的流程, 還有一個問題需要考慮:這些動態註冊的函數,什麼時候和什麼地方調用呢?
當Java層通過System.loadLibrary加載JNI動態庫後,緊接着會查找該庫中的一個叫JNI_OnLoad的函數,如果有就調用它,而動態註冊的工作就是在這裏完成的。

所以,如果想使用動態註冊的方法,必須實現JNI_OnLoad函數,只有在這個函數中才有機會去完成動態註冊的工作,有一些初始化工作可以在這裏做的。
對於libmediia_jni.so的JNI_OnLoad函數實在哪裏實現?多媒體系統很多地方都用了JNI,所以源碼裏面放在了android_media_MediaPlayer.cpp中。
\master\android\frameworks\base\media\jni\android_media_MediaPlayer.cpp



jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
//第一個參數類型是JavaVM,這可是虛擬機在JNI層的代表,每個進程中只有一個這樣的JavaVM
    JNIEnv* env = NULL;
    jint result = -1;
    assert(env != NULL);
    //動態註冊MediaScanner 的JNII函數
    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層通過調用System.loadLibrary加載完JNI動態庫後,緊接着會查找該庫中一個叫JNI_OnLoad的函數。如果有,就調用他,而動態註冊的工作就是在這裏完成的。
所以,如果想使用動態註冊方法,必須實現JNI_OnLoad函數,只有這個函數纔有機會完成動態註冊的工作,靜態註冊則沒有這個要求。

JNIEnv的認識

你會發現在JNI世界裏,你是離不開JNIEnv,它就是一個與線程相關的代表JNI環境的結構體
這裏寫圖片描述

上圖可以看到:JNIEnv提供了一些JNI系統函數,通過這些函數可以

  • 調用Java函數【jni層調用java層】
  • 操作jobject對象等很多事情【jni層調用native】

前面提到過JNI_OnLoad函數,第一個參數是JavaVM,它是虛擬機在JNI層的代表。

JNI_OnLoad(JavaVM* vm, void* reserved)

不論檢查中多少個線程,JavaVM獨此一份,在任意地方都可以使用它。
JavaVM和JNIEnv關係:

  • JavaVM的AttachCurrentThread函數可以得到這個線程的JNIEnv結構體,這樣後臺就可以毀掉Java函數
  • 後臺線程推出前,需要調用JvavVM的DetachCurrentThread函數來釋放對應的資源

總結:看看JNI底層實現,分析源碼,初步瞭解JNI。具體實踐還需要了解JNI層是怎麼操作Java層函數和調用JNI層函數的。

參考文章

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