Android NDK開發之JNI基礎

codeteenager發表於2019-03-16

前言

之前寫了一篇文章簡單的介紹了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++的模板

Android NDK開發之JNI基礎

點選next,配置專案的資訊

Android NDK開發之JNI基礎

點選next,選擇使用哪種C++標準,選擇Toolchain Default會使用預設的CMake設定即可。

Android NDK開發之JNI基礎

點選finish即可完成工程的建立。

工程結構

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

Android NDK開發之JNI基礎

.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的相關配置

Android NDK開發之JNI基礎

程式碼結構

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。

相關文章