在第6章我們學習到了在Native層如何呼叫Java靜態方法和例項方法,其中呼叫例項方法的示例程式碼中也提到了呼叫建構函式來實始化一個物件,但沒有詳細介紹,一帶而過了。還沒有閱讀過的同學請移步《JNI/NDK開發指南(六)——C/C++訪問Java例項方法和靜態方法》閱讀。這章詳細來介紹下初始一個物件的兩種方式,以及如何呼叫子類物件重寫的父類例項方法。
我們先回過一下,在Java中例項化一個物件和呼叫父類例項方法的流程。先看一段程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.study.jnilearn; public class Animal { public void run() { System.out.println("Animal.run..."); } } package com.study.jnilearn; public class Cat extends Animal { @Override public void run() { System.out.println(name + " Cat.run..."); } } public static void main(String[] args) { Animal cat = new Cat("湯姆"); cat.run(); } |
正如你所看到的那樣,上面這段程式碼非常簡單,有兩個類Animal和Cat,Animal類中定義了run和getName兩個方法,Cat繼承自Animal,並重寫了父類的run方法。在main方法中,首先定義了一個Animal型別的變數cat,並指向了Cat類的例項物件,然後呼叫了它的run方法。在執行new Cat(“湯姆”)這段程式碼時,會先為Cat類分配記憶體空間(所分配的記憶體空間大小由Cat類的成員變數數量決定),然後呼叫Cat的帶參構造方法初始化物件。 cat是Animal型別,但它指向的是Cat例項物件的引用,而且Cat重寫了父類的run方法,因為呼叫run方法時有多型存在,所以訪問的是Cat的run而非Animal的run,執行後列印的結果為:湯姆 Cat.run…
如果要呼叫父類的run方法,只需在Cat的run方法中呼叫super.run()即可,相當的簡單。
寫過C或C++的同學應該都有一個很深刻的記憶體管理概念,棧空間和堆空間,棧空間的記憶體大小受作業系統限制,由作業系統自動來管理,速度較快,所以在函式中定義的區域性變數、函式形參變數都儲存在棧空間。作業系統沒有限制堆空間的記憶體大小,只受實體記憶體的限制,記憶體需要程式設計師自己管理。在C語言中用malloc關鍵字動態分配的記憶體和在C++中用new建立的物件所分配記憶體都儲存在堆空間,記憶體使用完之後分別用free或delete/delete[]釋放。這裡不過多的討論C/C++記憶體管理方面的知識,有興趣的同學請自行百度。做Java的童鞋眾所周知,寫Java程式是不需要手動來管理記憶體的,記憶體管理那些煩鎖的事情全都交由一個叫GC的執行緒來管理(當一個物件沒有被其它物件所引用時,該物件就會被GC釋放)。但我覺得Java內部的記憶體管理原理和C/C++是非常相似的,上例中,Animal cat = new Cat(“湯姆”); 區域性變數cat存放在棧空間上,new Cat(“湯姆”);建立的例項物件存放在堆空間,返回一個記憶體地址的引用,儲存在cat變數中。這樣就可以通過cat變數所指向的引用訪問Cat例項當中所有可見的成員了。
所以建立一個物件分為2步:
1. 為物件分配記憶體空間
2. 初始化物件(呼叫物件的構造方法)
下面通過一個示例來了解在JNI中是如何呼叫物件構造方法和父類例項方法的。為了讓示例能清晰的體現構造方法和父類例項方法的呼叫流程,定義了Animal和Cat兩個類,Animal定義了一個String形參的構造方法,一個成員變數name、兩個成員函式run和getName,Cat繼承自Animal,並重寫了run方法。在JNI中實現建立Cat物件的例項,呼叫Animal類的run和getName方法。程式碼如下所示:
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 |
// Animal.java package com.study.jnilearn; public class Animal { protected String name; public Animal(String name) { this.name = name; System.out.println("Animal Construct call..."); } public String getName() { System.out.println("Animal.getName Call..."); return this.name; } public void run() { System.out.println("Animal.run..."); } } // Cat.java package com.study.jnilearn; public class Cat extends Animal { public Cat(String name) { super(name); System.out.println("Cat Construct call...."); } @Override public String getName() { return "My name is " + this.name; } @Override public void run() { System.out.println(name + " Cat.run..."); } } // AccessSuperMethod.java package com.study.jnilearn; public class AccessSuperMethod { public native static void callSuperInstanceMethod(); public static void main(String[] args) { callSuperInstanceMethod(); } static { System.loadLibrary("AccessSuperMethod"); } } |
AccessSuperMethod類是程式的入口,其中定義了一個native方法callSuperInstanceMethod。用javah生成的jni函式原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* Header for class com_study_jnilearn_AccessSuperMethod */ #ifndef _Included_com_study_jnilearn_AccessSuperMethod #define _Included_com_study_jnilearn_AccessSuperMethod #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessSuperMethod * Method: callSuperInstanceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif |
實現Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod函式,如下所示:
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 |
// AccessSuperMethod.c #include "com_study_jnilearn_AccessSuperMethod.h" JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod (JNIEnv *env, jclass cls) { jclass cls_cat; jclass cls_animal; jmethodID mid_cat_init; jmethodID mid_run; jmethodID mid_getName; jstring c_str_name; jobject obj_cat; const char *name = NULL; // 1、獲取Cat類的class引用 cls_cat = (*env)->FindClass(env, "com/study/jnilearn/Cat"); if (cls_cat == NULL) { return; } // 2、獲取Cat的構造方法ID(構造方法的名統一為:<init>) mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V"); if (mid_cat_init == NULL) { return; // 沒有找到只有一個引數為String的構造方法 } // 3、建立一個String物件,作為構造方法的引數 c_str_name = (*env)->NewStringUTF(env, "湯姆貓"); if (c_str_name == NULL) { return; // 建立字串失敗(記憶體不夠) } // 4、建立Cat物件的例項(呼叫物件的構造方法並初始化物件) obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name); if (obj_cat == NULL) { return; } //-------------- 5、呼叫Cat父類Animal的run和getName方法 -------------- cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) { return; } // 例1: 呼叫父類的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id if (mid_run == NULL) { return; } // 注意:obj_cat是Cat的例項,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run); // 例2:呼叫父類的getName方法 // 獲取父類Animal中getName方法的id mid_getName = (*env)->GetMethodID(env, cls_animal, "getName", "()Ljava/lang/String;"); if (mid_getName == NULL) { return; } c_str_name = (*env)->CallNonvirtualObjectMethod(env, obj_cat, cls_animal, mid_getName); name = (*env)->GetStringUTFChars(env, c_str_name, NULL); printf("In C: Animal Name is %s\n", name); // 釋放從java層獲取到的字串所分配的記憶體 (*env)->ReleaseStringUTFChars(env, c_str_name, name); quit: // 刪除區域性引用(jobject或jobject的子類才屬於引用變數),允許VM釋放被區域性變數所引用的資源 (*env)->DeleteLocalRef(env, cls_cat); (*env)->DeleteLocalRef(env, cls_animal); (*env)->DeleteLocalRef(env, c_str_name); (*env)->DeleteLocalRef(env, obj_cat); } |
執行結果:
程式碼講解 – 呼叫構造方法
呼叫構造方法和呼叫物件的例項方法方式是相似的,傳入”< init >”作為方法名查詢類的構造方法ID,然後呼叫JNI函式NewObject呼叫物件的建構函式初始化物件。如下程式碼所示:
1 |
obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name); |
上述這段程式碼呼叫了JNI函式NewObject建立了Class引用的一個例項物件。這個函式做了2件事情,1> 建立一個未初始化的物件並分配記憶體空間 2> 呼叫物件的建構函式初始化物件。這兩步也可以分開進行,為物件分配記憶體,然後再初始化物件,如下程式碼所示:
1 2 3 4 5 6 7 8 9 |
// 1、建立一個未初始化的物件,並分配記憶體 obj_cat = (*env)->AllocObject(env, cls_cat); if (obj_cat) { // 2、呼叫物件的建構函式初始化物件 (*env)->CallNonvirtualVoidMethod(env,obj_cat, cls_cat, mid_cat_init, c_str_name); if ((*env)->ExceptionCheck(env)) { // 檢查異常 goto quit; } } |
AllocObject函式建立的是一個未初始化的物件,後面在用這個物件之前,必須呼叫CallNonvirtualVoidMethod呼叫物件的建構函式初始化該物件。而且在使用時一定要非常小心,確保在一個物件上面,建構函式最多被呼叫一次。有時,先建立一個初始化的物件,然後在合適的時間再呼叫建構函式的方式是很有用的。儘管如此,大部分情況下,應該使用 NewObject,儘量避免使用容易出錯的AllocObject/CallNonvirtualVoidMethod函式。
程式碼講解 – 呼叫父類例項方法
如果一個方法被定義在父類中,在子類中被覆蓋,也可以呼叫父類中的這個例項方法。JNI 提供了一系列函式CallNonvirtualXXXMethod來支援呼叫各種返回值型別的例項方法。呼叫一個定義在父類中的例項方法,須遵循下面的步驟:
1.使用GetMethodID函式從一個指向父類的Class引用當中獲取方法ID
1 2 3 4 5 6 7 8 9 10 |
cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) { return; } //例1: 呼叫父類的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 獲取父類Animal中run方法的id if (mid_run == NULL) { return; } |
2.傳入子類物件、父類Class引用、父類方法 ID 和引數,並呼叫 CallNonvirtualVoidMethod、
CallNonvirtualBooleanMethod、CallNonvirtualIntMethod等一系列函式中的一個。其中CallNonvirtualVoidMethod 也可以被用來呼叫父類的建構函式。
1 2 |
// 注意:obj_cat是Cat的例項,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run); |
其實在開發當中,這種呼叫父類例項方法的情況是很少遇到的,通常在 JAVA 中可以很簡單地做到: super.func();但有些特殊需求也可能會用到,所以知道有這麼回事還是很有必要的。