接續上篇[JNI開發系列②.h頭文件分析](http://www.jianshu.com/p/cba836f6a08c)
### 前情提要
在前面 , 我們已經熟悉了JNI的開發流程 , .h頭文件的分析 , 生成頭文件`javah`命令 , 以及java類型在C語言中的表現形式 , 值得注意的是 , java中的所有引用類型都是`jobject`類型 , `native`生成的函數 , 以`Java_全類名_方法名`表示,包名的`.`以`_`表示 。
### 概述
在開篇的時候 ,我們就使用java的`native`方法調用過C函數 , 返回了一個String類型的字符串 , 使用`(*Env)->NewStringUTF(Env, "Jni C String");`函數 , 我們將字符指針轉換成`jstring` , java類型的字符串返回給了我們的java層 。今天我們來學習 , 使用C語言來調用Java的字段與方法 。
### part 1 : C 函數訪問java字段
>一 , 定義Java 的`String`類型字段與修改字段的`native`方法
```java
// 使用C語言修改java字段
public String name = "zeno" ;
// C語言修改java String 類型字段本地方法
public native void accessJavaStringField() ;
// 調用
HelloJni jni = new HelloJni() ;
System.out.println("修改前 name 的值:"+jni.name);
//C語言修改java字段本地方法
jni.accessJavaStringField();
System.out.println("修改后 name 的值:"+jni.name);
```
> 二 , 在C語言頭文件中定義`native`方法的實現函數 , 并實現
```c
// com.zeno.jni_HelloJNI.h
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *, jobject);
// Hello_JNI.c
/*C語言訪問java String類型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jcls = (*env)->GetObjectClass(env, jobj);
// 得到字段ID
jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");
// 得到字段的值
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);
// 將jstring類型轉換成字符指針
char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
//printf("is vaule:%s\n", cstr);
// 拼接字符
char text[30] = " xiaojiu and ";
strcat(text, cstr);
//printf("modify value %s\n", text);
// 將字符指針轉換成jstring類型
jstring new_str = (*env)->NewStringUTF(env, text);
// 將jstring類型的變量 , 設置到java 字段中
(*env)->SetObjectField(env, jobj, jfID, new_str);
}
```
> 三 , 輸出
```java
修改前 name 的值:zeno
修改后 name 的值: xiaojiu and zeno
```
> 四 , 分析
首先來分析C函數:
```c
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj)
JNIEXPORT jstring JNICALL Java_com_zeno_jni_HelloJni_getStringFromC
(JNIEnv *Env, jclass jclazz)
```
我們可以看出兩處不同 , 一處是返回值類型 , 一處是函數參數類型 ,返回值類型沒什么可講的 , 在分析.h頭文件的時候 , 已經詳細講述了 。那 , 這兩個函數參數類型`jobject`與`jclass`有什么區別呢 ? 這兩個類型表示 , Java的`native`函數 , 是成員方法還是類方法 , 成員方法需要對象.方法名 , 類方法則類名.方法名 , 可以在main方法里面直接使用 。
接下來是:
```c
// 得到jclass jclass就好比java的.class對象
jclass jcls = (*env)->GetObjectClass(env, jobj);
```
為什么要得到jclass呢 ?
因為 ,我們要獲取字段ID , 在JNI中 , 獲取java字段與方法都需要簽名。而簽名是在類加載的時候完成 , 所以在獲取字段ID的時候需要傳入jclass 。
// 得到字段ID
```c
jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");
```
通過傳入jclass , 字段名稱 , 字段簽名 , 就可以得到字段ID ,也可使用GetMethodID函數得到方法ID 。
為什么傳入了字段名稱,還需要簽名呢 ?
因為java支持重載 , 一個方法名稱可以有多個不同實現 , 根據傳入的參數不同 ,所以C語言調用函數為了區分不同的方法, 而對每個方法做了簽名 , 而字段則可用來標識類型 (僅個人理解)。
獲取字段與函數簽名的方式:
```
在.class的文件目錄下 ,使用`javap -s -p className` 就可以列舉出 , 所有的字段與方法簽名
```
```c
// 得到字段的值 , 類比java中的 對象.字段名得到值 , 這里是字段的ID
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);
// 將jstring類型轉換成字符指針
char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
//printf("is vaule:%s\n", cstr);
// 拼接字符 char text[30] = " xiaojiu and "; strcat(text, cstr);
// 將字符指針轉換成jstring類型
jstring new_str = (*env)->NewStringUTF(env, text);
```
因為java類型與C語言類型不是相通的 , 所有需要一個轉換 , 類型的介紹在上一篇已經詳細說明 。
```c
// 將jstring類型的變量 , 設置到java 字段中
// 類比java中的 對象.字段名得到值 , 這里是字段的ID
(*env)->SetObjectField(env, jobj, jfID, new_str);
```
> 畫龍點睛:
上述中 , 我們訪問修改了String類型的字段 , 也基本上能看出訪問字段的基本套路 , 首先得到jclass , 再得到字段ID , 繼而得到字段的值 , 進行類型轉換 , 最后將變化的值設置給Java字段 。由此可以推出 , 訪問其他類型的字段 , 也是這樣的套路 , 只不過類型變了 , 值得注意的是 , java中的引用類型是需要進行類型轉換的 。
### part 2 : C函數訪問Java方法
> 一 , ?定義Java 方法與調用方法的native方法
```java
// C語言調用java方法
private native void accessJavaRandomNumberMethod() ;
// 調用
jni.accessJavaRandomNumberMethod() ;
```
> 二 , ?在C語言頭文件中定義native方法的實現函數 , 并實現
```c
// com.zeno.jni_HelloJNI.h
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *, jobject);
// Hello_JNI.c
// 訪問java方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
// 得到方法ID
jmethodID jmtdId = (*env)->GetMethodID(env, jclazz, "getRandomNumber", "(I)I");
// 調用方法
jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);
// 打印
printf("得到java方法的隨機數:%ld\n", jRandomNum);
}
```
> 三 , 輸出
```java
得到java方法的隨機數:6
```
> 四 , 分析
```
不論是字段訪問還是方法的調用 , 其基本的套路不變 ,調用Java方法與調用字段不同的是 ,
將得到字段ID改成得到方法ID , 得到字段的值改成調用方法`CallXXX` , 通過調用調用Java方法得到方法返回值 。
```
### part 3 : C函數訪問Java字段與方法(靜態)
> 套路都是一樣的 , 這里僅給出代碼 , 不做詳細分析(本階段全部代碼) 。
```java
/**
*
* @author Zeno
*
* JNI (Java Native Interface) java本地化接口
*
* Android Framework層與Native層相互通信的基石
*
*
*/
public class HelloJni {
// 使用C語言修改java字段
public String name = "zeno" ;
// 使用C語言修改java int 類型字段
private int age = 20 ;
public static String flag = "flag1" ;
// 調用C語言函數方法
public static native String getStringFromC() ;
// 調用C++語言函數方法
public static native String getStringFromCPP() ;
// C語言修改java String 類型字段本地方法
public native void accessJavaStringField() ;
// C語言修改java String static 類型字段本地方法
public native void accessJavaStaticStringField() ;
// C語言修改java int 類型字段本地方法
public native void accessJavaIntField() ;
// C語言調用java方法
private native void accessJavaRandomNumberMethod() ;
// 調用Java靜態方法
private native void accessJavaStaticMethod() ;
// 靜態native方法訪問字段
private static native void staticAccessJavaField() ;
public static void main(String[] args) {
System.out.println("getStringFormC == "+getStringFromC());
System.out.println("getStringFormC == "+getStringFromCPP());
HelloJni jni = new HelloJni() ;
System.out.println("修改前 name 的值:"+jni.name);
//C語言修改java字段本地方法
jni.accessJavaStringField();
System.out.println("修改后 name 的值:"+jni.name);
System.out.println("修改前 flag 的值:"+flag);
//C語言修改java static 字段本地方法
jni.accessJavaStaticStringField();
System.out.println("修改后 flag 的值:"+flag);
System.out.println("修改前 age 的值:"+jni.age);
//C語言修改java字段本地方法
jni.accessJavaIntField();
System.out.println("修改后 age 的值:"+jni.age);
jni.accessJavaRandomNumberMethod() ;
jni.accessJavaStaticMethod() ;
// 靜態native方法 ,訪問java字段
System.out.println("修改前 flag 的值:"+flag);
staticAccessJavaField();
System.out.println("修改后 flag 的值:"+flag);
}
static{
// 加載動態庫
System.loadLibrary("Hello_JNI") ;
}
// 調用java方法
private int getRandomNumber(int bound) {
return new Random().nextInt(bound) ;
}
// 調用java靜態方法
private static String getUUID() {
return UUID.randomUUID().toString();
}
}
```
> C實現 , 這里就不貼頭文件了 。
調用靜態的Java字段與方法 , 在C語言中調用相應的static函數 , 例如:獲取靜態字段ID `GetStaticFieldID` 。
```c
#define _CRT_SECURE_NO_WARNINGS
#include "com_zeno_jni_HelloJni.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
/*
C/C++動態庫 , 在win平臺下以.dll文件標識 , 在linux下面以.so文件表示
在Android中 , 以.so文件表示 , 因為Android使用的是linux內核 。
*/
/*
* Class: com_zeno_jni_HelloJni
* Method: getStringFormC
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_zeno_jni_HelloJni_getStringFromC
(JNIEnv *Env, jclass jclazz) {
return (*Env)->NewStringUTF(Env, "Jni C String");
}
/*C語言訪問java String類型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField
(JNIEnv *env, jobject jobj) {
// 得到jclass , jclass就好比java的.class對象
jclass jcls = (*env)->GetObjectClass(env, jobj);
// 得到字段ID ,
jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");
// 得到字段的值
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);
// 將jstring類型轉換成字符指針
char* cstr = (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
//printf("is vaule:%s\n", cstr);
// 拼接字符
char text[30] = " xiaojiu and ";
strcat(text, cstr);
//printf("modify value %s\n", text);
// 將字符指針轉換成jstring類型
jstring new_str = (*env)->NewStringUTF(env, text);
// 將jstring類型的變量 , 設置到java 字段中
(*env)->SetObjectField(env, jobj, jfID, new_str);
}
/*C語言訪問java int 類型字段*/
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaIntField
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
// 得到字段ID
jfieldID jfid = (*env)->GetFieldID(env, jclazz, "age", "I");
// 得到字段值
jint jAge = (*env)->GetIntField(env, jobj, jfid);
jAge++;
(*env)->SetIntField(env, jobj, jfid, jAge);
}
// 訪問java方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaRandomNumberMethod
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
// 得到方法ID
jmethodID jmtdId = (*env)->GetMethodID(env, jclazz, "getRandomNumber", "(I)I");
// 調用方法
jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);
// 打印
printf("得到java方法的隨機數:%ld\n", jRandomNum);
}
// 訪問java靜態字段
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStaticStringField
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
// 得到字段ID
jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");
// 得到字段的值
jobject jFLagStr = (*env)->GetStaticObjectField(env, jclazz, jfid);
// 將java字符串轉換成C字符指針
char* cFlagStr = (*env)->GetStringUTFChars(env, jFLagStr, JNI_FALSE);
//printf("is vaule:%s\n", cFlagStr);
char newStr[30] = " access static field ";
strcat(newStr, cFlagStr);
// 將C字符指針 , 轉換成java字符串
jstring jNewStr = (*env)->NewStringUTF(env, newStr);
// 將字符串設置到java字段上
(*env)->SetStaticObjectField(env, jclazz, jfid, jNewStr);
}
// 訪問java靜態方法
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_accessJavaStaticMethod
(JNIEnv *env, jobject jobj) {
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
// 得到靜態方法ID
jmethodID mtdid = (*env)->GetStaticMethodID(env, jclazz, "getUUID", "()Ljava/lang/String;");
// 調用靜態方法
jobject jUUIDStr = (*env)->CallStaticObjectMethod(env, jclazz, mtdid);
// 將java字符串轉換成C字符指針
char* cUUIDStr = (*env)->GetStringUTFChars(env, jUUIDStr, JNI_FALSE);
printf("is vaule:%s\n", cUUIDStr);
// 根據UUID生成臨時文件
char file_path[100] ;
sprintf(file_path, "e:\\dn\\%s.txt", cUUIDStr);
printf("is address:%s\n", file_path);
FILE* fp = fopen(file_path, "w");
if (fp == NULL) {
printf("文件創建失敗\n");
}
char* content = "落花有意流水無情";
// 寫入內容
fputs(content, fp);
// 關閉流
fclose(fp);
}
// 靜態native方法 , 訪問java字段
JNIEXPORT void JNICALL Java_com_zeno_jni_HelloJni_staticAccessJavaField
(JNIEnv *env, jclass jclazz) {
// 得到字段ID
jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");
// 得到字段的值
jstring jflag = (*env)->GetStaticObjectField(env, jclazz, jfid);
// 將java字符串轉換成字符指針
char* cXj = (*env)->GetStringUTFChars(env, jflag, JNI_FALSE);
printf("is value:%s\n", cflag);
char newName[100] = "xiaojiu love ";
char* cNewName = strcat(newName, cflag);
// 將字符指針轉換成java類型
jstring newStr = (*env)->NewStringUTF(env, cNewName);
// 設置
(*env)->SetStaticObjectField(env, jclazz, jfid, newStr);
}
```
### 編寫套路
C語言訪問Java語言的字段與方法 , 只要理解了一種 , 其他的都是套路 , 根據步驟一步一步來就可以了 。
> 步驟一 、 得到`jclass` , 字節碼對象 , 如果是`static native`修飾 , 則函數會以`jclass`類型傳入 , 非靜態則需要得到`jclass`類型 。
```c
// 得到jclass
jclass jclazz = (*env)->GetObjectClass(env, jobj);
```
> 步驟二 、得到字段或方法ID , 區分靜態字段與對象字段 , 靜態字段或方法調用`(*env)->GetStaticFieldID`,`(*env)->GetMethodID`函數得到ID , 對象字段調用`(*env)->GetFieldID`,`(*env)->GetStaticMethodID`得到ID 。 可以得到一個套路 , 靜態修飾的 , 則調用`static`標識的函數 , 非靜態的則調用常規函數 。
```c
// 得到字段ID , 對象字段
jfieldID jfid = (*env)->GetFieldID(env, jclazz, "age", "I");
// 得到字段ID , 靜態字段
jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");
```
> 步驟三 、 取得字段的值或調用方法 , 需要注意的是, 得到字段的值與調用方法 , 都有類型的區分 。引用類型則使用`GetObjectField` , `CallStaticObjectMethod` , 其他類型 , 則有對于的jxxx類型對應 。套路簡寫:`Get<Type>Field` , `GetStatic<Type>Field` , `Call<Type>Method` , `CallStatic<Type>Method` 。
```c
// 得到字段的值
jstring jstr = (*env)->GetObjectField(env, jobj, jfID);
// 得到字段值
jint jAge = (*env)->GetIntField(env, jobj, jfid);
// 調用方法
jint jRandomNum = (*env)->CallIntMethod(env, jobj, jmtdId, 10);
// 調用靜態方法
jobject jUUIDStr = (*env)->CallStaticObjectMethod(env, jclazz, mtdid);
```
> 步驟四 、 類型轉換 , 如果是Java引用類型 , 則需要進行類型轉換
```
// 將java字符串轉換成字符指針
char* cflag = (*env)->GetStringUTFChars(env, jflag, JNI_FALSE);
```
### 結語
真正的高手 , 不是樂而學得的 , 真正的學習 , 不是輕輕松松的 。高手 , 需要刻意練習 , 刻意練習不是重復相同的動作 , 而是跳出舒適區熟悉區域 , 刻意練習自己不熟悉感覺艱難的事情 。感謝[動腦學院 ](http://www.dongnaoedu.com/)。
### 本文由老司機學院【[動腦學院](http://www.dongnaoedu.com/)】特約提供。
做一家受人尊敬的企業,做一位令人尊敬的老師
參考資料:
[Java Native Interface 6.0 Specification](http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html)
- 簡介
- C語言基礎及指針①語法基礎
- C語言基礎及指針②之指針內存分析
- C語言基礎及指針③函數與二級指針
- C語言基礎及指針④函數指針
- C語言基礎及指針⑤動態內存分配
- C語言基礎及指針⑥字符操作
- C語言基礎及指針⑦結構體與指針
- C語言基礎及指針⑧文件IO
- C語言基礎及指針⑨聯合體與枚舉
- C語言基礎及指針⑩預編譯及jni.h分析
- JNI開發系列①JNI概念及開發流程
- JNI開發系列②.h頭文件分析
- JNI開發系列③C語言調用Java字段與方法
- JNI開發系列④C語言調用構造方法
- JNI開發系列⑤對象引用的處理
- NDK開發基礎①使用Android Studio編寫NDK
- NDK開發基礎②文件加密解密與分割合并
- NDK開發基礎③增量更新之服務器端生成差分包
- C++基礎①命名空間結構體和引用