今天我們來看下Java JNI,先看下維基百科給的定義,
JNI, Java Native Interface, Java本地接口,是一種編程框架,使得Java虛擬機(jī)中的Java程序可以調(diào)用本地應(yīng)用或庫,也可以被其他程序調(diào)用。本地程序一般是用其它語言(C、C++或匯編語言)編寫的,并且被編譯為基于本地硬件和操作系統(tǒng)的程序。
本文就是分析下Java調(diào)用C++程序的步驟和JNI開發(fā)訪問數(shù)組和字符串的問題。
先看下Android中JNI的開發(fā)步驟。簡單寫了個(gè)Demo,看下效果:

調(diào)用方式:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, HelloWorld.sayHello("JNIEnjoy!"), Toast.LENGTH_LONG).show();
}
});
點(diǎn)擊SUM會(huì)對(duì)數(shù)組進(jìn)行求和和打印出Native傳遞過來的二維數(shù)組


調(diào)用代碼:
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開發(fā)步驟:
1.JNI開發(fā)步驟
第一步,在Java層先建立JNI Class,需要調(diào)用Native 方法的地方需要關(guān)鍵字native聲明,其中方法sayHello就是需要底層實(shí)現(xiàn)的,這個(gè)Demo中會(huì)用C實(shí)現(xiàn)。
public class HelloWorld {
public static native String sayHello(String name);
}
第二步, Make Project,這樣會(huì)在app/build/intermediates/classes/debug下生成class文件,如下圖所示,當(dāng)然需要的就是Hello World.class這個(gè)文件。

第三步, 在終端中切換目錄到app\build\intermediates\classes\debug, 通過命令生成.h頭文件javah -jni juexingzhe.com.hello.HelloWorld,juexingzhe.com.hello是包名,需要換成小伙伴自己的包名。juexingzhe.com.hello.HelloWorld.h文件。

看下文件內(nèi)容,默認(rèn)生成的函數(shù)名規(guī)則是:
Java_包名_類名_Native方法名
其中JNIEnv是線程相關(guān)的,即在每個(gè)線程中都有一個(gè)JNIEnv指針, 每個(gè)JNIEnv都是線程專有,線程A不能調(diào)用線程B的JNIEnv。
jclass就是HelloWorld這個(gè)類,因?yàn)樵谶@個(gè)例子中方法是靜態(tài)的,所以默認(rèn)生成的是jclass,如果方法不是靜態(tài)的,默認(rèn)生成的就會(huì)傳入jobject,指向調(diào)用這個(gè)native方法時(shí)的對(duì)象實(shí)例。
jstring就是定義方法時(shí)傳入的參數(shù)。
/* 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代碼的時(shí)候了,注意在頭文件中,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);
}
對(duì)上面的代碼有幾點(diǎn)需要注意的, 參考后面字符串處理。
經(jīng)過上面五步寫代碼的步驟就差不多了,還有一個(gè)問題,Java層怎么調(diào)到這個(gè)C文件呢?這就需要第六步配置ndk
第六步,配置ndk,在module包下面的build.gradle中的defaultConfig添加,其中moduleName就是最終打包出來的so庫的名字
ndk {
moduleName 'HelloWorld'
}
最終android這個(gè)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文件,這里沒有配置平臺(tái),所以會(huì)默認(rèn)生成所有平臺(tái)的so庫,包括arm/x86/mips等

第七步,需要在Java層加載這個(gè).so文件,在第一步編寫的HelloWorld.Java中添加,其中HelloWorld就是上面NDK配置生成的so庫名字。
public class HelloWorld {
static {
System.loadLibrary("HelloWorld");
}
public static native String sayHello(String name);
}
2.字符串處理
再回顧一下上面.c文件的內(nèi)容:
#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內(nèi)部的一個(gè)字符串,和基本類型不一樣,C代碼中不能直接拿來用,需要通過JNI函數(shù)來訪問JVM內(nèi)部的字符串?dāng)?shù)據(jù)結(jié)構(gòu)。
2.簡單看下GetStringUTFChars(env, jstr, &isCopy), jstr是Java傳遞給本地代碼的字符串指針,isCopy取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示返回JVM內(nèi)部源字符串的一份拷貝,并為新產(chǎn)生的字符串分配內(nèi)存空間。如果值為JNI_FALSE,表示返回JVM內(nèi)部源字符串的指針,意味著可以通過指針修改源字符串的內(nèi)容,不推薦這么做,因?yàn)檫@樣做就打破了Java字符串不能修改的規(guī)定。但我們在開發(fā)當(dāng)中,并不關(guān)心這個(gè)值是多少,通常情況下這個(gè)參數(shù)填NULL即可。
3.Java默認(rèn)使用Unicode編碼,而C/C++默認(rèn)使用UTF編碼,所以在本地代碼中操作字符串的時(shí)候,必須使用合適的JNI函數(shù)把jstring轉(zhuǎn)換成C風(fēng)格的字符串。JNI支持字符串在Unicode和UTF-8兩種編碼之間轉(zhuǎn)換,GetStringUTFChars可以把一個(gè)jstring指針(指向JVM內(nèi)部的Unicode字符序列)轉(zhuǎn)換成一個(gè)UTF-8格式的C字符串。在上例中sayHello函數(shù)中我們通過GetStringUTFChars正確取得了JVM內(nèi)部的字符串內(nèi)容
4.異常檢查。調(diào)用完GetStringUTFChars需要進(jìn)行安全檢查,因?yàn)镴VM需要為新誕生的字符串分配內(nèi)存,分配失敗會(huì)返回NULL,并拋出OutOfMemoryError異常。Java中如果遇到異常沒有捕獲程序會(huì)立即停止運(yùn)行。而JNI遇到未處理的異常不會(huì)改變程序的運(yùn)行流程,回繼續(xù)往下走,這樣后面對(duì)這個(gè)字符串的所有操作都是危險(xiǎn)的。所以如果NULL,需要return跳過后面的代碼。
5.釋放字符串。C和Java不一樣,需要手動(dòng)釋放內(nèi)存,通過ReleaseStringUTFChars函數(shù)通知JVM這塊內(nèi)存不需要了。注意GetXXX和ReleaseXXX要配套調(diào)用。
6.調(diào)用NewStringUTF函數(shù)會(huì)構(gòu)建一個(gè)新的java.lang.String字符串對(duì)象,這個(gè)對(duì)象會(huì)自動(dòng)轉(zhuǎn)換成Java支持的Unicode編碼。如果JVM不能為構(gòu)造java.lang.String分配足夠的內(nèi)存,NewStringUTF會(huì)拋出一個(gè)OutOfMemoryError異常,并返回NULL。
當(dāng)然,JNI提供操作字符串的函數(shù)很多,這里就不一一解釋了,主要需要注意內(nèi)存的分配和跨線程的問題。
3.數(shù)組處理
數(shù)組和上面的字符串類似,沒辦法直接操作,需要通過JNI函數(shù)從JVM中獲取到對(duì)應(yīng)的指針或者拷貝到內(nèi)存緩沖區(qū)再進(jìn)行操作。
按照上面步驟再添加一個(gè)數(shù)組的例子,看下Java代碼,兩個(gè)Native函數(shù),一個(gè)求和一個(gè)獲取二維數(shù)組。
public class ArrayJni {
static {
System.loadLibrary("HelloWorld");
}
//求和
public static native int arraySum(int[] array);
//獲取二維數(shù)組
public static native int[][] getArray(int size);
}
接下來先看下arraySum的C代碼,Java層定義的參數(shù)是int類型的數(shù)組對(duì)應(yīng)到Native就是jintArray, 通過GetArrayLength獲取參數(shù)數(shù)組的長度,然后通過GetIntArrayRegion將參數(shù)數(shù)組拷貝到內(nèi)存緩沖區(qū)buffer,之后就可以進(jìn)行求和操作了。操作完成記得釋放內(nèi)存。
/*
* 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.獲取數(shù)組長度
len = (*env)->GetArrayLength(env, jarr);
//2.分配緩沖區(qū)
buffer = (jint*) malloc(sizeof(jint) * len);
memset(buffer, 0, sizeof(jint) * len);
//3.拷貝Java數(shù)組中所有元素到緩沖區(qū)
(*env)->GetIntArrayRegion(env, jarr, 0, len, buffer);
//4.求和
for (int i = 0; i < len; ++i) {
sum += buffer[i];
}
//5.釋放內(nèi)存
free(buffer);
return sum;
}
再看下生成二維數(shù)組的代碼, 小伙伴們都知道二維數(shù)組中每一個(gè)元素其實(shí)是一維數(shù)組,所以需要先構(gòu)造一維數(shù)組的引用,通過FindClass,再通過NewObjectArray構(gòu)造二維數(shù)組。
通過NewIntArray構(gòu)造一維數(shù)組,然后SetIntArrayRegion賦值int類型數(shù)組元素,當(dāng)然也有GetIntArrayRegion函數(shù),可以將Java數(shù)組中的所有元素拷貝到C緩沖區(qū)中。
二維數(shù)組通過SetObjectArrayElement進(jìn)行賦值。
為了避免在循環(huán)內(nèi)創(chuàng)建大量的JNI局部引用,造成JNI引用表溢出,在外層循環(huán)中每次都要調(diào)用DeleteLocalRef將新創(chuàng)建的jintArray引用從引用表中移除。在JNI中,只有jobject以及子類屬于引用變量,會(huì)占用引用表的空間,jint,jfloat,jboolean等都是基本類型變量,不會(huì)占用引用表空間,即不需要釋放。引用表最大空間為512個(gè),如果超出這個(gè)范圍,JVM就會(huì)掛掉。
/*
* 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.獲取一維數(shù)組引用
onearray = (*env)->FindClass(env, "[I");
if (onearray == NULL){
return NULL;
}
//2.構(gòu)造二維數(shù)組
result = (*env)->NewObjectArray(env, size, onearray, NULL);
if (result == NULL){
return NULL;
}
//3.構(gòu)造一維數(shù)組
for (int i = 0; i < size; ++i) {
int j;
jint buffer[256];
//構(gòu)造一維數(shù)組
jintArray array = (*env)->NewIntArray(env, size);
if (array == NULL){
return NULL;
}
//準(zhǔn)備數(shù)據(jù)
for (int j = 0; j < size; ++j) {
buffer[j] = i + j;
}
//設(shè)置一維數(shù)組數(shù)據(jù)
(*env)->SetIntArrayRegion(env, array, 0, size, buffer);
//賦值一維數(shù)組給二維數(shù)組
(*env)->SetObjectArrayElement(env, result, i, array);
//刪除一維數(shù)組引用
(*env)->DeleteLocalRef(env, array);
}
return result;
}
同樣地, 數(shù)組操作的函數(shù)也有很多,這里不可能每個(gè)都進(jìn)行說明,有需要的小伙伴可以自行搜索,差別不會(huì)太大。
4.總結(jié)
本文只是對(duì)Android開發(fā)JNI的一點(diǎn)點(diǎn)理解總結(jié),包括JNI開發(fā)的步驟,字符串和數(shù)組的處理,在JNI Native開發(fā)過程中都沒辦法直接操作引用類型的數(shù)據(jù),需要通過JNI提供的函數(shù)來獲取JVM中的數(shù)據(jù),提供的函數(shù)有的會(huì)進(jìn)行原數(shù)據(jù)的拷貝有的會(huì)返回原數(shù)據(jù)的指針,根據(jù)自己需要進(jìn)行不同的選擇。
后面有可能會(huì)對(duì)JNI再出一些內(nèi)容,比如Native調(diào)用Java層的對(duì)象方法字段等,有需要的小伙伴們歡迎關(guān)注。