Android NDK開發之JNI基礎

前言

之前寫了一篇文章簡單的介紹了Android NDK的組件和結構,以及在Android studio中開發NDK,NDK是Android底層的c/c++庫,然而要在java中調用c/c++的原生功能,則需要使用JNI來實現。

什麼是JNI

JNI(Java Native Interface)是java本地接口,它主要是爲了實現Java調用c、c++等本地代碼所封裝的一層接口。大家都知道java是跨平臺開發語言,它的狂平臺特性導致與本地交互的能力不夠強大,一些和操作系統相關的特性Java無法完成,所以Java提供了JNI用於和Native代碼進行交互。通過JNI,Java可以調用c、c++,相反,c、c++也可以調用Java的相關代碼。

創建NDK工程

開發環境

  • Mac
  • Android studio:3.3.2

新建工程

本地的Android studio版本爲3.3.2,當你創建項目的時候有一個選項是選擇Native C++的模板

點擊next,配置項目的信息

點擊next,選擇使用哪種C++標準,選擇Toolchain Default會使用默認的CMake設置即可。

點擊finish即可完成工程的創建。

工程結構

這時候主工程目錄下會有cpp文件夾和.externalNativeBuild文件夾。

.externalNativeBuild文件夾:用於存放cmake編譯好的文件,包括支持的各種硬件等信息,有點類似於build.gradle文件明確Gradle如何編譯APP;
cpp文件夾:存放C/C++代碼文件,native-lib.cpp文件默認生成的;

cpp文件夾下有兩個文件,一個是native-lib.cpp文件,一個是CMakeLists.txt文件。CMakeLists.txt文件是cmake腳本配置文件,cmake會根據該腳本文件中的指令去編譯相關的C/C++源文件,並將編譯後產物生成共享庫或靜態塊,然後Gradle將其打包到APK中。

CMakeLists.txt的相關配置如下:

# 設置構建本地庫所需的最小版本的cbuild。
cmake_minimum_required(VERSION 3.4.1)
# 創建並命名一個庫,將其設置爲靜態
# 或者共享,並提供其源代碼的相對路徑。
# 您可以定義多個庫,而cbuild爲您構建它們。
# Gradle自動將共享庫與你的APK打包。
add_library( native-lib       #設置庫的名稱。即SO文件的名稱,生產的so文件爲“libnative-lib.so”,                                在加載的時候“System.loadLibrary("native-lib");”
             SHARED            # 將庫設置爲共享庫。
             native-lib.cpp    # 提供一個源文件的相對路徑
             helloJni.cpp      # 提供同一個SO文件中的另一個源文件的相對路徑
           )
# 搜索指定的預構建庫,並將該路徑存儲爲一個變量。因爲cbuild默認包含了搜索路徑中的系統庫,所以您只需要指定您想要添加的公共NDK庫的名稱。cbuild在完成構建之前驗證這個庫是否存在。
find_library(log-lib   # 設置path變量的名稱。
             log       #  指定NDK庫的名稱 你想讓CMake來定位。
             )
#指定庫的庫應該鏈接到你的目標庫。您可以鏈接多個庫,比如在這個構建腳本中定義的庫、預構建的第三方庫或系統庫。
target_link_libraries( native-lib    # 指定目標庫中。與 add_library的庫名稱一定要相同
                       ${log-lib}    # 將目標庫鏈接到日誌庫包含在NDK。
                       )
#如果需要生產多個SO文件的話,寫法如下
add_library( natave-lib       # 設置庫的名稱。另一個so文件的名稱
             SHARED           # 將庫設置爲共享庫。
             nataveJni.cpp    # 提供一個源文件的相對路徑
            )
target_link_libraries( natave-lib     #指定目標庫中。與 add_library的庫名稱一定要相同
                       ${log-lib}     # 將目標庫鏈接到日誌庫包含在NDK。
                        )     

build.gradle中有CMake的相關配置

代碼結構

java調用c、c++代碼分爲三個步驟:

  1. 加載so庫
  2. 編寫java函數
  3. 編寫c函數

在MainActivity.java,static{}語句中使用了加載so庫,此語句在類加載中只執行一次。

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

然後,編寫了原生的函數,函數名中要帶有native。

public native String stringFromJNI();

最後,編寫相對應的c函數,注意函數名的構成Java_com_example_myapplication_MainActivity_stringFromJNI爲Java_加上包名、類型、方法名的下劃線連成一起。

native-lib.cpp文件

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

這就是一個JNI方法調用示例。

雖然Java函數不帶參數,但是原生方法卻帶了兩個參數,第一個參數JNIEnv是指向可用JNI函數表的接口指針,第二個參數jobject是Java函數所在類的實例的Java對象引用。

JNIEnv接口指針

原生代碼(c)通過JNIEnv接口指針提供的各種函數來使用虛擬機的功能,JNIEnv是一個指向線程-局部數據的指針,線程-局部數據中包含指向函數表的指針。

原生代碼是c與原生代碼是c++的調用JNI函數的語法不同,在c代碼中,JNIEnv是指向JNINativeInterface結構的指針,而在c++代碼中,JNIEnv是c++類實例,這兩種方式調用函數的方式是不一樣的。例如:

c代碼中:

(*env)->NewStringUTF(env,"Hello from JNI");

c++代碼中:

env->NewStringUTF("Hello from JNI");

實例方法與靜態方法

Java程序設計有兩類方法,實例方法和靜態方法。實例方法與類實例相關,只能在類實例中調用。靜態方法不與類死裏相關,它們可以在靜態上下文中直接調用。在原生代碼中可以獲取Java類的實例引用和類引用。例如:

類實例引用

extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject  thiz) {
}

類引用

extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv *env,
        jclass  clazz) {
}

從函數中看出來,JNI提供了自己的數據類型從而讓原生代碼瞭解Java數據類型。

JNI數據類型

JNI的數據類型包含兩種:基本類型和引用類型。與Java數據類型的對應關係如下:

基本數據類型:

| JNI類型 | Java類型 |
| ------ | ------ |
| jboolean | boolean |
| jbyte | byte |
| jchar | char |
| jshort | short |
| jint | int |
| jlong | long |
| jfloat | float |
| jdouble | double |
| void | void |

引用類型:

| JNI類型 | Java類型 |
| ------ | ------ |
| jobject | Object |
| jclass | Class |
| jstring | String |
| jobjectArray | Object[] |
| jbooleanArray | boolean[] |
| jbyteArray | char[] |
| jshortArray | short[] |
| jintArray | int[] |
| jlongArray | long[] |
| jfloatArray | float[] |
| jdoubleArray | double[] |
| jthrowable | Throwable |

引用數據類型的操作

JNI提供了與引用類型密切相關的一組API,這些API通過JNIEnv接口指針提供給原生函數。例如:

  • 字符串
  • 數組
  • NIO緩衝區
  • 字段
  • 方法

字符串操作

JNI把Java字符串當成引用類型處理,提供了Java與c字符串之間相互轉換的必要函數,由於Java字符串對象是不可變得,所以JNI不提供修改現有Java字符串內容的函數。

創建字符串

可以在原生代碼中使用NewString函數構建Unicode編碼格式的字符串實例,也可以中NewStringUTF函數構建UTF-8編碼格式的字符串實例,這些函數以C字符串爲參數,並返回一個Java字符串引用類型jstring值。例如:

jstring javaStr = (*env)->NewStringUTF(env,"Hello");

把Java字符串轉換成C字符串

爲了在原生代碼中使用Java字符串,需要將Java字符串轉換成C字符串。用GetStringChars函數可以將Unicode格式的Java字符串轉換成C字符串,用GetStringUTFChars函數可以將UTF-8格式的Java字符串轉換成C字符串。例如:

const jbyte* str
jboolean isCopy;
str = (*env)->GetStringUTFChars(env,javaString,&isCopy);

釋放字符串

通過JNI GetStringChars函數和GetStringUTFChars函數獲得的C字符串在原生代碼中使用完後要釋放,否則會引起內存泄漏。JNI提供了ReleaseStringChars函數和ReleaseStringUTFChars函數來釋放Unicode編碼和UTF-8編碼格式的字符串。例如:

(*env)->ReleaseStringUTFChars(env,javaString,str);

數組操作

創建數組

用New"Type"Array函數在原生代碼中創建數組實例,其中"Type"可以是Int、Char等類型,例如:

    jintArray javaArray = (*env)->NewIntArray(env,10);

訪問數組元素

將數組的代碼複製成C數組或者讓JNI提供直接指向數組元素的指針方式來訪問Java數組元素。

對副本的操作

Get"Type"ArrayRegion函數將給定的基本Java數組複製到給定的C數組中,例如:

    jint nativeArray[10];
    (*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);

原生代碼可以使用和修改數組元素,使用Set"Type"ArrayRegion函數將C數組複製回Java數組中。例如:

(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);

NIO操作

JNI提供了在原生代碼中使用NIO的函數,與數組操作相比,NIO性能較好,更適合在原生代碼和Java應用程序之間傳送大量數據。

創建直接字節緩衝區

unsigned char* buffer = (unsigned char*) malloc(1024);
jobject directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);

注意:原生函數應用通過釋放未使用的內存分配以避免內存泄漏。

獲取直接字節緩衝區

unsigned char* buffer;
buffer = (unsigned char*)(*env)->GetDirectBufferAddress(env,directBuffer);

訪問域

Java有兩類域:實例域和靜態域,這兩個的區別就是有沒有static聲明靜態。

獲取域ID

JNI提供了用域ID訪問兩類域的方法,可以通過給定實例的class對象獲取域ID,用GetObjectClass函數來獲取class對象。例如:

jclass clazz = (*env)->GetObjectClass(env,instance);

用GetFieldId函數來獲取實例域。

jfieldId instanceFieldId = (*env)->GetFieldId(env,clazz,"instanceField","Ljava/lang/String");

用GetStaticFieldId獲取靜態域ID。

jfieldID staticFieldId = (*env)->GetStaticFieldID(env,clazz,"staticField","Ljava/lang/String");

其中最後一個參數是Java中表示域類型的域描述符,"Ljava/lang/String"表明域類型是String。

獲取域

獲得域ID之後可以用Get"Type"Field函數獲取實際的實例域。例如:

jstring instanceField = (*env)->GetObjectField(env,instance,instanceFieldId);

用GetStatic"Type"Field函數獲得靜態域。例如:

jstring staticField = (*env)->GetStaticObjectField(env,clazz,staticFieldId);

調用方法

與域類似,Java中有兩類方法:實例方法和靜態方法。

獲取方法ID

JNI提供了用方法ID訪問兩類方法的途徑,可以用給定實例的class對象獲取方法ID,用GetMethodID函數獲得實例方法的方法ID。例如:

jmethodID instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");

用GetStaticMethodID函數獲得靜態域的方法ID,例如:

jmethodID staticMethodId=(*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String;");

調用方法

以方法ID爲參數通過Call"Type"Method類函數調用實際的實例方法。例如:

jstring instanceMethodResult = (*env)->CallStringMetthod(env,instance,instanceMethodId);

用CallStatic"Type"Field類函數調用靜態方法,例如:

jstring staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,staticMethodId);

域和方法描述符

在上面獲取域ID和方法ID均分別需要域描述符和方法描述符,域描述符和方法描述符可以通過下表Java類型簽名映射獲取:

| Java類型 | 簽名|
| ------ | ------ |
| Boolean | Z |
| Byte | B |
| Char | C |
| Short | S |
| Long | J |
| Int | I |
| Float | F |
| Double | D |
| void | V |
| fully-qualified-class | Lfully-qualified-class |
| type[] | [type |
| method type | (arg-type)ret-type |

類的簽名採用"L+包名+類名+;"的形式,將其中的.替換爲/即可,比如java.lang.String,它的簽名爲Ljava/lang/String;數組的簽名就是[+類型簽名,比如int數組,簽名就是[I,多維數組就是[[I。
方法的簽名爲(參數類型簽名)+返回值類型簽名,例如:boolean fun1(int a,double b,int[] c),其中參數類型的簽名爲ID[I,返回值類型的簽名爲Z,所以這個方法的簽名就是(ID[I)Z。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章