1. 程式人生 > >Android進階之路(二) -- NDK初探

Android進階之路(二) -- NDK初探

繼續學習NDK開發,今天來實現一個簡單的計算器功能,NativeUtil類中有一個靜態的native方法,它接收三個引數,分別是兩個運算元和一個操作符,並且返回C的計算結果。

NativeUtil類定義如下

public class NativeUtil {
    static {
        System.loadLibrary("native-lib");
    }

    public static int ADD = 0;
    public static int SUB = 1;
    public static int MULTI = 2;
    public static int DIVISION = 3;

    public static native int calculate(int arg0,int arg1,int symbol);
}
其中,ADD,SUB,MULTI,DIVISION,分別表示加減乘除,C中取出arg0和arg1的值,根據symbol和NativeUtil的靜態成員變數對比,判斷進行哪種操作,然後返回計算結果。

在動手之前,我們先要解決幾個問題:
1、C怎麼獲取Java層的資料型別?
2、C怎麼訪問NativeUtil的成員變數?

要解決第一個問題,我們得先了解C與Java資料型別的轉換,轉換表如下:

Java 型別本地型別描述
booleanjbooleanC/C++8位整型
bytejbyteC/C++帶符號的8位整型
charjcharC/C++無符號的16位整型
shortjshortC/C++帶符號的16位整型
intjintC/C++帶符號的32位整型
longjlongC/C++帶符號的64位整型e
floatjfloatC/C++32位浮點型
doublejdoubleC/C++64位浮點型
Objectjobject任何Java物件,或者沒有對應java型別的物件
ClassjclassClass物件
Stringjstring字串物件
Object[]jobjectArray任何物件的陣列
boolean[]jbooleanArray布林型陣列
byte[]jbyteArray位元型陣列
char[]jcharArray字元型陣列
short[]jshortArray短整型陣列
int[]jintArray整型陣列
long[]jlongArray長整型陣列
float[]jfloatArray浮點型陣列
double[]jdoubleArray雙浮點型陣列

Java中int型的資料,在C中就會用jint表示。

還有很關鍵的一點是方法簽名,方法簽名的作用是唯一確定一個方法,因為方法可以有過載,所以僅靠方法的名稱是無法準確定位一個方法的,這個時候就需要方法簽名了,方法簽名實際上就是方法返回值和引數表的組合,它的規則如下:


使用時的形式是(引數型別1引數型別2……)返回型別
比如:
public int calculate(int arg0,int arg1,int symbol);
它的方法簽名就是(III)I

C如何訪問NativeUtil的成員變數呢?
這就需要請出JNI大總管JNIEnv了,JNIEnv負責Java與C的互動,比如從呼叫Java層的方法、訪問Java層的變數,都需要JNIEnv的參與。
大致閱讀原始碼可知JNIEnv在C和C++中分別有不同實現,在C中JNIEnv是JNINativeInterface的一個指標型別,在C++中JNIEnv是一個結構體,它們的共同點是都與JNINativeInterface有關,JNINativeInterface中定義了很多指標,C/C++就通過這些指標呼叫Java層函式,詳細的內容會在後面原始碼部分介紹。
查閱文件可以知道,JNIEnv中有一個GetStaticIntField(jclass clazz, jfieldID fieldID)函式,第一個引數是需要獲取成員變數的類,第二個引數是欄位ID。

欄位ID如何獲取呢?繼續查閱文件,我們又找到了GetStaticFieldID(jclass clazz, const char* name, const char* sig)函式,它的三個引數分別是需要獲取成員變數的類,成員變數名和簽名。

與此相同,JNIEnv中還提供了很多很多獲取變數資訊、獲取方法資訊的函式。


由此,就可以開始編寫C程式碼了,寫出的的程式碼如下:
#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_tustcs_ndktest_NativeUtil_calculate(JNIEnv *env, jclass type, jint arg0, jint arg1,
                                             jint symbol) {
    if(symbol == env->GetStaticIntField(type,env->GetStaticFieldID(type,"ADD","I")))
        return arg0 + arg1;
    else if(symbol == env->GetStaticIntField(type,env->GetStaticFieldID(type,"SUB","I")))
        return arg0 - arg1;
    else if(symbol == env->GetStaticIntField(type,env->GetStaticFieldID(type,"MULTI","I")))
        return arg0 * arg1;
    else if(symbol == env->GetStaticIntField(type,env->GetStaticFieldID(type,"DIVISION","I")))
        return arg0 / arg1;
}

然後在CMakeLists.txt中新增路徑,再去MainActivity呼叫。。嗯,大功告成。


今天附上關於靜態塊和成員變數初始化的小知識:

public class InitTest {
	public static int sInt1 = 10;
	public static int sInt2;
	public int mInt1 = 30;
	public int mInt2;
	//區域性程式碼塊
	{
		if(mInt1 == 30) {
			System.out.println("成員變數mInt1已完成初始化:mInt1 = " + mInt1);
		}
		mInt2 = 40;
		System.out.println("程式碼塊mInt2初始化:mInt2 = " + mInt2);
	}
	//靜態塊
	static{
		sInt2 = 20;
		System.out.println("靜態塊sInt2初始化:sInt2 = " + sInt2);
	}
	public static void print() {
		System.out.println("靜態方法:print()呼叫");
	}	
}
主方法執行:
public static void main(String[] args) {
		InitTest.print();
		//InitTest test = new InitTest(); 
}
在主函式呼叫InitTest的靜態方法,結果如下:


我們可以看到,呼叫類的靜態方法不會呼叫區域性程式碼塊,而會先呼叫靜態塊。

去掉註釋,例項化一個類試試

public static void main(String[] args) {
		//InitTest.print();
		InitTest test = new InitTest(); 
}
執行結果如下:

我們可以看到,例項化一個類時,會優先呼叫靜態塊,再呼叫程式碼塊和給成員變數賦值。

那麼,程式碼塊和給成員變數賦值哪個先執行呢?

答案是從頭開始執行,程式執行到哪裡,就先呼叫哪裡,按順序執行。