今天我們來看下Java JNI,先看下維基百科給的定義,
JNI, Java Native Interface, Java本地介面,是一種程式設計框架,使得Java虛擬機器中的Java程式可以呼叫本地應用或庫,也可以被其他程式呼叫。本地程式一般是用其它語言(C、C++或組合語言)編寫的,並且被編譯為基於本地硬體和作業系統的程式。
本文就是分析下Java呼叫C++程式的步驟和JNI開發訪問陣列和字串的問題。
先看下Android中JNI的開發步驟。簡單寫了個Demo,看下效果:
呼叫方式:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, HelloWorld.sayHello("JNIEnjoy!"), Toast.LENGTH_LONG).show();
}
});
複製程式碼
點選SUM會對陣列進行求和和列印出Native傳遞過來的二維陣列
呼叫程式碼:
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
btn2.setText(String.valueOf(ArrayJni.arraySum(get())));
int[][] arr = ArrayJni.getArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Log.d("JNILOG", String.valueOf(arr[i][j]));
}
}
}
});
private int[] get() {
int[] array = new int[10];
for (int i = 0; i < array.length; i++) {
array[i] = i;
}
return array;
}
複製程式碼
接下來看下JNI開發步驟:
1.JNI開發步驟
第一步,在Java層先建立JNI Class,需要呼叫Native 方法的地方需要關鍵字native宣告,其中方法sayHello就是需要底層實現的,這個Demo中會用C實現。
public class HelloWorld {
public static native String sayHello(String name);
}
複製程式碼
第二步, Make Project,這樣會在app/build/intermediates/classes/debug下生成class檔案,如下圖所示,當然需要的就是Hello World.class這個檔案。
第三步, 在終端中切換目錄到app\build\intermediates\classes\debug, 通過命令生成.h標頭檔案javah -jni juexingzhe.com.hello.HelloWorld
,juexingzhe.com.hello是包名,需要換成小夥伴自己的包名。juexingzhe.com.hello.HelloWorld.h檔案。
看下檔案內容,預設生成的函式名規則是:
Java_包名_類名_Native方法名
複製程式碼
其中JNIEnv是執行緒相關的,即在每個執行緒中都有一個JNIEnv指標, 每個JNIEnv都是執行緒專有,執行緒A不能呼叫執行緒B的JNIEnv。
jclass就是HelloWorld這個類,因為在這個例子中方法是靜態的,所以預設生成的是jclass,如果方法不是靜態的,預設生成的就會傳入jobject,指向呼叫這個native方法時的物件例項。
jstring就是定義方法時傳入的引數。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class juexingzhe_com_hello_HelloWorld */
#ifndef _Included_juexingzhe_com_hello_HelloWorld
#define _Included_juexingzhe_com_hello_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: juexingzhe_com_hello_HelloWorld
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
複製程式碼
第四步,在main目錄下新建jni資料夾,將上面生成的.h檔案剪下過來。
第五步,終於到了寫c程式碼的時候了,注意在標頭檔案中,C和C++寫法是不一樣的。
C中(*env)->NewStringUTF(env, "string)
C++中env->NewStringUTF("string")
複製程式碼
最終的juexingzhe.com.hello.HelloWorld.c
檔案如下:
#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */
/*
* Class: juexingzhe_com_hello_HelloWorld
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
(JNIEnv *env, jclass jcls, jstring jstr)
{
const char *c_str = NULL;
char buff[128] = { 0 };
c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (c_str == NULL)
{
printf("out of memory.\n");
return NULL;
}
sprintf(buff, "hello %s", c_str);
(*env)->ReleaseStringUTFChars(env, jstr, c_str);
return (*env)->NewStringUTF(env, buff);
}
複製程式碼
對上面的程式碼有幾點需要注意的, 參考後面字串處理。
經過上面五步寫程式碼的步驟就差不多了,還有一個問題,Java層怎麼調到這個C檔案呢?這就需要第六步配置ndk
第六步,配置ndk,在module包下面的build.gradle中的defaultConfig新增,其中moduleName就是最終打包出來的so庫的名字
ndk {
moduleName 'HelloWorld'
}
複製程式碼
最終android這個task是下面這樣的
android {
compileSdkVersion 26
defaultConfig {
applicationId "juexingzhe.com.hello"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {
moduleName 'HelloWorld'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
複製程式碼
還需要在工程目錄下的gradle.properties中新增下面這句話
android.useDeprecatedNdk=true
複製程式碼
重新Make Project就可以生成.so檔案,這裡沒有配置平臺,所以會預設生成所有平臺的so庫,包括arm/x86/mips等
第七步,需要在Java層載入這個.so檔案,在第一步編寫的HelloWorld.Java中新增,其中HelloWorld就是上面NDK配置生成的so庫名字。
public class HelloWorld {
static {
System.loadLibrary("HelloWorld");
}
public static native String sayHello(String name);
}
複製程式碼
2.字串處理
再回顧一下上面.c檔案的內容:
#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */
/*
* Class: juexingzhe_com_hello_HelloWorld
* Method: sayHello
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
(JNIEnv *env, jclass jcls, jstring jstr)
{
const char *c_str = NULL;
char buff[128] = { 0 };
c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
if (c_str == NULL)
{
printf("out of memory.\n");
return NULL;
}
sprintf(buff, "hello %s", c_str);
(*env)->ReleaseStringUTFChars(env, jstr, c_str);
return (*env)->NewStringUTF(env, buff);
}
複製程式碼
-
1.jstring型別是指向JVM內部的一個字串,和基本型別不一樣,C程式碼中不能直接拿來用,需要通過JNI函式來訪問JVM內部的字串資料結構。
-
2.簡單看下GetStringUTFChars(env, jstr, &isCopy), jstr是Java傳遞給原生程式碼的字串指標,isCopy取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內部源字串的一份拷貝,併為新產生的字串分配記憶體空間。如果值為JNI_FALSE,表示返回JVM內部源字串的指標,意味著可以通過指標修改源字串的內容,不推薦這麼做,因為這樣做就打破了Java字串不能修改的規定。但我們在開發當中,並不關心這個值是多少,通常情況下這個引數填NULL即可。
-
3.Java預設使用Unicode編碼,而C/C++預設使用UTF編碼,所以在原生程式碼中操作字串的時候,必須使用合適的JNI函式把jstring轉換成C風格的字串。JNI支援字串在Unicode和UTF-8兩種編碼之間轉換,GetStringUTFChars可以把一個jstring指標(指向JVM內部的Unicode字元序列)轉換成一個UTF-8格式的C字串。在上例中sayHello函式中我們通過GetStringUTFChars正確取得了JVM內部的字串內容
-
4.異常檢查。呼叫完GetStringUTFChars需要進行安全檢查,因為JVM需要為新誕生的字串分配記憶體,分配失敗會返回NULL,並丟擲OutOfMemoryError異常。Java中如果遇到異常沒有捕獲程式會立即停止執行。而JNI遇到未處理的異常不會改變程式的執行流程,回繼續往下走,這樣後面對這個字串的所有操作都是危險的。所以如果NULL,需要return跳過後面的程式碼。
-
5.釋放字串。C和Java不一樣,需要手動釋放記憶體,通過ReleaseStringUTFChars函式通知JVM這塊記憶體不需要了。注意GetXXX和ReleaseXXX要配套呼叫。
-
6.呼叫NewStringUTF函式會構建一個新的java.lang.String字串物件,這個物件會自動轉換成Java支援的Unicode編碼。如果JVM不能為構造java.lang.String分配足夠的記憶體,NewStringUTF會丟擲一個OutOfMemoryError異常,並返回NULL。
當然,JNI提供操作字串的函式很多,這裡就不一一解釋了,主要需要注意記憶體的分配和跨執行緒的問題。
3.陣列處理
陣列和上面的字串類似,沒辦法直接操作,需要通過JNI函式從JVM中獲取到對應的指標或者拷貝到記憶體緩衝區再進行操作。
按照上面步驟再新增一個陣列的例子,看下Java程式碼,兩個Native函式,一個求和一個獲取二維陣列。
public class ArrayJni {
static {
System.loadLibrary("HelloWorld");
}
//求和
public static native int arraySum(int[] array);
//獲取二維陣列
public static native int[][] getArray(int size);
}
複製程式碼
接下來先看下arraySum的C程式碼,Java層定義的引數是int型別的陣列對應到Native就是jintArray, 通過GetArrayLength獲取引數陣列的長度,然後通過GetIntArrayRegion將引數陣列拷貝到記憶體緩衝區buffer,之後就可以進行求和操作了。操作完成記得釋放記憶體。
/*
* Class: juexingzhe_com_hello_ArrayJni
* Method: arraySum
* Signature: ([I)I
*/
JNIEXPORT jint JNICALL Java_juexingzhe_com_hello_ArrayJni_arraySum
(JNIEnv *env, jclass jcls, jintArray jarr)
{
jint i, sum = 0, len;
jint *buffer;
//1.獲取陣列長度
len = (*env)->GetArrayLength(env, jarr);
//2.分配緩衝區
buffer = (jint*) malloc(sizeof(jint) * len);
memset(buffer, 0, sizeof(jint) * len);
//3.拷貝Java陣列中所有元素到緩衝區
(*env)->GetIntArrayRegion(env, jarr, 0, len, buffer);
//4.求和
for (int i = 0; i < len; ++i) {
sum += buffer[i];
}
//5.釋放記憶體
free(buffer);
return sum;
}
複製程式碼
再看下生成二維陣列的程式碼, 小夥伴們都知道二維陣列中每一個元素其實是一維陣列,所以需要先構造一維陣列的引用,通過FindClass,再通過NewObjectArray構造二維陣列。
通過NewIntArray構造一維陣列,然後SetIntArrayRegion賦值int型別陣列元素,當然也有GetIntArrayRegion函式,可以將Java陣列中的所有元素拷貝到C緩衝區中。
二維陣列通過SetObjectArrayElement進行賦值。
為了避免在迴圈內建立大量的JNI區域性引用,造成JNI引用表溢位,在外層迴圈中每次都要呼叫DeleteLocalRef將新建立的jintArray引用從引用表中移除。在JNI中,只有jobject以及子類屬於引用變數,會佔用引用表的空間,jint,jfloat,jboolean等都是基本型別變數,不會佔用引用表空間,即不需要釋放。引用表最大空間為512個,如果超出這個範圍,JVM就會掛掉。
/*
* Class: juexingzhe_com_hello_ArrayJni
* Method: getArray
* Signature: (I)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_juexingzhe_com_hello_ArrayJni_getArray
(JNIEnv *env, jclass jcls, jint size)
{
jobjectArray result;
jclass onearray;
//1.獲取一維陣列引用
onearray = (*env)->FindClass(env, "[I");
if (onearray == NULL){
return NULL;
}
//2.構造二維陣列
result = (*env)->NewObjectArray(env, size, onearray, NULL);
if (result == NULL){
return NULL;
}
//3.構造一維陣列
for (int i = 0; i < size; ++i) {
int j;
jint buffer[256];
//構造一維陣列
jintArray array = (*env)->NewIntArray(env, size);
if (array == NULL){
return NULL;
}
//準備資料
for (int j = 0; j < size; ++j) {
buffer[j] = i + j;
}
//設定一維陣列資料
(*env)->SetIntArrayRegion(env, array, 0, size, buffer);
//賦值一維陣列給二維陣列
(*env)->SetObjectArrayElement(env, result, i, array);
//刪除一維陣列引用
(*env)->DeleteLocalRef(env, array);
}
return result;
}
複製程式碼
同樣地, 陣列操作的函式也有很多,這裡不可能每個都進行說明,有需要的小夥伴可以自行搜尋,差別不會太大。
4.總結
本文只是對Android開發JNI的一點點理解總結,包括JNI開發的步驟,字串和陣列的處理,在JNI Native開發過程中都沒辦法直接操作引用型別的資料,需要通過JNI提供的函式來獲取JVM中的資料,提供的函式有的會進行原資料的拷貝有的會返回原資料的指標,根據自己需要進行不同的選擇。
後面有可能會對JNI再出一些內容,比如Native呼叫Java層的物件方法欄位等,有需要的小夥伴們歡迎關注。