通過前面5章的學習,我們知道了如何通過JNI函式來訪問JVM中的基本資料型別、字串和陣列這些資料型別。下一步我們來學習原生程式碼如何與JVM中任意物件的屬性和方法進行互動。比如原生程式碼呼叫Java層某個物件的方法或屬性,也就是通常我們所說的來自C/C++層本地函式的callback(回撥)。這個知識點分2篇文章分別介紹,本篇先介紹方法回撥,在第七章中介紹原生程式碼訪問Java的屬性。
在這之前,先回顧一下在Java中呼叫一個方法時在JVM中的實現原理,有助於下面講解原生程式碼呼叫Java方法實現的機制。寫過Java的童鞋都知道,呼叫一個類的靜態方法,直接通過 類名.方法 就可以呼叫。這也太簡單了,有什麼好講的呢。。。但在這個呼叫過程中,JVM是幫我們做了很多工作的。當我們在執行一個Java程式時,JVM會先將程式執行時所要用到所有相關的class檔案載入到JVM中,並採用按需載入的方式載入,也就是說某個類只有在被用到的時候才會被載入,這樣設計的目的也是為了提高程式的效能和節約記憶體。所以我們在用類名呼叫一個靜態方法之前,JVM首先會判斷該類是否已經載入,如果沒有被ClassLoader載入到JVM中,JVM會從classpath路徑下查詢該類,如果找到了,會將其載入到JVM中,然後才是呼叫該類的靜態方法。如果沒有找到,JVM會丟擲java.lang.ClassNotFoundException異常,提示找不到這個類。ClassLoader是JVM載入class位元組碼檔案的一種機制,不太瞭解的童鞋,請移步閱讀《深入分析Java ClassLoader原理》一文。其實在JNI開發當中,原生程式碼也是按照上面的流程來訪問類的靜態方法或例項方法的,下面通過一個例子,詳細介紹原生程式碼呼叫Java方法流程當中的每個步聚:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.study.jnilearn; /** * AccessMethod.java * 原生程式碼訪問類的例項方法和靜態方法 * @author yangxin */ public class AccessMethod { public static native void callJavaStaticMethod(); public static native void callJavaInstaceMethod(); public static void main(String[] args) { callJavaStaticMethod(); callJavaInstaceMethod(); } static { System.loadLibrary("AccessMethod"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.study.jnilearn; /** * ClassMethod.java * 用於原生程式碼呼叫 * @author yangxin */ public class ClassMethod { private static void callStaticMethod(String str, int i) { System.out.format("ClassMethod::callStaticMethod called!-->str=%s," + " i=%d\n", str, i); } private void callInstanceMethod(String str, int i) { System.out.format("ClassMethod::callInstanceMethod called!-->str=%s, " + "i=%d\n", str, i); } } |
由AccessMethod.class生成的標頭檔案:
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_AccessMethod */ #ifndef _Included_com_study_jnilearn_AccessMethod #define _Included_com_study_jnilearn_AccessMethod #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaStaticMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *, jclass); /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaInstaceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif |
原生程式碼對標頭檔案中函式原型的實現:
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 |
// AccessMethod.c #include "com_study_jnilearn_AccessMethod.h" /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaStaticMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *env, jclass cls) { jclass clazz = NULL; jstring str_arg = NULL; jmethodID mid_static_method; // 1、從classpath路徑下搜尋ClassMethod這個類,並返回該類的Class物件 clazz =(*env)->FindClass(env,"com/study/jnilearn/ClassMethod"); if (clazz == NULL) { return; } // 2、從clazz類中查詢callStaticMethod方法 mid_static_method = (*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V"); if (mid_static_method == NULL) { printf("找不到callStaticMethod這個靜態方法。"); return; } // 3、呼叫clazz類的callStaticMethod靜態方法 str_arg = (*env)->NewStringUTF(env,"我是靜態方法"); (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); // 刪除區域性引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,str_arg); } /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaInstaceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *env, jclass cls) { jclass clazz = NULL; jobject jobj = NULL; jmethodID mid_construct = NULL; jmethodID mid_instance = NULL; jstring str_arg = NULL; // 1、從classpath路徑下搜尋ClassMethod這個類,並返回該類的Class物件 clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod"); if (clazz == NULL) { printf("找不到'com.study.jnilearn.ClassMethod'這個類"); return; } // 2、獲取類的預設構造方法ID mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V"); if (mid_construct == NULL) { printf("找不到預設的構造方法"); return; } // 3、查詢例項方法的ID mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V"); if (mid_instance == NULL) { return; } // 4、建立該類的例項 jobj = (*env)->NewObject(env,clazz,mid_construct); if (jobj == NULL) { printf("在com.study.jnilearn.ClassMethod類中找不到callInstanceMethod方法"); return; } // 5、呼叫物件的例項方法 str_arg = (*env)->NewStringUTF(env,"我是例項方法"); (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); // 刪除區域性引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,jobj); (*env)->DeleteLocalRef(env,str_arg); |
執行結果:
程式碼解析:
AccessMethod.java是程式的入口,在main方法中,分別呼叫了callJavaStaticMethod和callJavaInstaceMethod這兩個native方法,用於測試native層呼叫MethodClass.java中的callStaticMethod靜態方法和callInstanceMethod例項方法,這兩個方法的返回值都為Void,引數都有兩個,分別為String和int
一、callJavaStaticMethod靜態方法實現說明
1 2 |
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *env, jclass cls) |
定位到AccessMethod.c的31行:
1 |
(*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); |
CallStaticVoidMethod函式的原型如下:
1 |
void (JNICALL *CallStaticVoidMethod)(JNIEnv *env, jclass cls, jmethodID methodID, ...); |
該函式接收4個引數:
- env:JNI函式表指標
- cls:呼叫該靜態方法的Class物件
- methodID:方法ID(因為一個類中會存在多個方法,需要一個唯一標識來確定呼叫類中的哪個方法)
- 引數4:方法實參列表
根據函式引數的提示,分以下四步完成Java靜態方法的回撥:
第一步:呼叫FindClass函式,傳入一個Class描述符,JVM會從classpath路徑下搜尋該類,並返回jclass型別(用於儲存Class物件的引用)。注意ClassMethod的Class描述符為com/study/jnilearn/ClassMethod,要將.(點)全部換成/(反斜槓)
1 |
(*env)->FindClass(env,"com/study/jnilearn/ClassMethod"); |
第二步:呼叫GetStaticMethodID函式,從ClassMethod類中獲取callStaticMethod方法ID,返回jmethodID型別(用於儲存方法的引用)。實參clazz是第一步找到的jclass物件,實參”callStaticMethod”為方法名稱,實參“(Ljava/lang/String;I)V”為方法的簽名
1 |
(*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V"); |
第三步:呼叫CallStaticVoidMethod函式,執行ClassMethod.callStaticMethod方法呼叫。str_arg和100是callStaticMethod方法的實參。
1 2 |
str_arg = (*env)->NewStringUTF(env,"我是靜態方法"); (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); |
注意:JVM針對所有資料型別的返回值都定義了相關的函式。上面callStaticMethod方法的返回型別為Void,所以呼叫CallStaticVoidMethod。根據返回值型別不同,JNI提供了一系列不同返回值的函式,如:CallStaticIntMethod、CallStaticFloatMethod、CallStaticShortMethod、CallStaticObjectMethod等,分別表示呼叫返回值為int、float、short、Object型別的函式,引用型別統一呼叫CallStaticObjectMethod函式。另外,每種返回值型別的函式都提供了接收3種實參型別的實現:CallStaticXXXMethod(env, clazz, methodID, …),CallStaticXXXMethodV(env, clazz, methodID, va_list args),CallStaticXXXMethodA(env, clazz, methodID, const jvalue *args),分別表示:接收可變引數列表、接收va_list作為實參和接收const jvalue*為實參。下面是jni.h標頭檔案中CallStaticVoidMethod的三種實參的函式原型:
1 2 3 4 5 6 |
void (JNICALL *CallStaticVoidMethod) (JNIEnv *env, jclass cls, jmethodID methodID, ...); void (JNICALL *CallStaticVoidMethodV) (JNIEnv *env, jclass cls, jmethodID methodID, va_list args); void (JNICALL *CallStaticVoidMethodA) (JNIEnv *env, jclass cls, jmethodID methodID, const jvalue * args); |
第四步、釋放區域性變數
1 2 3 |
// 刪除區域性引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,str_arg); |
雖然函式結束後,JVM會自動釋放所有區域性引用變數所佔的記憶體空間。但還是手動釋放一下比較安全,因為在JVM中維護著一個引用表,用於儲存區域性和全域性引用變數,經測試在Android NDK環境下,這個表的最大儲存空間是512個引用,如果超過這個數就會造成引用表溢位,JVM崩潰。在PC環境下測試,不管申請多少區域性引用也不釋放都不會崩,我猜可能與JVM和Android Dalvik虛擬機器實現方式不一樣的原因。所以有申請就及時釋放是一個好的習慣!(區域性引用和全域性引用在後面的文章中會詳細介紹)
二、callInstanceMethod例項方法實現說明
1 2 |
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *env, jclass cls) |
定位到AccessMethod.c的43行:
1 |
(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); |
CallVoidMethod函式的原型如下:
1 |
void (JNICALL *CallVoidMethod) (JNIEnv *env, jobject obj, jmethodID methodID, ...); |
該函式接收4個引數:
- env:JNI函式表指標
- obj:呼叫該方法的例項
- methodID:方法ID
- 引數4:方法的實參列表
根據函式引數的提示,分以下六步完成Java靜態方法的回撥:
第一步、同呼叫靜態方法一樣,首先通過FindClass函式獲取類的Class物件
第二步、獲取類的構造方法ID,因為建立類的物件首先會呼叫類的構造方法。這裡以預設構造方法為例
1 |
(*env)->GetMethodID(env,clazz, "<init>","()V"); |
<init>代表類的構造方法名稱,()V代表無參無返回值的構造方法(即預設構造方法)
第三步、呼叫GetMethodID獲取callInstanceMethod的方法ID
1 |
(*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V"); |
第四步、呼叫NewObject函式,建立類的例項物件
1 |
<span style="font-size:18px;">(*env)->NewObject(env,clazz,mid_construct);</span> |
第五步、呼叫CallVoidMethod函式,執行ClassMethod.callInstanceMethod方法呼叫,str_arg和200是方法實參
1 2 |
str_arg = (*env)->NewStringUTF(env,"我是例項方法"); (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); |
同JNI呼叫Java靜態方法一樣,JVM針對所有資料型別的返回值都定義了相關的函式(CallXXXMethod),如:CallIntMethod、CallFloatMethod、CallObjectMethod等,也同樣提供了支援三種型別實參的函式實現,以CallVoidMethod為例,如下是jni.h標頭檔案中該函式的原型:
1 2 3 |
void (JNICALL *CallVoidMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); void (JNICALL *CallVoidMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); void (JNICALL *CallVoidMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args); |
第六步、刪除區域性引用(從引用表中移除)
1 2 3 4 |
// 刪除區域性引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,jobj); (*env)->DeleteLocalRef(env,str_arg); |
三、方法簽名
在上面的的例子中,無論是呼叫靜態方法還是例項方法,都必須傳入一個jmethodID的引數。因為在Java中存在方法過載(方法名相同,引數列表不同),所以要明確告訴JVM呼叫的是類或例項中的哪一個方法。呼叫JNI的GetMethodID函式獲取一個jmethodID時,需要傳入一個方法名稱和方法簽名,方法名稱就是在Java中定義的方法名,方法簽名的格式為:(形參引數型別列表)返回值。形參引數列表中,引用型別以L開頭,後面緊跟類的全路徑名(需將.全部替換成/),以分號結尾。下面是一些示例:
Java基本型別與方法簽名中引數型別和返回值型別的對映關係如下:
比如,String fun(int a, float b, boolean c, String d) 對應的JNI方法簽名為:”(IFZLjava/lang/String;)Ljava/lang/String;”
總結:
1、呼叫靜態方法使用CallStaticXXXMethod/V/A函式,XXX代表返回值的資料型別。如:CallStaticIntMethod
2、呼叫例項方法使用CallXXXMethod/V/A函式,XXX代表返回的資料型別,如:CallIntMethod
3、獲取一個例項方法的ID,使用GetMethodID函式,傳入方法名稱和方法簽名
4、獲以一個靜態方法的ID,使用GetStaticMethodID函式,傳入方法名稱和方法簽名
5、獲取構造方法ID,方法名稱使用”<init>”
6、獲取一個類的Class例項,使用FindClass函式,傳入類描述符。JVM會從classpath目錄下開始搜尋。
7、建立一個類的例項,使用NewObject函式,傳入Class引用和構造方法ID
8、刪除區域性變數引用,使用DeleteLocalRef,傳入引用變數
9、方法簽名格式:(形參引數列表)返回值型別。注意:形參引數列表之間不需要用空格或其它字元分隔
10、類描述符格式:L包名路徑/類名;,包名之間用/分隔。如:Ljava/lang/String;
11、呼叫GetMethodID獲取方法ID和呼叫FindClass獲取Class例項後,要做異常判斷
示例程式碼下載地址:https://code.csdn.net/xyang81/jnilearn