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

Android: JNI

編輯:關於Android編程

一、分析的文件路徑

./frameworks/base/media/java/android/media/MediaScanner.java
./frameworks/base/media/jni/android_media_MediaScanner.cpp
./frameworks/base/media/jni/android_media_MediaPlayer.cpp
./frameworks/base/media/jni/AndroidRuntime.cpp
./libnativehelper/JNIHelp.cpp

二、代碼分析

1. java層

// frameworks/base/media/java/android/media/MediaScanner.java
public class MediaScanner 
{
    static {//class被加載的時候自動掉用static裡邊的函數,,java的基礎,,
        /*加載對應的JNI庫*/
        System.loadLibrary("media_jni");
        //這裡負責加載JNI模塊編譯出來的庫。在實際加載動態庫時會將其拓展為libmedia_jni.so。

        native_init(); //調用native_init()函數,這個函數是對應的cpp文件裡邊的函數
    } 
    ........
    //聲明一個native函數,native為java關鍵字,表示它將由JNI層完成
    private static native final void native_init();
    private native final void native_setup();
      ........
}

注:native_init函數位於android.media這個包中,其全路徑名稱為android.media.MediaScanner.nantive_init。
根據規則其對應的JNI層函數名稱為:android_media_MediaScanner_native_init。

2. JNI層

//frameworks/base/media/jni/android_media_MediaScanner.cpp
//以下是frameworks/base/media/jni/Android.mk的編譯腳本
/*
LOCAL_SRC_FILES:= \
        ...\
        android_media_MediaScanner.cpp \
        ...\

LOCAL_MODULE:= libmedia_jni //編譯生成庫的名字為libmedia_jni.so
include $(BUILD_SHARED_LIBRARY)
*/

static const char* const kClassMediaScanner = //MediaScanner.java的路徑!!
        "android/media/MediaScanner";
     ........

/*native_init函數的JNI層實現*/
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    //FindClass根據路徑尋找java class!
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

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

2.1 JNI注冊方法

靜態注冊

大體的流程如下:
1. 先編譯java代碼,然後編譯生成.class文件
2. 使用java的工具程序javah,如javah -o output packagename.classname,這樣它會生成一個叫output.h的JNI層頭文件。這裡packagename.classname就是上面編譯生成的class文件,而在這裡生成的output.h文件裡則聲明了對應的JNI層函數,只要實現裡邊的函數即可。

靜態注冊中,java函數是怎麼找到對應的jni函數的?其實就是用名字找到的。比如在java中調用native_init函數的時候,它就會在JNI庫中尋找android_media_MediaScanner_native_init函數,如果沒有就會報錯。如果找到,則會為這兩個函數建立連接,其實就是保存JNI層函數的函數指針。以後再調用native_init函數時,直接使用這個函數指針就可以了,當然這項工作是虛擬機完成的。

動態注冊

上面說過java native函數和JNI函數是一一對應的,所以動態注冊方式就采用JNINativeMethod的結構體來記錄這種關系。JNINativeMethod都是定義在各自的JNI層文件中。

typedef struct {
    const char* name; //保存JNI對應的java函數的名字,比如"native_init",不用加路徑
    const char* signature;//保存java函數的簽名信息,用字符串表示,是參數類型和返回值類型的組合
    void* fnPtr;//JNI層對應函數的函數指針,注意它是void *類型
} JNINativeMethod;

/*比如android_media_MediaScanner.cpp文件中,定義了如下一個JNINativeMethod數組*/
static JNINativeMethod gMethods[] = {
{
    "processDirectory",//java中native函數的函數名
    //processFile的簽名信息
    "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
    (void *)android_media_MediaScanner_processDirectory//JNI層對應的函數指針
},
.......
{
    "native_init",//java中native函數的函數名
    "()V",//native_init函數的簽名信息
    (void *)android_media_MediaScanner_native_init /*JNI層native_init函數的函數指針*/ 
},
{
    "native_setup",
    "()V",
    (void *)android_media_MediaScanner_native_setup
},
.......
};

/*注冊JNINativeMethod數組*/
int register_android_media_MediaScanner(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

接著調用AndroidRuntime中的registerNativeMethods:

//AndroidRuntime.cpp
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{ 
    return jniRegisterNativeMethods(env, className, gMethods, numMethods); 
}

//JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, 
    const JNINativeMethod* gMethods, int numMethods)
{ 
    JNIEnv* e = reinterpret_cast(env);

    ALOGV("Registering %s natives", className);

    scoped_local_ref c(env, findClass(env, className));
    if (c.get() == NULL) {
        ALOGE("Native registration unable to find class '%s', aborting", className);
        abort();
    }

    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        ALOGE("RegisterNatives failed for '%s', aborting", className);
        abort();
    }

    return 0;
}

上面這些說了JNI層怎麼定義的注冊函數等等,那上面的register_android_media_MediaScanner()這種注冊函數是在什麼時間被調用來完成注冊的呢??
當Java層通過System.loadLibrary加載完JNI動態庫後,緊接著會查找一個JNI_OnLoad的函數。如果有就調用它,動態注冊的工作在這裡完成。

//android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    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;
}

2.2 數據類型轉換

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 jdouble 有符號 64位

Java的基本類型和Native層的基本類型轉換非常簡單,不過必須注意轉換成Native類型後對應數據類型的字長,例如jchar在Native語言中是16位,占兩個字節,這和普通的char占一個自己的情況是不一樣的。

下面是Java引用數據類型和Native類型的轉換表

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

2.3 JNIEnv介紹

JNIEnv用來操作java類的對象,比如讀取修改java類的類成員變量,或者直接調用java類的成員函數。
JNIEnv變量,在每個JNI層函數中都是以第一個參數傳入。JNIEnv變量可以看到在JNI_Onload()函數中,由Java VM相關函數GetEnv函數取出

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */){
    JNIEnv* env = NULL;
    ...
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ...
    }
    ...
}

2.3.1 通過JNIEnv操作jobject

jobject即java對象在JNI中的表示,通過JNIEnv可以操作jobject以達到讀取修改java類的成員變量和調用java函數的目的。

jfieldID和jmethodID介紹
jfieldID和jmethodID分別代表jobject中成員變量以及成員函數,可以從JNIEnv對應的函數中得到jfieldID
和jmethodID
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

獲取jmethodID和使用方法

class MyMediaScannerClient : public MediaScannerClient
{
public:
    MyMediaScannerClient(JNIEnv *env, jobject client) //構造函數中設置mClient等
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
            mScanFileMethodID(0),
            mHandleStringTagMethodID(0),
            mSetMimeTypeMethodID(0)
    {
        ...
        mSetMimeTypeMethodID = env->GetMethodID(
            mediaScannerClientInterface,
                "setMimeType",
                "(Ljava/lang/String;)V");
        ...
    }
}
virtual status_t setMimeType(const char* mimeType)
{
    jstring mimeTypeStr;
    ...
    //mClient就是構造函數中獲取的jobject
    //mSetMimeTypeMethodID就是構造函數中調用GetMethodID獲取到的響應的jmethondID
    mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr);
    ...
}

獲取jfieldID和使用的例子

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ...
    jclass clazz = env->FindClass(kClassMediaScanner);
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    ....
}

使用下面的函數修改或者讀取jfieldID對應的成員變量,這裡注意變量的類型!!!

//修改對應的變量
static void setNativeScanner_l(JNIEnv* env, jobject thiz, MediaScanner *s)
{
    env->SetLongField(thiz, fields.context, (jlong)s);
}

//讀取對應的變量
static MediaScanner *getNativeScanner_l(JNIEnv* env, jobject thiz)
{
    return (MediaScanner *) env->GetLongField(thiz, fields.context);
}

2.3.2 JNI類型簽名介紹

2.3.3 垃圾回收

Java中創建的對象最後是由垃圾回收器來回收和釋放內存的,可它對JNI有什麼影響呢?下面看一個例子

static jobject save_thiz = NULL;//定義一個全局的jobject
static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{ 
    ...
    //保存Java層傳入的jobject對象,代表MediaScanner對象
    save_thiz = thiz;
    ...
    return;
}
//假設在某個時間,有地方調用callMediaScanner函數
void callMediaScanner(){
    //在這個函數中操作save_thiz會有什麼問題?
}

上面的做法肯定會有問題,因為和save_thiz對應的Java層中的MediaScanner很有可能已經被垃圾回收了,也就是說,save_thiz保存的這個jobject可能是一個野指針,如果使用它,後果會很嚴重。
可能有人會問,對一個引用類型執行賦值操作,它的引用計數不會增加嗎? 而垃圾回收機制只會保證那些沒有被引用的對象才會被清理。問得對,但如果在JNI層使用下面這樣的語句,是不會增加引用計數的

save_thiz = thiz;//這種賦值不會增加jobject的引用計數
Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是Local Reference,它包含函數調用時傳入的jobject和在JNI層函數中創建的jobject。Local Reference最大的特點就是,一旦JNI層函數返回,這些jobject就可能被垃圾回收 Global Reference:全局引用,這種對象如果不主動釋放,它永遠不會被垃圾回收

Weak Global Reference:弱全局引用,一種特殊的Global Reference,在運行過程中可能會被垃圾回收。所以在使用它之前,需要調用JNIEnv的IsSameObject判斷它是否被回收了

平時用得最多的是Local Reference和Global Reference,下面來看一個例子,代碼如下:
“`c
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
//調用NewGlobalRef創建一個Global Reference,這樣mClient就不用擔心被回收了
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{

}

//析構函數
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient); //DeleteGlobalRef函數釋放這個全局引用
}

像上面這樣,每當JNI層想要保存Java層中的某個對象時,就可以使用Global Reference,使用完後記住釋放它就可以了。

下面來看一下Local Reference。
```c
    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        jstring pathStr;
        //調用NewStringUTF創建一個jstring對象,它是Local Reference類型
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);

        //調用DeleteLocalRef釋放Local Reference,按照前面的說法,在return之後,
        //pathStr就會被釋放,所以這裡釋放Local Reference好像是多余的,但有區別
        //1)如果不調用DeleteLocalRef,pathStr將在函數返回後被釋放
        //2)調用DeleteLocalRef,pathStr將立即被釋放
        //由於垃圾回收時間不定而且如果在頻繁調用NewStringUTF的時候,
        //還是需要馬上釋放Local Reference,像下面這樣不馬上釋放的話內存會馬上被耗光
        for(int i=0;i<100;i++){
            jstring pathStr = mEnv->NewStringUTF(path);
            //mEnv->DeleteLocalRef(pathStr);
        }

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





2.3.4 JNI中的異常處理

JNI中也有一場,如果調用JNIEnv的某些函數出錯了,則會產生一個異常,但這個異常不會中斷本地函數的執行,直到從JNI層返回到Java層後,虛擬機才會拋出這個異常。雖然在JNI層中產生的異常不會中斷本地函數的運行,但一旦產生異常後,就只能做一些資源清理工作了(例如釋放全局引用,或者ReleaseStringChars)。如果這時調用除上面提到的函數之外的其他JNIEnv函數,則會導致程序死掉。
來看一個和異常處理有關的例子,代碼如下所示:

    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();//清理當前JNI層中發生的異常
            return NO_MEMORY;
        }
    }

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

ExceptionOccured函數,用來判斷是否發生異常 ExceptionClear函數,用來清理當前JNI層中發生的異常 ThrowNew函數,用來向Java層拋出異常
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved