JNI開發(fā)流程與引用數(shù)據(jù)類型的處理

今天我們來看下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,看下效果:

Demo.png

調(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ù)組

SUM.png
Log.png

調(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è)文件。

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

看下文件內(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文件剪切過來。

New JNI Folder.png

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

Make SO.png

第七步,需要在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)注。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原鏈接:http://www.ibm.com/developerworks/cn/java/j-jni/ 使用 J...
    王朋6閱讀 8,229評(píng)論 0 8
  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全稱 Java Native Interface,Java...
    cfanr閱讀 58,518評(píng)論 9 133
  • 開發(fā)者使用JNI時(shí)最常問到的是JAVA和C/C++之間如何傳遞數(shù)據(jù),以及數(shù)據(jù)類型之間如何互相映射。本章我們從整數(shù)等...
    738bc070cd74閱讀 1,119評(píng)論 0 1
  • 在一個(gè)偏僻遙遠(yuǎn)的山谷里,有一個(gè)高達(dá)數(shù)千尺的斷崖。不知道什么時(shí)候,斷崖邊上長出了一株小小的百合。 百合剛剛誕生的時(shí)候...
    清心閣閱讀 1,938評(píng)論 0 1
  • 蔡彥軍一如既往的雙手捧著玉米棒棒,大夏天的戴著像火車頭一樣的咖啡色毛線茸茸帶圍脖的帽子,腦袋和脖子捂的嚴(yán)嚴(yán)實(shí)實(shí),生...
    那霞閱讀 594評(píng)論 14 6

友情鏈接更多精彩內(nèi)容