Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入理解JNI

深入理解JNI

編輯:關於Android編程

最近在學習android底層的一些東西,看了一些大神的博客,整體上有了一點把握,也產生了很多疑惑,於是再次把鄧大神的深入系列翻出來仔細看看,下面主要是一些閱讀筆記。

JNI概述

JNI是Java Native Interface的縮寫 ,通常稱為“Java本地調用”,通過這種技術可以做到:

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

Native程序中的函數可以調用Java層的函數,也就是說C/C++程序可以調用Java函數。

通過JNI可以將底層Native世界和java世界聯系起來

學習JNI實例:MediaScanner

\

Java層對應的是MediaScanner,這個類有一些函數需要由Native層來實餡喎?/kf/yidong/wp/" target="_blank" class="keylink">WPC9wPg0KCTxwPkpOSbLjttTS+2xpYm1lZGlhX2puaS5zb6Os0ruw47LJ08M8Y29kZT5saWLEo7/pw/tfam5pLnNvPC9jb2RlPrXEw/zD+7e9yr08L3A+DQoJPHA+TmF0aXZlsuO21NOmtcTKx2xpYm1lZGlhLnNvo6zV4rj2v+LN6rPJwcvKtbzKtcS5psTcPC9wPg0KPC9ibG9ja3F1b3RlPg0KPGgzIGlkPQ=="1調用native函數">1、調用native函數

Java調用native函數,就需要通過一個位於JNI層的動態庫來實現,這個通常是在類的static語句中加載,調用System.loadLibrary方法,該方法的參數是動態庫的名稱,在這裡為media_jni(系統會根據不同平台擴展成真實的動態庫文件名,如在linux中libmedia_jni.so,而在windows平台則會擴展為media_jin.dll)
[MediaScanner.java]

`static {
    //加載對應的JNI庫media_jni是JNI庫的名稱。實際動態加載時將其擴展成為libmedia_jni.so
    //在windows平台則擴展成為media_jni.dll
    System.loadLibrary("media_jni");
    native_init();//調用native_init函數
    ……
    //申明一個native函數,表示它由JNI層完成
    private native void processFile(String path, String mimeType, MediaScannerClient client);
    ……
    private static native final void native_init();
}`

2、Java層和JNI層函數關聯

即java層的native_init和processFile[MediaScanner.java如上]函數對應的是JNI層的android_media_MediaScanner_native_init和android_media_MediaScanner_processFile[android_media_MediaScanner.cpp如下]函數呢?

`
//native_init的JNI層實現
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
ALOGV("native_init");
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)
{
ALOGV("processFile");

// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
     ……
//調用JNIEnv的GetStringUTFChars得到本地字符串pathStr
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) {  // Out of memory
    return;
}
const char *mimeTypeStr =
    (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
if (mimeType && mimeTypeStr == NULL) {  // Out of memory
    // ReleaseStringUTFChars can be called with an exception pending.
    //使用完記得釋放資源否則會引起JVM內存洩露
    env->ReleaseStringUTFChars(path, pathStr);
    return;
}
    ……
}   

`

注冊JNI函數

注冊之意就是將Java層的native函數與JNI層對應的實現函數關聯起來,這樣在調用java層的native函數時,就能順利轉到JNI層對應的函數執行。
拿native_init來說,在android.media這個包中,全路徑為andorid.media.MediaScanner.native_init而JNI函數名字是android_media_MediaScanner_native_init,由於在Native語言中符號“.”有著特殊意義需要將java函數名(包括包名)中的“.”換成“_”,這樣java中的native_init找到JNI中的android_media_MediaScanner_native_init

注冊的兩種方式

靜態方式

動態方式

靜態方式

根據函數名來找對應的JNI函數,需要java的工具程序javah參與,流程如下:

先編寫java代碼,然後編譯生成.class文件

使用java的工具程序javah,如javah -o output packagename.classname 這樣就會生成一個叫output的JNI層頭文件(函數名有_轉換後為_l)

在靜態方法中native函數是如何找到的,過程如下:當java層調用native_init函數時,它會從對應的JNI庫中尋找java_android_media_MediaScanner_native_linit函數,如果沒有找到,就會報錯,如果找到就會為native_init和java_android_media_MediaScanner_native_linit建立一個函數指針,以後再調用native時直接使用這個指針即可,這個工作是由虛擬機完成

缺點:每個class都需要使用javah生成一個頭文件,並且生成的名字很長書寫不便;初次調用時需要依據名字搜索對應的JNI層函數來建立關聯關系,會影響運行效率

動態注冊

使用一種數據結構JNINativeMethod來記錄Java native函數和JNI函數的對應關系

`typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;`

[android_media_MediaScanner.cpp]中native_init和processFile的動態注冊

`//動態注冊
//定義一個JNINativeMethod數組,其成員就是MS中所有native函數一一對應關系
static JNINativeMethod gMethods[] = {
    ……
{
    "processFile", //java中native函數的函數名
    //processFile的簽名信息
    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
    (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)
{
return AndroidRuntime::registerNativeMethods(env,
            kClassMediaScanner, gMethods, NELEM(gMethods));
}

`

這裡使用AndroidRunTime類提供的registerNativeMethods將getMethods來完成注冊工作

[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使用的一個幫助函數

[JNIHelp.c]

`
/*
 * Register native JNI-callable methods.
 *
 * "className" looks like "java/lang/String".
 */
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGV("Registering %s natives\n", className);
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        LOGE("Native registration unable to find class '%s'\n", className);
        return -1;
    }
    //實際上是調用了JNIEnv的RegisterNatives函數完成注冊的
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        LOGE("RegisterNatives failed for '%s'\n", className);
        return -1;
    }
    return 0;
}

`

從這裡我們可以清晰看出函數調用關系

`AndroidRuntime::registerNativeMethods
                jniRegisterNativeMethods`

而在jniRegisterNativeMethods中核心步驟只有兩步

通過類名找到類(env指向一個JNIEnv結構體,className為對應Java類名,由於JNINativeMethod中使用的函數名並非全路徑名,這裡要指明具體類)

jclass clazz = (*env)->FindClass(env, className);

調用JNIEnv的RegisterNatives函數完成注冊關聯關系

(*env)->RegisterNatives(env, clazz, gMethods, numMethods)

何時調用該動態注冊函數?

在第一小節調用native函數時首先使用System.loadLibrary來加載動態庫,當加載完成JNI動態庫後,緊接著會查找該庫匯總一個叫JNI_OnLoad的函數,如果有就調用該函數,動態注冊工作就是在這裡完成。因此要實現動態注冊就必須實現JNI_OnLoad函數,只有在這個函數中才有機會完成動態注冊的工作。這裡是放在了android_media_MediaPlayer.cpp中

[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) {
    ALOGE("ERROR: GetEnv failed\n");
    goto bail;
}
assert(env != NULL);
……
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;
}`

ok,至此JNI注冊結束

JNIEnv介紹

在注冊過程中JNIEnv已經多次出現,這裡做下詳細介紹。代表JNI環境的結構體

\

而且JNIEnv是一個線程相關的,也就是說線程A有個JNIEnv,線程B有個JNIEnv。由於線程相關不能在B線程中去訪問線程A的JNIEnv結構體。由於我們無法保存一個線程的JNIEnv結構體,然後放到後台線程中去使用。為了解決這個問題,在
JNI_OnLoad函數中第一個參數是JavaVM對象,它是虛擬機在JNI層的代表

`
//全進程只有一個javavm對象,所以可以保存,並且在任何地方使用都沒有問題
JNI_OnLoad(JavaVM* vm, void*  reserved )`

其中

調用JavaVM的AttachCunrrentThread函數,就可以的得到這個線程的JNIEnv結構體。這樣就可以在後台線程中回調Java函數

在後台線程退出前,需要調用JavaVM的Detach的DetachCurrentThread函數來釋放對應的資源

這樣就是可以方便使用JNIEnv了。

如何使用JNIEnv

在JNI中除了基本類型數組、Class、String和Throwable外其余所有Java對象的數據類型在JNI中都用jobject表示(數據類型下一節會介紹),因此JNIEnv如何操作jobject顯得很重要。

首先要取得這些屬性和方法。操作jobject的本質就是操作這些對象的成員變量和成員函數。在JNI中使用jfieldID和jmethodID來表示Java類的成員變量和成員函數

`jfieldID GetFieldID(jclass clazz,const char *name,const char *sig)
 jmethodID GetMethod(jclass clazz,const char *name,const char *sig)
`

其中jclass表示java類,name表示成員變量/成員函數名稱,sig表示變量/函數的簽名信息,使用如下所示
[android_media_MediaScanner.cpp]

` mScanFileMethodID = env->GetMethodID(
                                mediaScannerClientInterface,
                                "scanFile",
                                "(Ljava/lang/String;JJZZ)V");`

這裡所做就是將這些ID保存以便於後續使用,使得運行效率更高。

獲取這些屬性/方法ID後再看如何使用,如前面已經獲取了mScanFileMethodID,下面是使用

` mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);
`

清清楚楚,使用JNIEnv輸出CallVoidMethod,再把jobject、jMethodID和對應的參數傳入,就可以調用java對象的函數了。這裡是無返回值對象,實際上JNIEnv輸出了一些列類似CallVoidMethod的函數,如CallIntMethod等,實際形式如下

`NativeType CallMethod(JNIEnv *env,jobject obj,jmethodID methodID,……)`

其中type對應java函數返回值,要是調用java中的static函數,則需要使用JNIEnv輸出的CallStaticMethod系列

同理通過jfieldID操作jobject的成員變量

`NativeType GetField(JNIEnv *env,jobject obj,jfieldID fieldID)
 NativeType SetField(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)`

JNI類型和簽名

類型

java數據類型分為基本數據類型和引用數據類型兩種

先看基本數據類型

Java Native JNI層字長 boolean jboolean 8位 byte jbyte 8位 char jchar 16位 short jshort 16位 int jint 32位 long jlong 64位 float jfloat 32位 double jdouble 64位

再看引用類型

Java引用類型 Native類型 All objects jobject java.lang.Class jclass java.lang.String jstring Object[] jobjectArray boolean[] jbooleanArray byte[] jbyteArray java.lang.Throwabe實例 jthrowable

簽名

由於java支持函數重載,因此僅僅根據函數名是無法找到具體函數的,為解決這個問題,JNI技術中就將參數類型和返回值類型組合作為一個函數的簽名,
如在[MedaiScanner.java]processFile函數定義

`  private native void processFile(String path, String mimeType, MediaScannerClient client);`

對應的JNI函數簽名是
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
其中,括號內是參數標識,最右邊是返回值類型的標識,void類型標識是V,當參數類型是引用類型時其格式是”L包名”,包中的點換成/。

類型標識表

類型標識 java類型 Z boolean B byte C char S short I int J long F float D double L/java/lanaugeString String [I int[] [L/java/lang/object Object[]

函數簽名手動寫很容易出錯,java提供了一個javap的工具可以幫助生成函數或變量的簽名信息

垃圾回收

JNI中提供三種類型的引用來解決垃圾回收問題

Local Reference:本地引用,一旦JNI層函數返回,這些jobject就可能被垃圾回收

Global Reference:全局引用,不主動釋放,永遠不會被回收

Weak Global Reference:弱全局引用,在運行過程中可能會被垃圾回收,因此在使用之前,需要調用JNIEnv的isSameObject判斷是否被回收

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved