JNI開發流程與引用數據類型的處理

今天咱們來看下Java JNI,先看下維基百科給的定義,java

JNI, Java Native Interface, Java本地接口,是一種編程框架,使得Java虛擬機中的Java程序能夠調用本地應用或庫,也能夠被其餘程序調用。本地程序通常是用其它語言(C、C++或彙編語言)編寫的,而且被編譯爲基於本地硬件和操做系統的程序。android

本文就是分析下Java調用C++程序的步驟和JNI開發訪問數組和字符串的問題。編程

先看下Android中JNI的開發步驟。簡單寫了個Demo,看下效果:數組

Demo.png

調用方式:安全

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

SUM.png

Log.png

調用代碼:數據結構

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

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這個文件。ide

Make Class.png

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

Make h.png

看下文件內容,默認生成的函數名規則是:

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文件剪切過來。

New JNI Folder.png

第五步,終於到了寫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等

Make SO.png

第七步,須要在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層的對象方法字段等,有須要的小夥伴們歡迎關注。

相關文章
相關標籤/搜索