JNI開發系列③C語言呼叫Java欄位與方法

weixin_33861800發表於2016-08-30

接續上篇JNI開發系列②.h標頭檔案分析

前情提要

在前面 , 我們已經熟悉了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方法

// 使用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方法的實現函式 , 並實現

// 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);
}

三 , 輸出

修改前 name 的值:zeno
修改後 name 的值:  xiaojiu and zeno

四 , 分析

首先來分析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標頭檔案的時候 , 已經詳細講述了 。那 , 這兩個函式引數型別jobjectjclass有什麼區別呢 ? 這兩個型別表示 , Java的native函式 , 是成員方法還是類方法 , 成員方法需要物件.方法名 , 類方法則類名.方法名 , 可以在main方法裡面直接使用 。

接下來是:

// 得到jclass jclass就好比java的.class物件
jclass jcls = (*env)->GetObjectClass(env, jobj);

為什麼要得到jclass呢 ?
因為 ,我們要獲取欄位ID , 在JNI中 , 獲取java欄位與方法都需要簽名。而簽名是在類載入的時候完成 , 所以在獲取欄位ID的時候需要傳入jclass 。

// 得到欄位ID

jfieldID jfID = (*env)->GetFieldID(env, jcls, "name", "Ljava/lang/String;");

通過傳入jclass , 欄位名稱 , 欄位簽名 , 就可以得到欄位ID ,也可使用GetMethodID函式得到方法ID 。

為什麼傳入了欄位名稱,還需要簽名呢 ?
因為java支援過載 , 一個方法名稱可以有多個不同實現 , 根據傳入的引數不同 ,所以C語言呼叫函式為了區分不同的方法, 而對每個方法做了簽名 , 而欄位則可用來標識型別 (僅個人理解)。

獲取欄位與函式簽名的方式:

在.class的檔案目錄下 ,使用`javap -s -p className`   就可以列舉出 , 所有的欄位與方法簽名
// 得到欄位的值 , 類比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語言型別不是相通的 , 所有需要一個轉換 , 型別的介紹在上一篇已經詳細說明 。

// 將jstring型別的變數 , 設定到java 欄位中 
// 類比java中的 物件.欄位名得到值 , 這裡是欄位的ID
(*env)->SetObjectField(env, jobj, jfID, new_str);

畫龍點睛:
上述中 , 我們訪問修改了String型別的欄位 , 也基本上能看出訪問欄位的基本套路 , 首先得到jclass , 再得到欄位ID , 繼而得到欄位的值 , 進行型別轉換 , 最後將變化的值設定給Java欄位 。由此可以推出 , 訪問其他型別的欄位 , 也是這樣的套路 , 只不過型別變了 , 值得注意的是 , java中的引用型別是需要進行型別轉換的 。

part 2 : C函式訪問Java方法

一 , 定義Java 方法與呼叫方法的native方法

// C語言呼叫java方法
private native void accessJavaRandomNumberMethod() ;

// 呼叫
jni.accessJavaRandomNumberMethod() ;

二 , 在C語言標頭檔案中定義native方法的實現函式 , 並實現


// 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方法的隨機數:6

四 , 分析

不論是欄位訪問還是方法的呼叫 , 其基本的套路不變 ,呼叫Java方法與呼叫欄位不同的是 ,
將得到欄位ID改成得到方法ID , 得到欄位的值改成呼叫方法`CallXXX` , 通過呼叫呼叫Java方法得到方法返回值 。

part 3 : C函式訪問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

#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型別 。

// 得到jclass 
jclass jclazz = (*env)->GetObjectClass(env, jobj);

步驟二 、得到欄位或方法ID , 區分靜態欄位與物件欄位 , 靜態欄位或方法呼叫(*env)->GetStaticFieldID得到靜態欄位ID ,(*env)->GetStaticMethodID得到靜態方法ID , 物件欄位呼叫(*env)->GetFieldID得到欄位ID,(*env)->GetMethodID得到方法ID 。 可以得到一個套路 , 靜態修飾的 , 則呼叫static標識的函式 , 非靜態的則呼叫常規函式 。

// 得到欄位ID , 物件欄位
 jfieldID jfid = (*env)->GetFieldID(env, jclazz, "age", "I");

// 得到欄位ID , 靜態欄位
jfieldID jfid = (*env)->GetStaticFieldID(env, jclazz, "flag", "Ljava/lang/String;");

步驟三 、 取得欄位的值或呼叫方法 , 需要注意的是, 得到欄位的值與呼叫方法 , 都有型別的區分 。引用型別則使用GetObjectFieldCallStaticObjectMethod , 其他型別 , 則有對於的jxxx型別對應 。套路簡寫:Get<Type>FieldGetStatic<Type>FieldCall<Type>MethodCallStatic<Type>Method

// 得到欄位的值 
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);

結語

真正的高手 , 不是樂而學得的 , 真正的學習 , 不是輕輕鬆鬆的 。高手 , 需要刻意練習 , 刻意練習不是重複相同的動作 , 而是跳出舒適區熟悉區域 , 刻意練習自己不熟悉感覺艱難的事情 。感謝動腦學院

本文由老司機學院【動腦學院】特約提供。

做一家受人尊敬的企業,做一位令人尊敬的老師

參考資料:
Java Native Interface 6.0 Specification

相關文章