編寫JNI的兩種應用層與JNI層方法對映方式
通常我們在編寫的JNI 時,在定義上層應用層需要呼叫的函式中,我們需要對該函式進行應用層與JNI層方法之間的對映。這樣上層的Android應用程式才能正確的呼叫我們的JNI函式,這種對映的方式一共有兩種。
在函式名中進行對映
在函式名中進行對映是最為簡單的一種方法,因為只要我們知道呼叫我們JNI函式的Java檔案所在的路徑,那麼我們就將該路徑放在我們JNI對應函式的前面就可以了,同時還要在函式前面加上Java,路徑間用”_”進行分隔。例如,如果我們的應用程式中這樣載入動態連結庫:
package com.intel.jni;
public class CC1100DataSource
{
public native int Open();
public native int Close();
public native byte[] Read( int len);
public native int Write(char[] buf , int len);
public static CC1100DataSource cc1100instance;
public static CC1100DataSource getCC1100DataSource()
{
cc1100instance = new CC1100DataSource();
return cc1100instance;
}
static
{
System.loadLibrary("cc1100");
}
}
由上面的應用程式我們知道該java檔案放在com/intel/jni包中,同時檔名為CC1100DataSource,那麼我們在編寫相應的jni函式時就應該這樣編寫,這裡以open函式為例:
/*
* Class: cc1100
* Method: Open
* Signature: ()I
*/
JNIEXPORT jint JNICALL Java_com_intel_jni_CC1100DataSource_Open
(JNIEnv *env , jobject obj)
{
if(fd<=0)fd = open("/dev/cc1100", O_RDWR|O_NDELAY|O_NOCTTY);
if(fd <=0 )__android_log_print(ANDROID_LOG_ERROR, "serial", "open /dev/cc1100 Error");
else __android_log_print(ANDROID_LOG_INFO, "serial", "open /dev/cc1100 Sucess fd=%d\n",fd);
}
可以看到我們的函式名為Java_com_intel_jni_CC1100DataSource_Open,在open前面分別有Java和Java檔案所處的路徑。這樣編寫之後,我們的jni就和上層呼叫的Java程式就建立好連線了。
採用註冊本地方法的方式
這種方式相對於前一種在編寫的時候要稍微複雜一點,但是定義好了之後,會更加靈活。在我們實際的開發過程中會更加傾向該種方式。該種方式在定義的時候主要分為三步:
第一步:建立對映連線
建立對映連線需要使用到JNINativeMethod結構體型別的陣列,JNINativeMethod型別的資料主要由三個部分組成,第一個是定義上層應用程式的呼叫函式的名,第二個是定義函式返回型別和引數型別。第四個是其對應的JNI函式名。
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
下面就以我們通用的open、read、write、close函式作為例子來進行對映。
static const JNINativeMethod methods[] =
{
{"_open", "()I", (void *) Radar_Open},
{"_read", "(I)[B", (void *) Radar_Read},
{"_write", "(C)I", (void *) Radar_Write},
{"_open", "()I", (void *) Radar_Open},
};
上面建立了一個JNINativeMethod型別的陣列,其中open函式為返回值為int型別,無引數。read函式引數為int型別資料,返回值型別為byte陣列。關於這種表示方式可以參照博文Android JNI 使用的資料結構JNINativeMethod詳解。
第二步:採用RegisterNatives註冊第一步所建立好的對映
在這一步中我們要將之前建立的函式對映註冊進動態連結庫檔案,同時由於在上一步建立對映的過程中我們並沒有定義好上層呼叫的Java檔案的路徑和檔名,所以在註冊的時候我們也要一一定義。
這裡我還是以自己編寫的一個radar驅動程式為例:
int register_radar_jni(JNIEnv* env)
{
static const char* const kClassName = "com/intel/jni/radar";
jclass clazz;
clazz = (*env)->FindClass(env,kClassName);
if(clazz == NULL)
{
LOGE("Can't find class %s\n", kClassName);
return -1;
}
if((*env)->RegisterNatives(env,clazz, methods, sizeof(methods)/sizeof(methods[0]))!= JNI_OK)
{
LOGE("Failed registering methods for %s\n", kClassName);
return -1;
}
return 0;
}
從上面我們也可以體會到,每次當我們需要更改上層呼叫的java檔案的路徑或者是檔名時,只需要更改kClassName所指定的路徑就行,而不需要像第一種方式那樣要將所有與之對映的檔案的JNI函式全部都更改名字。
第三步:將上一步的註冊函式放入JNI_OnLoad方法當中
注意這個JNI_OnLoad方法屬於JNI的系統方法,當我們在上層java程式中呼叫System.loadLibrary(“cc1100”);時就會首先去呼叫JNI檔案裡面的JNI_OnLoad函式。平時如果我們沒有寫JNI_Onload方法,則表示JNI_Onload方法為空。
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if((*vm)->GetEnv(vm,(void **) &env, JNI_VERSION_1_4)!=JNI_OK)
{
LOGE("GetEnv failed!!!!!!!!!!!!!!");
return result;
}
register_radar_jni(env);
return JNI_VERSION_1_4;
}
需要注意的是對於JNI檔案,我們既可以採用C++的語言編寫,也可以採用C語言編寫,如果採用C語言編寫,可能在有些語法規則上面會有一些細微區別。例如我們在使用env的時候,如果是C++語言,則為:env->RegisterNatives,而如果是C語言的話,便是(*env)->RegisterNatives。網上有一篇關於JNIEnv的介紹JNIEnv解析。