JNI系列
深入理解JNI
最近在學習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層來實現
JNI層對飲libmedia_jni.so,一般採用
lib模塊名_jni.so
的命名方式Native層對應的是libmedia.so,這個庫完成了實際的功能
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 Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID,……)`
其中type對應java函數返回值,要是調用java中的static函數,則需要使用JNIEnv輸出的CallStaticMethod系列
同理通過jfieldID操作jobject的成員變量
`NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
NativeType Set<type>Field(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判斷是否被回收
小結
通過閱讀本章主要學習了
JNI作用
結合MediaScanner等源碼學習了JNI註冊調用等過程
JNIEnv的用法
JNI中籤名、數據類型、垃圾回收機制