在上一章中我們學習到了如何在原生程式碼中訪問任意Java類中的靜態方法和例項方法,本章我們也通過一個示例來學習Java中的例項變數和靜態變數,在原生程式碼中如何來訪問和修改。靜態變數也稱為類變數(屬性),在所有例項物件中共享同一份資料,可以直接通過【類名.變數名】來訪問。例項變數也稱為成員變數(屬性),每個例項都擁有一份例項變數資料的拷貝,它們之間修改後的資料互不影響。下面看一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package com.study.jnilearn; /** * C/C++訪問類的例項變數和靜態變數 * @author yangxin */ public class AccessField { private native static void accessInstanceField(ClassField obj); private native static void accessStaticField(); public static void main(String[] args) { ClassField obj = new ClassField(); obj.setNum(10); obj.setStr("Hello"); // 原生程式碼訪問和修改ClassField為中的靜態屬性num accessStaticField(); accessInstanceField(obj); // 輸出原生程式碼修改過後的值 System.out.println("In Java--->ClassField.num = " + obj.getNum()); System.out.println("In Java--->ClassField.str = " + obj.getStr()); } static { System.loadLibrary("AccessField"); } } |
AccessField是程式的入口類,定義了兩個native方法:accessInstanceField和accessStaticField,分別用於演示在原生程式碼中訪問Java類中的例項變數和靜態變數。其中accessInstaceField方法訪問的是類的例項變數,所以該方法需要一個ClassField例項作為形參,用於訪問該物件中的例項變數。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.study.jnilearn; /** * ClassField.java * 用於原生程式碼訪問和修改該類的屬性 * @author yangxin * */ public class ClassField { private static int num; private String str; public int getNum() { return num; } public void setNum(int num) { ClassField.num = num; } public String getStr() { return str; } public void setStr(String str) { this.str = str; } } |
在本例中沒有將例項變數和靜態變數定義在程式入口類中,新建了一個ClassField的類來定義類的屬性,目的是為了加深在C/C++程式碼中可以訪問任意Java類中的屬性。在這個類中定義了一個int型別的例項變數num,和一個java.lang.String型別的靜態變數str。這兩個變數會被原生程式碼訪問和修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessField */ #ifndef _Included_com_study_jnilearn_AccessField #define _Included_com_study_jnilearn_AccessField #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessField * Method: accessInstanceField * Signature: (Lcom/study/jnilearn/ClassField;)V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessInstanceField (JNIEnv *, jclass, jobject); /* * Class: com_study_jnilearn_AccessField * Method: accessStaticField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessStaticField (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif |
以上程式碼是程式入口類AccessField.class為native方法生成的原生程式碼函式原型標頭檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
// AccessField.c #include "com_study_jnilearn_AccessField.h" /* * Class: com_study_jnilearn_AccessField * Method: accessInstanceField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessInstanceField (JNIEnv *env, jclass cls, jobject obj) { jclass clazz; jfieldID fid; jstring j_str; jstring j_newStr; const char *c_str = NULL; // 1.獲取AccessField類的Class引用 clazz = (*env)->GetObjectClass(env,obj); if (clazz == NULL) { return; } // 2. 獲取AccessField類例項變數str的屬性ID fid = (*env)->GetFieldID(env,clazz,"str", "Ljava/lang/String;"); if (clazz == NULL) { return; } // 3. 獲取例項變數str的值 j_str = (jstring)(*env)->GetObjectField(env,obj,fid); // 4. 將unicode編碼的java字串轉換成C風格字串 c_str = (*env)->GetStringUTFChars(env,j_str,NULL); if (c_str == NULL) { return; } printf("In C--->ClassField.str = %s\n", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); // 5. 修改例項變數str的值 j_newStr = (*env)->NewStringUTF(env, "This is C String"); if (j_newStr == NULL) { return; } (*env)->SetObjectField(env, obj, fid, j_newStr); // 6.刪除區域性引用 (*env)->DeleteLocalRef(env, clazz); (*env)->DeleteLocalRef(env, j_str); (*env)->DeleteLocalRef(env, j_newStr); } /* * Class: com_study_jnilearn_AccessField * Method: accessStaticField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessStaticField (JNIEnv *env, jclass cls) { jclass clazz; jfieldID fid; jint num; //1.獲取ClassField類的Class引用 clazz = (*env)->FindClass(env,"com/study/jnilearn/ClassField"); if (clazz == NULL) { // 錯誤處理 return; } //2.獲取ClassField類靜態變數num的屬性ID fid = (*env)->GetStaticFieldID(env, clazz, "num", "I"); if (fid == NULL) { return; } // 3.獲取靜態變數num的值 num = (*env)->GetStaticIntField(env,clazz,fid); printf("In C--->ClassField.num = %d\n", num); // 4.修改靜態變數num的值 (*env)->SetStaticIntField(env, clazz, fid, 80); // 刪除屬部引用 (*env)->DeleteLocalRef(env,clazz); } |
以上程式碼是對標頭檔案中函式原型的實現。
執行程式,輸出結果如下:
程式碼解析:
一、訪問例項變數
在main方法中,通過呼叫accessInstanceField()方法來呼叫本地函式Java_com_study_jnilearn_AccessField_accessInstanceField,定位到函式32行:
1 |
j_str = (jstring)(*env)->GetObjectField(env,obj,fid); |
該函式就是用於獲取ClassField物件中num的值。下面是函式的原型:
1 |
jobject (JNICALL *GetObjectField) (JNIEnv *env, jobject obj, jfieldID fieldID); |
因為例項變數str是String型別,屬於引用型別。在JNI中獲取引用型別欄位的值,呼叫GetObjectField函式獲取。同樣的,獲取其它型別欄位值的函式還有GetIntField,GetFloatField,GetDoubleField,GetBooleanField等。這些函式有一個共同點,函式引數都是一樣的,只是函式名不同,我們只需學習其中一個函式如何呼叫即可,依次類推,就自然知道其它函式的使用方法。
GetObjectField函式接受3個引數,env是JNI函式表指標,obj是例項變數所屬的物件,fieldID是變數的ID(也稱為屬性描述符或簽名),和上一章中方法描述符是同一個意思。env和obj引數從Java_com_study_jnilearn_AccessField_accessInstanceField函式形參列表中可以得到,那fieldID怎麼獲取呢?瞭解Java反射的童鞋應該知道,在Java中任何一個類的.class位元組碼檔案被載入到記憶體中之後,該class子節碼檔案統一使用Class類來表示該類的一個引用(相當於Java中所有類的基類是Object一樣)。然後就可以從該類的Class引用中動態的獲取類中的任意方法和屬性。注意:Class類在Java SDK繼承體系中是一個獨立的類,沒有繼承自Object。請看下面的例子,通過Java反射機制,動態的獲取一個類的私有例項變數的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void main(String[] args) throws Exception { ClassField obj = new ClassField(); obj.setStr("YangXin"); // 獲取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); } |
執行程式後,輸出結果當然是列印出str屬性的值“YangXin”。所以我們在原生程式碼中呼叫JNI函式訪問Java物件中某一個屬性的時候,首先第一步就是要獲取該物件的Class引用,然後在Class中查詢需要訪問的欄位ID,最後呼叫JNI函式的GetXXXField系列函式,獲取欄位(屬性)的值。上例中,首先呼叫GetObjectClass函式獲取ClassField的Class引用:
1 |
clazz = (*env)->GetObjectClass(env,obj); |
然後呼叫GetFieldID函式從Class引用中獲取欄位的ID(str是欄位名,Ljava/lang/String;是欄位的型別)
1 |
fid = (*env)->GetFieldID(env,clazz,"str", "Ljava/lang/String;"); |
最後呼叫GetObjectField函式,傳入例項物件和欄位ID,獲取屬性的值
1 |
j_str = (jstring)(*env)->GetObjectField(env,obj,fid); |
呼叫SetXXXField系列函式,可以修改例項屬性的值,最後一個引數為屬性的值。引用型別全部呼叫SetObjectField函式,基本型別呼叫SetIntField、SetDoubleField、SetBooleanField等
1 |
(*env)->SetObjectField(env, obj, fid, j_newStr); |
二、訪問靜態變數
訪問靜態變數和例項變數不同的是,獲取欄位ID使用GetStaticFieldID,獲取和修改欄位的值使用Get/SetStaticXXXField系列函式,比如上例中獲取和修改靜態變數num:
1 2 3 4 |
// 3.獲取靜態變數num的值 num = (*env)->GetStaticIntField(env,clazz,fid); // 4.修改靜態變數num的值 (*env)->SetStaticIntField(env, clazz, fid, 80); |
總結:
1、由於JNI函式是直接操作JVM中的資料結構,不受Java訪問修飾符的限制。即,在原生程式碼中可以呼叫JNI函式可以訪問Java物件中的非public屬性和方法
2、訪問和修改例項變數操作步聚:
1>、呼叫GetObjectClass函式獲取例項物件的Class引用
2>、呼叫GetFieldID函式獲取Class引用中某個例項變數的ID
3>、呼叫GetXXXField函式獲取變數的值,需要傳入例項變數所屬物件和變數ID
4>、呼叫SetXXXField函式修改變數的值,需要傳入例項變數所屬物件、變數ID和變數的值
3、訪問和修改靜態變數操作步聚:、
1>、呼叫FindClass函式獲取類的Class引用
2>、呼叫GetStaticFieldID函式獲取Class引用中某個靜態變數ID
3>、呼叫GetStaticXXXField函式獲取靜態變數的值,需要傳入變數所屬Class的引用和變數ID
4>、呼叫SetStaticXXXField函式設定靜態變數的值,需要傳入變數所屬Class的引用、變數ID和變數的值
示例程式碼下載地址:https://code.csdn.net/xyang81/jnilearn