在 Android 開發中會有部分場景需要大量計算,因為語言的特性,Java 并不能滿足計算性能,因此需要用到 C/C++ 來處理計算過程。
Android 中為我們提供了 JNI(Java Nactive Interface)以及 NDK (Native Development Kit)來實現 Java 對本地代碼(C/C++)的交互。同時使用 JNI 編譯出來的.so庫可以重復利用,并且比 Java 更難被反編譯破解。
JNI 是 Java 中提供 Java 與 本地代碼通信的接口
NDK 是 Android 的開發工具包,提供快速開發 C 動態庫,并自動打包.so文件到應用中去
本次筆記記錄NDK以下知識點:
- 在 Java 中關聯本地代碼
- JNI 關聯方法命名定義
- JNI 與 Java 對應類型以及簽名
- 在本地代碼中調用 Java 的屬性/方法
- 調用類的構造函數
添加 static 靜態引用
在我們需要使用到 JNI 本地代碼(native)的 Java 文件中添加以下代碼
static {
System.loadLibrary("native-lib");
}
「static」靜態代碼塊,在項目啟動時,虛擬機加載類時會主動加載執行
JNI 方法定義
當我們需要調用 JNI 接口方法處理數據時,我們先需要在 Java 文件中定義所需要的本地方法名
例:
public native String strFromJNI();
注意,此時在本地代碼中需要有一個與之一一對應的本地方法
本地方法名有一下格式要求:
//包名中的 . 需要改成 _ , _ 需要改成 1 (阿拉伯數字1)
JNIEXPORT '返回類型' JNICALL
'方法名(Java_包名_類名_方法名)'(JNIEnv *env, jobject instance)
假設當前我們的包名是
com.example.jni
與之對應的 Java 方法名為
public native String strFromJNI()
則當前 JNI 方法名以及參數表定義應為
JNIEXPORT jstring JNICALL
Java_com_example_jni_strFromJNI(JNIEnv *env, jobject instance)
「JNIEnv」
方法參數表中的「JNIEnv」接口指針用來提供 JNI 中函數的調用
JNIEnv類型是一個指向全部JNI方法的指針。該指針只在創建它的線程有效,不能跨線程傳遞。其聲明如下:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
//C++定義部分
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
//C定義部分
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
從這一部分我們可以看出的是對于 JNIEnv 在 C 語言環境和 C++ 語言環境中的實現是不一樣的。也就是說我們在 C 語言和 C++ 語言中對于 JNI 方法的調用是有區別的。
C 語言環境調用方法如下
(*env)->GetJNIMethod(env, ...);
C++ 語言環境調用方法如下
env->GetJNIMethod(env, ...);
「jobject」
代表當前調用本地方法的實體對象 例如在以下情況
Class clazz = new Class;
clazz.strFromJNI();
jobject 代表的就是上述的 clazz 實體, 可以在 JNI 方法中對其進行操作
以上我們就已經知道了 JNI 中如何定義與 Java 中對應的方法,已經方法參數的作用以及意義
JNI 與 Java 類型對應
在 JNI 中有與 Java 一一對應的所有基本類型以及object對象類型,基本數據類型對應表以及 JNI 中的簽名如下:
「類型簽名」可以理解為方法描述符,在 JNI 調用 Java 屬性或方法時,只知道屬性或方法名是不能夠定位調用的是那個屬性或方法,因此需要「類型簽名」來標示屬性類型,或是參數返回類型以及參數表
Java類型名 | JNI類型名 | JNI簽名 |
---|---|---|
void | void | V |
boolean | jboolean | Z |
byte | jbyte | B |
char | jchar | C |
short | jshort | S |
int | jint | I |
long | jlong | J |
float | jfloat | F |
double | jdouble | D |
引用數據類型(類,對象,數組)對應表以及 JNI 中的簽名如下:
Java類型名 | JNI類型名 | JNI簽名 |
---|---|---|
Object | jobject | Ljava/lang/Object; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
int[] | jintArray | [I |
long[] | jlongArray | [J |
Throwable | jthrowable | Ljava/lang/Throwable; |
以上沒有將所有數組類型列出來,不過我們可以很明顯得找到規律:
如果是數組,其 JNI 類型名稱為 'j'+'java名稱',其 JNI 簽名為 '['+'對應類型的簽名'
如果是類或者對象,其簽名則是 'L' + "類或對象路徑" + ';'
例如,我們在 com.example.jni.bean 路徑下有一個實體類 Bean ,此時 Bean 對應的簽名就是 Lcom/example/jni/bean/Bean; (注意對象簽名結尾分號)
了解了 Java 與 JNI 中的類型對應關系以及類型簽名之后,我們就可以開始使用JNI工作了
調用 Java 屬性/方法
使用 JNI 調用 Java 中的屬性或方法時,需要先通過
//當前持有需要獲取「屬性」或「方法」的對象時,調用這個方法
jclass clazz = env->GetObjectClass(jobject);
或
//當前沒有持有需要操作的對象時,調用這個方法
jclass clazz = env->FindClass('ClassPathName');
其中一個方法獲取到需要操作的 Java 類引用(jclass)
為了方便下面講述調用 Java 屬性/方法,我們先創建一個提供調用的類
package com.example.jni.bean
class Bean{
static{
System.loadLibrary('native_lib')
}
//賦初始值 1
private int variable = 1;
//將 addVal 的值添加到 variable 成員屬性上
private int addVariable(int addVal){
return variable += addVal;
}
//這里只是個普通的 Java 方法,加上 native 關鍵字后與本地代碼中名稱對應的方法關聯
public native int addVariableInNative(int addVal);
}
**** nactive ****
//獲取 jclass
jclass clazz = env->GetObjectClass(jobject);
或
jclass clazz = env->FindClass("com/example/jni/Bean");
以下調用的過程以 已經拿到需要操作的 Java 類引用(現在變量名為 clazz) 為前提
在 JNI 中每個 Java 中的屬性/方法都有對應的 Id,當我們需要操作對應屬性/方法時,需要獲取屬性/方法的Id
調用 Java 屬性
在 JNI 中使用 GetFieldID(jclass, fieldName, sign) 方法來獲取屬性id,使用 jfieldID 來接收,然后使用 GetObjectField(jobject, jfieldid) 方法獲取屬性對象
//獲取 variable 屬性id
jfieldId id = env->GetFieldID(clazz, "variable", "I");
//獲取 variable 屬性對象 GetObjectField 返回的時 jobject 類型, 使用需要轉化成對應的類型
jint var = static_cast<jint>(env->GetObjectField(instance, id));
當我們需要將修改后的值同步到 Java 屬性中時,使用 SetObjectField(jobject, jfieldId, val) 方法來同步值
var = 2;
env->SetObjectField(instance, id, var);
此時在 Java 中調用對象的 variable 屬性值時,它的值就為 2 ;
調用 Java 方法
同理,在 JNI 中調用 Java 方法,需要先用 GetMethodId(jclass, methodName, sign) 方法獲取方法id,使用 jmethodID來接收,然后使用 CallXXMethod(jobject, jmethodId, ...) 方法調用對應的 Java 方法,第三個參數為調用方法的參數值
//獲取 int addVariable(int) 方法id
jmethodID id = env->GetMethodID(clazz, "addVariable", "(I)I")
//調用方法 獲取返回值
int val = env->CallIntMethod(instance, id, 1);
此時返回的 val 值為 variable + 1 = 3
調用類的構造方法
當我們需要使用某個其他類的方法時(例如我們需要使用 Date 下的 getTime 方法時,我們需要先構建一個這個類型的對象,才能去調用對應的方法
我們先來看下如果初始化構建一個對象,在 env 下有一個 jobject NewObject(jclass, initId) 方法,我們需要一個 jclass ,以及一個 構造方法Id。
jclass 我們通過 FindClass(classPath) 方法獲取,并獲取到它的 構造方法Id
jclass clazz = env->FindClass("java/util/Date");
//調用構造方法不關心類名時什么,統一使用 ‘<init>’代表調用的是構造方法
jmethodId initId = env->GetMethodId(clazz, "<init>", "()V");
//生成 Date 對象
jobject date = env->NewObject(clazz, initId);
實體對象創建完成后 接下來只要調用 Java 方法即可
jmethodId getTimeId = env->GetMethodId(clazz, "GetTime", "()J");
//調用 getTime 方法 這里在實際使用時發現 long 類型精度不夠,推薦使用 long long 類型
long long time = env->CallLongMethod(date, getTimeId);
什么時候需要在 本地代碼 中調用 Java 中的方法 ?
比如在本地代碼中需要獲取當前的時間戳,使用 Java 比較容易實現,此時就可以調用 Java 中的實現代碼
需要注意的點
jstring 類型是 jint 與 java 的映射類型,其指向 JVM 內部的字符串,與常規 C 的 char* 字符串類型不同,因此不能直接使用,在 本地代碼 中使用 jstring 類型的變量時,需要使用 env->GetStringUTFChars(jstring, NULL) 轉化成 char* 類型,在被使用完之后調用 ReleaseStringUTFChars(jstring, char*) 釋放字符串