以下內容主要參考深入理解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 toGetStaticMethodID)
. - Pass the object, superclass, method Id, and arguments to the family of nonvirtual invocation functions:
CallNonvirtualVoidMethod
,CallNonvirtualBooleanMethod
, 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的文章:SQLite--SQLiteDatabase、SQLiteOpenHelper、sqlite3.c--(jni、頭文件)--源碼分析基於Android M