今天咱們來看下Java JNI,先看下維基百科給的定義,java
JNI, Java Native Interface, Java本地接口,是一種編程框架,使得Java虛擬機中的Java程序能夠調用本地應用或庫,也能夠被其餘程序調用。本地程序通常是用其它語言(C、C++或彙編語言)編寫的,而且被編譯爲基於本地硬件和操做系統的程序。android
本文就是分析下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傳遞過來的二維數組bash
調用代碼:數據結構
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開發步驟:app
第一步,在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這個文件。ide
第三步, 在終端中切換目錄到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);
}
複製代碼
再回顧一下上面.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提供操做字符串的函數不少,這裏就不一一解釋了,主要須要注意內存的分配和跨線程的問題。
數組和上面的字符串相似,沒辦法直接操做,須要經過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;
}
複製代碼
一樣地, 數組操做的函數也有不少,這裏不可能每一個都進行說明,有須要的小夥伴能夠自行搜索,差異不會太大。
本文只是對Android開發JNI的一點點理解總結,包括JNI開發的步驟,字符串和數組的處理,在JNI Native開發過程當中都沒辦法直接操做引用類型的數據,須要經過JNI提供的函數來獲取JVM中的數據,提供的函數有的會進行原數據的拷貝有的會返回原數據的指針,根據本身須要進行不一樣的選擇。
後面有可能會對JNI再出一些內容,好比Native調用Java層的對象方法字段等,有須要的小夥伴們歡迎關注。