JNI/NDK開發指南(八)——調用構造方法和父類執行個體方法,jnindk
轉載請註明出處:http://blog.csdn.net/xyang81/article/details/44002089
在第6章我們學習到了在Native層如何調用Java靜態方法和執行個體方法,其中調用執行個體方法的範例程式碼中也提到了調用建構函式來實始化一個對象,但沒有詳細介紹,一帶而過了。還沒有閱讀過的同學請移步《JNI/NDK開發指南(六)——C/C++訪問Java執行個體方法和靜態方法》閱讀。這章詳細來介紹下初始一個對象的兩種方式,以及如何調用子類對象重寫的父類執行個體方法。
我們先回過一下,在Java中執行個體化一個對象和調用父類執行個體方法的流程。先看一段代碼:
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方法。代碼如下所示:
// Animal.javapackage 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.javapackage 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.javapackage 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函數原型如下:
/* Header for class com_study_jnilearn_AccessSuperMethod */#ifndef _Included_com_study_jnilearn_AccessSuperMethod#define _Included_com_study_jnilearn_AccessSuperMethod#ifdef __cplusplusextern "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函數,如下所示:
// 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調用對象的建構函式初始化對象。如下代碼所示:
obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name);
上述這段代碼調用了JNI函數NewObject建立了Class引用的一個執行個體對象。這個函數做了2件事情,1> 建立一個未初始化的對象並分配記憶體空間 2> 調用對象的建構函式初始化對象。這兩步也可以分開進行,為對象分配記憶體,然後再初始化對象,如下代碼所示:
// 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
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方法的idif (mid_run == NULL) { return;}
2.傳入子類對象、父類Class引用、父類方法 ID 和參數,並調用 CallNonvirtualVoidMethod、
CallNonvirtualBooleanMethod、CallNonvirtualIntMethod等一系列函數中的一個。其中CallNonvirtualVoidMethod 也可以被用來調用父類的建構函式。
// 注意:obj_cat是Cat的執行個體,cls_animal是Animal的Class引用,mid_run是Animal類中的方法ID(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);
其實在開發當中,這種調用父類執行個體方法的情況是很少遇到的,通常在 JAVA 中可以很簡單地做到: super.func();但有些特殊需求也可能會用到,所以知道有這麼回事還是很有必要的。
範例程式碼:https://code.csdn.net/xyang81/jnilearn