NDK 學習筆記(一)

在 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以下知識點

  1. 在 Java 中關聯本地代碼
    • JNI 關聯方法命名定義
    • JNI 與 Java 對應類型以及簽名
  2. 在本地代碼中調用 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*) 釋放字符串

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容