JNI使用小結

以下內容主要參考深入理解Android卷1.

JNI函數動態註冊:

使用這個數據結構存儲一個JNI函數的註冊信息:

typedef struct {

   //Java中native函數的名字,不用攜帶包的路徑。例如“native_init“。

constchar* name;    

//Java函數的簽名信息,用字符串表示,是參數類型和返回值類型的組合。

    const char* signature;

   void*       fnPtr;  //JNI層對應函數的函數指針,注意它是void*類型。

} JNINativeMethod;

從以上成員看出java中聲明的native函數好像可以對應上該jni文件中的函數,但是實際上jni函數的在這個結構上是唯一的,但是java聲明的不是,因爲不帶包路徑,只是一個函數名,怎麼知道這個函數是哪個類的呢,如果其他java文件也有這樣一個同名的native聲明呢?其實在註冊時,會去獲取對應的java類對象(Class類型的對象)。

爲JNI動態庫中的所有JNI函數建立一個JNINativeMethod數組,註冊就用這個數組作爲參數。示例:

static JNINativeMethod g_methods[] = {
    {"nativeSetBlueLightFilterStrength", "(I)Z", (void*)nativeSetBlueLightFilterStrength},
    {"nativeGetBlueLightFilterStrength", "()I", (void*)nativeGetBlueLightFilterStrength},
    {"nativeEnableBlueLightFilter", "(Z)Z", (void*)nativeEnableBlueLightFilter},
    {"nativeIsBlueLightFilterEnabled", "()Z", (void*)nativeIsBlueLightFilterEnabled},
    {"nativeGetBlueLightFilterStrengthRange", "()I", (void*)nativeGetBlueLightFilterStrengthRange},
    {"nativeBlueLightFilterInit", "()Z", (void*)nativeBlueLightFilterInit},
};

JNI函數簽名:

 

JNI規範定義的函數簽名信息格式:

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

比較繁瑣,具體編碼時,讀者可以定義字符串宏

類型標示示意表

類型標示

Java類型

類型標示

Java類型

Z

boolean

F

float

B

byte

D

double

C

char

L/java/langaugeString;

String

S

short

[I

int[]

I

int

[L/java/lang/object;

Object[]

J

long

 

 

上面列出了一些常用的類型標示,如果Java類型是數組,則標示中會有一個“[”,另外,引用類型(除基本類型的數組外)的標示最後都有一個“;”,返回值簽名時也是要加分號的

 函數簽名小例子

函數簽名

Java函數

“()Ljava/lang/String;”

String f()

“(ILjava/lang/Class;)J”

long f(int i, Class c)

“([B)V”

void f(byte[] bytes)

 

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

 

在JNI_OnLoad函數中註冊會用到一下兩行代碼:

/*

env指向一個JNIEnv結構體,classname爲對應的Java類名(全路徑名,包名+類名),由於

JNINativeMethod中使用的函數名並非全路徑名,所以要指明是哪個類。

*/

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

//調用JNIEnv的RegisterNatives函數,註冊關聯關係。

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

 

示例:

 

/*
 * Register several native methods for one class.
 */
static int registerNativeMethods(JNIEnv* env, const char* className,
    JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    clazz = env->FindClass(className);
    if (clazz == NULL) {
        ALOGE("Native registration unable to find class '%s'", className);
        return JNI_FALSE;
    }
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        ALOGE("RegisterNatives failed for '%s'", className);
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

// ----------------------------------------------------------------------------

/*
 * This is called by the VM when the shared library is first loaded.
 */

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

    UNUSED(reserved);

    ALOGI("JNI_OnLoad");

    if (JNI_OK != vm->GetEnv((void **)&env, JNI_VERSION_1_4)) {
        ALOGE("ERROR: GetEnv failed");
        goto bail;
    }

    if (!registerNativeMethods(env, classPathName, g_methods, sizeof(g_methods) / sizeof(g_methods[0]))) {
        ALOGE("ERROR: registerNatives failed");
        goto bail;
    }
//必須返回這個值,否則會報錯。
    result = JNI_VERSION_1_4;

bail:
    return result;
}

 

JNI層代碼中一般要包含jni.h這個頭文件。Android源碼中提供了一個幫助頭文件JNIHelp.h,它內部其實就包含了jni.h,所以我們在自己的代碼中直接包含這個JNIHelp.h即可。

 

頭文件路徑可以通過在Android.mk中指定:

LOCAL_C_INCLUDES := $(JNI_H_INCLUDE) \
        $(MTK_PATH_SOURCE)/hardware/pq/mt8163/inc

java調用native函數時,可以傳遞任意java類型的對象。在jni函數中,並不能識別具體的對象類型,jni函數通過類似java反射的技術調用java層傳過來的對象的成員和方法。

 

2. 數據類型轉換

通過前面的分析,解決了JNI函數的註冊問題。下面來研究數據類型轉換的問題。

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

Java數據類型分爲基本數據類型和引用數據類型兩種,JNI層也是區別對待這二者的。先來看基本數據類型的轉換。

(1)基本類型的轉換

基本類型的轉換很簡單,可用表2-1表示:

表2-1  基本數據類型轉換關係表

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基本數據類型和JNI層數據類型對應的轉換關係,非常簡單。不過,應務必注意,轉換成Native類型後對應數據類型的字長,例如jchar在Native語言中是16位,佔兩個字節,這和普通的char佔一個字節的情況完全不一樣。上面的基本類型除了注意類型所佔字節長度外,在jni中可以混合使用,不會報錯。如一個JNIEnv中的方法要求jint類型的,可以直接傳一個int類型的,native方法返回類型是jint的,可以直接返回int。

接下來看Java引用數據類型的轉換。

(2)引用數據類型的轉換

引用數據類型的轉換如表2-2所示:

表2-2  Java引用數據類型轉換關係表

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[]

floatArray

byte[]

jbyteArray

double[]

jdoubleArray

java.lang.Throwable實例

jthrowable

 

 

由上表可知:

·  除了Java中基本數據類型的數組、Class、String和Throwable外,其餘所有Java對象的數據類型在JNI中都用jobject表示。

 

JNIEnv是一個和線程相關的,代表JNI環境的結構體。

 

JNIEnv實際上就是提供了一些JNI系統函數。通過這些函數可以做到:

·  調用Java的函數。

·  操作jobject對象等很多事情。

JNIEnv,是一個和線程有關的變量。也就是說,線程A有一個JNIEnv,線程B有一個JNIEnv。由於線程相關,所以不能在線程B中使用線程A的JNIEnv結構體。讀者可能會問,JNIEnv不都是native函數轉換成JNI層函數後由虛擬機傳進來的嗎?使用傳進來的這個JNIEnv總不會錯吧?是的,在這種情況下使用當然不會出錯。不過當後臺線程收到一個網絡消息,而又需要由Native層函數主動回調Java層函數時,JNIEnv是從何而來呢?根據前面的介紹可知,我們不能保存另外一個線程的JNIEnv結構體,然後把它放到後臺線程中來用。這該如何是好?

還記得前面介紹的那個JNI_OnLoad函數嗎?它的第一個參數是JavaVM,它是虛擬機在JNI層的代表,代碼如下所示:

//全進程只有一個JavaVM對象,所以可以保存,任何地方使用都沒有問題。

jint JNI_OnLoad(JavaVM* vm, void* reserved)

正如上面代碼所說,不論進程中有多少個線程,JavaVM卻是獨此一份,所以在任何地方都可以使用它。那麼,JavaVM和JNIEnv又有什麼關係呢?答案如下:

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

·  另外,後臺線程退出前,需要調用JavaVM的DetachCurrentThread函數來釋放對應的資源。

 

操作對象jobject

成員變量和成員函數是由類定義的,它是類的屬性,所以在JNI規則中,用jfieldID 和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爲這個函數和變量的簽名信息。如前所示,成員函數和成員變量都是類的信息,這兩個函數的第一個參數都是jclass。

調用method:

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

示例:

/*

調用JNIEnv的CallVoidMethod函數,注意CallVoidMethod的參數:

第一個是代表MediaScannerClient的jobject對象,

第二個參數是函數scanFile的jmethodID,後面是Java中scanFile的參數。

*/

       mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

獲取成員:

 

//獲得fieldID後,可調用Get<type>Field系列函數獲取jobject對應成員變量的值。

NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者調用Set<type>Field系列函數來設置jobject對應成員變量的值。

void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面我們列出一些參加的Get/Set函數。

GetObjectField()         SetObjectField()

GetBooleanField()         SetBooleanField()

GetByteField()           SetByteField()

GetCharField()           SetCharField()

GetShortField()          SetShortField()

GetIntField()            SetIntField()

GetLongField()           SetLongField()

GetFloatField()          SetFloatField()

GetDoubleField()                  SetDoubleField()

 

 

jstring介紹

Java中的String也是引用類型,不過由於它的使用非常頻繁,所以在JNI規範中單獨創建了一個jstring類型來表示Java中的String類型。雖然jstring是一種獨立的數據類型,但是它並沒有提供成員函數供操作。相比而言,C++中的string類就有自己的成員函數了。那麼該怎麼操作jstring呢?還是得依靠JNIEnv提供的幫助。這裏看幾個有關jstring的函數:

·  調用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以從Native的字符串得到一個jstring對象。其實,可以把一個jstring對象看成是Java中String對象在JNI層的代表,也就是說,jstring就是一個Java String。但由於Java String存儲的是Unicode字符串,所以NewString函數的參數也必須是Unicode字符串。

·  調用JNIEnv的NewStringUTF將根據Native的一個UTF-8字符串得到一個jstring對象。在實際工作中,這個函數用得最多。

·  上面兩個函數將本地字符串轉換成了Java的String對象,JNIEnv還提供了GetStringChars和GetStringUTFChars函數,它們可以將Java String對象轉換成本地字符串。其中GetStringChars得到一個Unicode字符串,而GetStringUTFChars得到一個UTF-8字符串。

·  另外,如果在代碼中調用了上面幾個函數,在做完相關工作後,就都需要調用ReleaseStringChars或ReleaseStringUTFChars函數對應地釋放資源,否則會導致JVM內存泄露。這一點和jstring的內部實現有關係,讀者寫代碼時務必注意這個問題。

爲了加深印象,來看processFile是怎麼做的:

[-->android_media_MediaScanner.cpp]

static void

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

{

   MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);

......

//調用JNIEnv的GetStringUTFChars得到本地字符串pathStr

    constchar *pathStr = env->GetStringUTFChars(path, NULL);

......

//使用完後,必須調用ReleaseStringUTFChars釋放資源

   env->ReleaseStringUTFChars(path, pathStr);

    ......

}

 

垃圾回收

 

 

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

[-->垃圾回收例子]

static jobject save_thiz = NULL; //定義一個全局的jobject

static void

android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,

 jstringmimeType, 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的引用計數。

JNI規範已很好地解決了這一問題,JNI技術一共提供了三種類型的引用,它們分別是:

·  Local Reference:本地引用。在JNI層函數中使用的非全局引用對象都是Local Reference。它包括函數調用時傳入的jobject、在JNI層函數中創建的jobject。LocalReference最大的特點就是,一旦JNI層函數返回,這些jobject就可能被垃圾回收。

·  Global Reference:全局引用,這種對象如不主動釋放,就永遠不會被垃圾回收。

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

平時用得最多的是Local Reference和Global Reference,下面看一個實例,代碼如下所示:

[-->android_media_MediaScanner.cpp::MyMediaScannerClient構造函數]

 MyMediaScannerClient(JNIEnv *env, jobjectclient)

       :   mEnv(env),

        //調用NewGlobalRef創建一個GlobalReference,這樣mClient就不用擔心被回收了。

           mClient(env->NewGlobalRef(client)),

           mScanFileMethodID(0),

           mHandleStringTagMethodID(0),

           mSetMimeTypeMethodID(0)

{

  ......

}

//析構函數

virtual ~MyMediaScannerClient()

{

  mEnv->DeleteGlobalRef(mClient);//調用DeleteGlobalRef釋放這個全局引用。

 }

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

---------------------------------------------------------------------------------------------------------------------------------------------------------------

JNI 的 call<>method 與 callNonVirtual<>method

nonVirtual應該就是忽略了繼承的多態性,即訪問的方法,不是實例實際類型的方法,而是引用類型的方法。下面是一個類型:

public class Father {

	@Override
	public void fun() {
		// TODO Auto-generated method stub
		Log.d("333", "Father involked");
	}

}
public class Child extends Father{
	@Override
	public void fun() {
		// TODO Auto-generated method stub
		Log.d("333", "Child involked");
	}
}

在如果定義

Father instance = new Child();

在C++中如下調用

jobject fObj = env->GetObjectField(obj,fID);
jclass fclass=env->FindClass("lc/test/jni/Father");
jmethodID fm= env->GetMethodID(fclass,"fun","()V");
env->CallNonvirtualVoidMethod(fObj,fclass,fm);

Calling Instance Methods of a Superclass(調用實例超類的方法)

You can call instance methods defined in a superclass that have been overridden in the class to which the object belongs. The JNI provides a set of CallNonvirtual<type>Method functions for this purpose. To call instance methods from the superclass that defined them, you do the following:

  • Obtain the method ID from the superclass using GetMethodID, as opposed to GetStaticMethodID).
  • Pass the object, superclass, method Id, and arguments to the family of nonvirtual invocation functions: CallNonvirtualVoidMethodCallNonvirtualBooleanMethod, and so on.

It is rare that you will need to invoke the instance methods of a superclass. This facility is similar to calling a superclass method, say f, using:

super.f();

in Java.

 

順帶說下 FindClass( ) 和 getObjectClass( )的區別,因爲在寫上面的測試代碼的時候我出過這個錯

FindClass( ) 就是通過包名類名去找,這個相當於絕對路徑吧

getObjectClass( ) 是通過一個obj的類型去找,這個地方需要注意的是,他是通過對象類型去找,不是通過引用類型去找

比如一開始在java 裏面寫了

 

Father p=new Child();

那麼在C++裏面獲得這個obj之後,如果用GetObjectClass( ) 獲得的就是 Child 的 Class , 不是 Father 的 Class

 -------------------------------------------------------------------------------------------------------------------------------------------------------------------

GetStringUTFChars/ReleaseStringUTFChars和Get<Type>ArrayElements/Release<Type>ArrayElements

其中GetStringUTFChars有一個isCopy的參數,代表是否複製一份在native堆,但是不知道有什麼用,反正返回的都是一個const char*,就是說無法改變其內容的指針。這個const char*在被調用ReleaseStringUTFChars前有效。

Get<Type>ArrayElements也有個isCopy參數,返回的是jbyte*,如果isCopy爲JNI_TRUE,那麼這個copy而來的buf是可以修改的,而且修改後的數據可以通過Release<Type>ArrayElements覆蓋java堆中的數據,是否覆蓋決定於Release<Type>ArrayElements的mode參數:

mode actions
0 copy back the content and free the elems buffer
JNI_COMMIT copy back the content but do not free the elems buffer
JNI_ABORT free the buffer without copying back the possible changes

-----------------------------------------------------------------------------------------------------------------------------------------------------------------

 

JNI參考指南

 

本人另一篇可以用於學習jni的文章:SQLite--SQLiteDatabase、SQLiteOpenHelper、sqlite3.c--(jni、頭文件)--源碼分析基於Android M

 

 

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