Android NDK開發之旅10 JNI JNI數據類型與方法屬性訪問

###JNI數據類型java

####基本數據 Java基本數據類型與JNI數據類型的映射關係數組

Java類型->JNI類型->C類型
複製代碼

JNI的基本數據類型有(左邊是Java,右邊是JNI):微信

boolean 			jboolean
byte 				jbyte
char 				jchar
short 				jshort
int 				jint
long 				jlong
float 				jfloat
double 				jdouble
void 				void
複製代碼

####引用類型(對象)dom

String 				jstring
Object 				jobject

數組,基本數據類型的數組
byte[]				jByteArray
對象數組
object[](String[]) 	jobjectArray
複製代碼

###native函數參數說明ide

每一個native函數,都至少有兩個參數(JNIEnv*,jclass或者jobject)。函數

1)當native方法爲靜態方法時: jclass 表明native方法所屬類的class對象(JniTest.class)。測試

2)當native方法爲非靜態方法時: jobject 表明native方法所屬的對象。spa

######native函數的頭文件能夠本身寫。命令行

###關於屬性與方法的簽名code

####1、屬性的簽名

屬性的簽名其實就是屬性的類型的簡稱,對應關係以下:

Java屬性的簽名.png

尤爲注意的是,類的簽名格式就是:

L完整包名;
複製代碼

其中完整包名用 / 代替.

末尾的 ; 不能省略

數組的簽名就是:

[類型簽名
複製代碼

其中,多爲數組就用多個[

####2、方法的簽名

獲取方法的簽名比較麻煩一些,經過下面的方法也能夠拿到屬性的簽名。

打開命令行,輸入javap,出現如下信息:

javap命令.png

上述信息告訴咱們,經過如下命令就能夠拿到指定類的全部屬性、方法的簽名了,很方便有木有?!

javap -s -p 完整類名
複製代碼

咱們經過cd命令,來到Java工程的bin目錄,而後輸入命令:

D:\WebPrj\TestJni\bin>javap -s -p com.test.JniTest
Compiled from "JniTest.java"
public class com.test.JniTest {
  public java.lang.String str;
    descriptor: Ljava/lang/String;
  public static int NUM;
    descriptor: I
  static {};
    descriptor: ()V

  public com.test.JniTest();
    descriptor: ()V

  public static native java.lang.String getStringFromC();
    descriptor: ()Ljava/lang/String;

  public native void accessField();
    descriptor: ()V

  public native void accessStaticField();
    descriptor: ()V

  public int genRandomInt(int);
    descriptor: (I)I

  public native void accessMethod();
    descriptor: ()V

  public native void accessStaticMethod();
    descriptor: ()V

  public static java.lang.String getUUID();
    descriptor: ()Ljava/lang/String;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
}
複製代碼

其中,descriptor就是咱們須要的簽名了,注意簽名中末尾的分號不能省略。

方法簽名的規律就是,括號不能夠省略:

(參數類型簽名)返回值類型簽名
複製代碼

###C/C++訪問Java的屬性、方法

有如下幾種狀況:

  1. 訪問Java類的非靜態屬性。
  2. 訪問Java類的靜態屬性。
  3. 訪問Java類的非靜態方法。
  4. 訪問Java類的靜態方法。
  5. 間接訪問Java類的父類的方法。
  6. 訪問Java類的構造方法。

####1、訪問Java的非靜態屬性

Java方法中,經過調用accessField,利用C修改靜態屬性

public String str = "Li lu";

//訪問非靜態屬性str,修改它的值
public native void accessField();
複製代碼

C代碼以下:(頭文件能夠不寫,直接寫實現)

JNIEXPORT void JNICALL Java_com_test_JniTest_accessField
(JNIEnv * env, jobject jobj){

	//經過對象拿到Class
	jclass clz = (*env)->GetObjectClass(env, jobj);
	//拿到對應屬性的ID
	jfieldID fid = (*env)->GetFieldID(env, clz, "str", "Ljava/lang/String;");
	//經過屬性ID拿到屬性的值
	jstring jstr = (*env)->GetObjectField(env, jobj, fid);

	//經過Java字符串拿到C字符串,第三個參數是一個出參,用來告訴咱們GetStringUTFChars內部是否複製了一份字符串
	//若是沒有複製,那麼出參爲isCopy,這時候就不能修改字符串的值了,由於Java中常量池中的字符串是不容許修改的(可是jstr能夠指向另一個字符串)
	char* cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
	//在C層修改這個屬性的值
	char res[20] = "I love you : ";
	strcat(res, cstr);

	//從新生成Java的字符串,而且設置給對應的屬性
	jstring jstr_new = (*env)->NewStringUTF(env, res);
	(*env)->SetObjectField(env, jobj, fid, jstr_new);

	//最後釋放資源,通知垃圾回收器來回收
	//良好的習慣就是,每次GetStringUTFChars,結束的時候都有一個ReleaseStringUTFChars與之呼應
	(*env)->ReleaseStringUTFChars(env, jstr, cstr);
}
複製代碼

最後在Java中測試:

public static void main(String[] args) {

	JniTest test = new JniTest();
	System.out.println(test.str);
	//修改非靜態屬性str
	test.accessField();
	System.out.println(test.str);
	
}
複製代碼

####2、訪問Java的靜態屬性

Java代碼以下:

//訪問靜態屬性NUM,修改它的值
public static int NUM = 1;

public native void accessStaticField();
複製代碼

C代碼以下:

JNIEXPORT void JNICALL Java_com_test_JniTest_accessStaticField
(JNIEnv * env, jobject jobj){
	//與上面相似,只不過是某些方法須要加上Static
	jclass clz = (*env)->GetObjectClass(env, jobj);
	jfieldID fid = (*env)->GetStaticFieldID(env, clz, "NUM", "I");
	jint jInt = (*env)->GetStaticIntField(env, clz, fid);
	jInt++;
	(*env)->SetStaticIntField(env, clz, fid, jInt);
}
複製代碼

最後在Java中測試:

public static void main(String[] args) {
	
	JniTest test = new JniTest();
	System.out.println(NUM);
	test.accessStaticField();
	System.out.println(NUM);

}
複製代碼

####3、訪問Java的非靜態方法

Java代碼以下,經過調用accessMethod,在底層用C語言調用genRandomInt方法

//產生指定範圍的隨機數
public int genRandomInt(int max){
	System.out.println("genRandomInt 執行了...max = "+ max);
	return new Random().nextInt(max); 
}

public native void accessMethod();
複製代碼

C代碼以下:

JNIEXPORT void JNICALL Java_com_test_JniTest_accessMethod
(JNIEnv * env, jobject jobj){
	jclass clz = (*env)->GetObjectClass(env, jobj);
	//拿到方法的ID,最後一個參數是方法的簽名
	jmethodID mid = (*env)->GetMethodID(env, clz, "genRandomInt", "(I)I");
	//調用該方法,最後一個是可變參數,就是調用該方法所傳入的參數
	//套路是若是返回是:Call返回類型Method
	jint jInt = (*env)->CallIntMethod(env, jobj, mid, 100);
	printf("output from C : %d", jInt);
}
複製代碼

最後在Java中測試:

public static void main(String[] args) {
	
	JniTest test = new JniTest();
	test.accessMethod();

}
複製代碼

####4、訪問Java的靜態方法

Java代碼以下,經過調用accessStaticMethod,在底層用C語言調用getUUID方法

public native void accessStaticMethod();

//產生UUID字符串
public static String getUUID(){
	System.out.println("getUUID 執行了...");
	return UUID.randomUUID().toString();
}
複製代碼

C代碼以下:

JNIEXPORT void JNICALL Java_com_test_JniTest_accessStaticMethod
(JNIEnv * env, jobject jobj){
	jclass clz = (*env)->GetObjectClass(env, jobj);
	jmethodID mid = (*env)->GetStaticMethodID(env, clz, "getUUID", "()Ljava/lang/String;");

	//調用java的靜態方法,拿到返回值
	jstring jstr = (*env)->CallStaticObjectMethod(env, clz, mid);

	//把拿到的Java字符串轉換爲C的字符串
	char* cstr= (*env)->GetStringUTFChars(env, jstr, NULL);

	//後續操做,產生以UUID爲文件名的文件
	char fielName[100];
	sprintf(fielName, "D:\\%s.txt", cstr);
	FILE* f = fopen(fielName, "w");
	fputs(cstr, f);
	fclose(f);

	printf("output from C : File had saved", jstr);
}
複製代碼

最後在Java中測試:

public static void main(String[] args) {

	JniTest test = new JniTest();
	test.accessStaticMethod();

}
複製代碼

####5、間接訪問Java類的父類的方法

Java代碼以下:

父類:

package com.test;

public class Human {
	protected void speek() {
		System.out.println("Human Speek");
	}	
}
複製代碼

子類:

package com.test;

public class Man extends Human {
	@Override
	protected void speek() {
		// 能夠經過super關鍵字來訪問父類的方法
		// super.speek();
		System.out.println("Man Speek");
	}
}
複製代碼

在TestJni類中有Human屬性:

//父類的引用指向子類的對象
Human man= new Man();

public native void accessNonvirtualMethod();
複製代碼

若是是直接使用man.speek()的話,訪問的是子類Man的方法 可是經過底層C的方式能夠間接訪問到父類Human的方法,跳過子類的實現,甚至你能夠直接哪一個父類(若是父類有多個的話),這是Java作不到的。

下面是C代碼實現,無非就是屬性和方法的訪問:

JNIEXPORT void JNICALL Java_com_test_JniTest_accessNonvirtualMethod
(JNIEnv * env, jobject jobj){
	//先拿到屬性man
	jclass clz=(*env)->GetObjectClass(env, jobj);
	jfieldID fid = (*env)->GetFieldID(env, clz, "man", "Lcom/test/Human;");
	jobject man = (*env)->GetObjectField(env, jobj, fid);

	//拿到父類的類,以及speek的方法id
	jclass clz_human = (*env)->FindClass(env, "com/test/Human");
	jmethodID mid = (*env)->GetMethodID(env, clz_human, "speek", "()V");

	//調用本身的speek實現
	(*env)->CallVoidMethod(env, man, mid);
	//調用父類的speek實現
	(*env)->CallNonvirtualVoidMethod(env, man, clz_human, mid);
}
複製代碼
  1. 當有這個類的對象的時候,使用(*env)->GetObjectClass(),至關於Java中的test.getClass()
  2. 當有沒有這個類的對象的時候,(*env)->FindClass(),至關於Java中的Class.forName("com.test.TestJni")

這裏直接使用CallVoidMethod,雖然傳進去的是父類的Method ID,可是訪問的讓然是子類的實現。

最後,經過CallNonvirtualVoidMethod,訪問不覆蓋的父類方法(C++使用virtual關鍵字來覆蓋父類的實現),固然你也能夠指定哪一個父類(若是有多個父類的話)。

最後在Java中測試:

public static void main(String[] args) {

	JniTest test = new JniTest();
	//這時候是調用子類Man的方法
	test.man.speek();
	//可是經過JNI的方式,能夠訪問到父類的speek方法
	test.accessNonvirtualMethod();

}
複製代碼

####6、訪問Java類的構造方法

Java代碼以下,經過調用accessConstructor,在底層用C語言調用java.util.Date產生一個當前的時間戳,而且返回。

//調用Date的構造函數
public native long accessConstructor();
複製代碼

C代碼以下:

JNIEXPORT jlong JNICALL Java_com_test_JniTest_accessConstructor
(JNIEnv * env, jobject jobj){

	jclass clz_date = (*env)->FindClass(env, "java/util/Date");
	//構造方法的函數名的格式是:<init>
	//不能寫類名,由於構造方法函數名都同樣區分不了,只能經過參數列表(簽名)區分
	jmethodID mid_Date = (*env)->GetMethodID(env, clz_date, "<init>", "()V");;

	//調用構造函數
	jobject date = (*env)->NewObject(env, clz_date, mid_Date);

	//注意簽名,返回值long的屬性簽名是J
	jmethodID mid_getTime= (*env)->GetMethodID(env, clz_date, "getTime", "()J");
	//調用getTime方法
	jlong jtime = (*env)->CallLongMethod(env, date, mid_getTime);

	return jtime;
}
複製代碼

最後在Java中測試:

public static void main(String[] args) {

	JniTest test = new JniTest();
	//直接在Java中構造Date而後調用getTime
	Date date = new Date();
	System.out.println(date.getTime());
	//經過C語音構造Date而後調用getTime
	long time = test.accessConstructor();
	System.out.println(time);

}
複製代碼

####總結

屬性、方法的訪問的使用是和Java的反射API相似的。

###綜合進階案例——JNI返回中文亂碼問題

測試亂碼問題:

public native void testChineseIn(String chinese);//傳進去
public native String testChineseOut();//取出來會亂碼

public static void main(String[] args) {

	//傳中文進去,而後轉爲C字符串,直接在C層輸出是沒有問題的
	JniTest test = new JniTest();
	test.testChineseIn("我愛你");
	//C層將C字符串轉換爲JavaString而後輸出,就會亂碼
	System.out.println(test.testChineseOut());

}
複製代碼

C代碼以下:

JNIEXPORT void JNICALL Java_com_test_JniTest_testChineseIn
(JNIEnv * env, jobject jobj, jstring chinese){

	char* c_chinese = (*env)->GetStringUTFChars(env, chinese, NULL);
	printf("%s", c_chinese);
}

JNIEXPORT jstring JNICALL Java_com_test_JniTest_testChineseOut
(JNIEnv * env, jobject jobj){

	char* c_str = "我愛你";
	jstring j_str = (*env)->NewStringUTF(env, c_str);
	return j_str;
}
複製代碼

結果輸出,其中第一條是C返回的亂碼,第二條是傳進去在C層打印的結果:

ÎҰ®Ä
我愛你
複製代碼

######能夠看到C執行的速度要比Java快。

#####緣由分析,調用NewStringUTF的時候,產生的是UTF-16的字符串,可是咱們須要的時候UTF-8字符串。

解決辦法,經過Java的String類的構造方法來進行字符集變換。

JNIEXPORT jstring JNICALL Java_com_test_JniTest_testChineseOut
(JNIEnv * env, jobject jobj){

	//須要返回的字符串
	char* c_str = "我愛你";
	//jstring j_str = (*env)->NewStringUTF(env, c_str);

	//經過調用構造方法String string = new String(byte[], charsetName);來解決亂碼問題

	//0.找到String類
	jclass clz_String =  (*env)->FindClass(env, "java/lang/String");
	jmethodID mid = (*env)->GetMethodID(env, clz_String, "<init>", "([BLjava/lang/String;)V");

	//準備new String的參數:byte數組以及字符集
	//1.建立字節數組,而且將C的字符串拷貝進去
	jbyteArray j_byteArray = (*env)->NewByteArray(env, strlen(c_str));
	(*env)->SetByteArrayRegion(env, j_byteArray, 0, strlen(c_str), c_str);
	//2.建立字符集的參數,這裏用Windows的more字符集GB2312
	jstring charsetName = (*env)->NewStringUTF(env, "GB2312");

	//調用
	jstring j_new_str = (*env)->NewObject(env, clz_String, mid, j_byteArray, charsetName);
	return j_new_str;

}
複製代碼

若是以爲個人文字對你有所幫助的話,歡迎關注個人公衆號:

公衆號:Android開發進階

個人羣歡迎你們進來探討各類技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進羣交(♂)流(♀)

相關文章
相關標籤/搜索