Android NDK開發筆記四:Java和c/c++的相互調用

       JNI的引入使java有了調用C/C++端代碼的能力,然而在JNI中還有 一個非常重要的內容,那就是在C/C++本地
代碼中訪問Java端的代碼,一個常見的應用就是獲取類的屬性和調用類的方法,爲了在C/C++中表示屬性和方法,JNI
在jni.h頭文件中定義了jfieldId,jmethodID類型來分別代表Java端的屬性和方法。
       我們在訪問,或者設置Java屬性的時候,首先就要先在本地代碼取得代表該Java屬性的jfieldID,然後才能在本地代碼
中進行Java屬性操作,同樣的,我們需要呼叫Java端的方法時,也是需要取得代表該方法的jmethodID才能進行Java
方法調用。

使用JNIEnv的:

GetFieldID/GetMethodID

GetStaticFieldID/GetStaticMethodID

來取得相應的jfieldID和jmethodID。


下面來具體看一下這幾個方法:
GetFieldID(jclass clazz,const char* name,const char* sign) 
方法的參數說明: 

clazz:這個簡單就是這個方法依賴的類對象的class對象 

name:這個是這個字段的名稱 

sign:這個是這個字段的簽名(我們知道每個變量,每個方法都是有簽名的)

簽名的規則如下:

數據類型 簽名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
object L開頭,然後以/分割包的完整類型,再加;號,如Ljava/lang/String;
Array 以[開頭,再加上數組元素的簽名,比如int[],簽名就是[I,Object數組簽名就是[Ljava/lang/Object;

 

 

 

 

 

 

 

 

 

 

 

 

 

來看一個使用簽名獲得屬性和方法的例子:

Java代碼:

public class Student {
    public int age;
    public void setBirthday(int age, Date date, int[] arr){
        System.out.println("print setBirthday function");
    }
    public native void test();
}

Jni代碼:

extern "C" JNIEXPORT jstring JNICALL
Java_com_demo_Student_test(
        JNIEnv *env,
        jobject thiz
) {
    jclass student_clz = env->GetObjectClass(thiz);
    jfieldID age_field = env->GetFieldID(student_clz, "age", "I");
    jmethodID set_method = env->GetMethodID(student_clz, "setBirthday", "(ILjava/util/Date;[I)V");
    env->CallVoidMethod(thiz, set_method, 0, NULL, NULL);
}

其中的 "I" 和 "(ILjava/util/Date;[I)V" 就是屬性和方法的簽名

Jni調用java的方法跟反射有點類似,我們對比一下反射的調用方式:

public static void main(String[] args) throws Exception {
	ClassField obj = new ClassField();
	obj.setStr("Test");
	// 獲取ClassField字節碼對象的Class引用
	Class<?> clazz = obj.getClass();
	// 獲取str屬性
	Field field = clazz.getDeclaredField("str");
	// 取消權限檢查,因爲Java語法規定,非public屬性是無法在外部訪問的
	field.setAccessible(true);
	// 獲取obj對象中的str屬性的值
	String str = (String)field.get(obj);
	System.out.println("str = " + str);
}

所以我們在本地代碼中調用JNI函數訪問Java對象中某一個
屬性的時候,首先第一步就是要獲取該對象的Class引用,然後在Class中查找需要訪問的字段ID,最後調用JNI函數的
GetXXXField系列函數,獲取字段(屬性)的值。

JNI訪問字符串

java內部使用的是utf-16 16bit 的編碼方式
jni 裏面使用的utf-8 unicode編碼方式 英文是1個字節,中文 3個字節
C/C++ 使用 ascii編碼 ,中文的編碼方式 GB2312編碼 中文 2個字節

Java代碼:

public native static String sayHello(String text);

C/C++代碼:

JNIEXPORT jstring JNICALL Java_JString_sayHello
(JNIEnv * env, jclass jclaz, jstring jstr) {
	const char * c_str = NULL;
	char buf[128] = {0};
	jboolean iscopy;
	c_str = (*env)->GetStringUTFChars(env, jstr, &iscopy);
	printf("isCopy:%d\n", iscopy);
	if(c_str == NULL) {
	return NULL;
	}
	printf("C_str: %s \n", c_str);
	sprintf(buf,"Hello, 你好 %s", c_str);
	printf("C_str: %s \n", buf);
	(*env)->ReleaseStringUTFChars(env, jstr, c_str);
	return (*env)->NewStringUTF(env,buf);
}

一 、訪問Java成員變量

Java成員變量一般有兩類:靜態和非靜態。所以在JNI中對這兩種不同的類型就有了兩種不太相同的調用方法。

1.訪問非靜態成員

java代碼

public int property;

Jni代碼:

JNIEXPORT void JNICALL Java_Hello_testField(JNIEnv *env, jobject jobj) {
	jclass claz = (*env)->GetObjectClass(env,jobj);
	jfieldID jfid = (*env)->GetFieldID(env, claz, "property","I");
	jint va = (*env)->GetIntField(env,jobj, jfid);
	printf("va: %d", va);
	(*env)->SetIntField(env, jobj, jfid, va + 100);
}

上例中,首先調用GetObjectClass函數獲取ClassField的Class引用:

clazz = (*env)->GetObjectClass(env,obj);

然後調用GetFieldID函數從Class引用中獲取字段的ID(property是字段名,I是字段的簽名)

jfieldID jfid = (*env)->GetFieldID(env, claz, "property","I");

最後調用GetIntField函數,傳入實例對象和字段ID,獲取屬性的值

jint va = (*env)->GetIntField(env,jobj, jfid);

如果要修改這個值就可以使用SetIntField函數:

(*env)->SetIntField(env, jobj, jfid, va + 100);

2.訪問靜態成員

訪問靜態變量和實例變量不同的是,獲取字段ID使用GetStaticFieldID,獲取和修改字段的值使用
Get/SetStaticXXXField系列函數,比如上例中獲取和修改靜態變量num:

num = (*env)->GetStaticIntField(env,clazz,fid);
// 修改靜態變量num的值
(*env)->SetStaticIntField(env, clazz, fid, 80);

二、訪問Java中的函數

Java成員函數一般有兩類:靜態和非靜態。所以在JNI中對這兩種不同的類型就有了兩種不太相同的調用方法,這兩
種不同類型雖然他們的調用方式有些許不同,但是,他們的實質上是一樣的。只是調用的接口的名字有區別,而對於
流程是沒有區別的。

Java代碼如下:

private static void callStaticMethod(String str, int i) {
    System.out.format("ClassMethod::callStaticMethod called!-->str=%s," +
    " i=%d\n", str, i);
}

JNI代碼如下:

JNIEXPORT void JNICALL Java_JniTest_callJavaStaticMethod(JNIEnv *env, jobject jobje){
	jclass clz = (*env)->FindClass(env,"ClassMethod");
	if(clz == NULL) {
	printf("clz is null");
	return;
	}
	jmethodID jmeid = (*env)->GetStaticMethodID(env, clz, "callStaticMethod", "
	(Ljava/lang/String;I)V");
	if(jmeid == NULL) {
	printf("jmeid is null");
	return;
	}
	jstring arg = (*env)->NewStringUTF(env, "我是靜態類");
	(*env)->CallStaticVoidMethod(env,clz,jmeid,arg,100);
	(*env)->DeleteLocalRef(env,clz);
	(*env)->DeleteLocalRef(env,arg);
}

總結

1、由於JNI函數是直接操作JVM中的數據結構,不受Java訪問修飾符的限制。即,在本地代碼中可以調用JNI函數可以
訪問Java對象中的非public屬性和方法
 
2、訪問和修改實例變量操作步聚:
  • 調用GetObjectClass函數獲取實例對象的Class引用
  • 調用GetFieldID函數獲取Class引用中某個實例變量的ID
  • 調用GetXXXField函數獲取變量的值,需要傳入實例變量所屬對象和變量ID
  • 調用SetXXXField函數修改變量的值,需要傳入實例變量所屬對象、變量ID和變量的值
  •  
3、訪問和修改靜態變量操作步聚:
  • 調用FindClass函數獲取類的Class引用
  • 調用GetStaticFieldID函數獲取Class引用中某個靜態變量ID
  • 調用GetStaticXXXField函數獲取靜態變量的值,需要傳入變量所屬Class的引用和變量ID
  • 調用SetStaticXXXField函數設置靜態變量的值,需要傳入變量所屬Class的引用、變量ID和變量的值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章