IntelliJ IDEA平臺(tái)下JNI編程(三)—字符串、數(shù)組

轉(zhuǎn)載請(qǐng)注明出處:【huachao1001的簡(jiǎn)書:http://www.itdecent.cn/users/0a7e42698e4b/latest_articles】

在前面HelloWorld篇中,自動(dòng)生成的頭文件對(duì)本地方法聲明的形參列表中的第一個(gè)參數(shù)即為JNIEnv *。那么JNIEnv到底能用來做什么?初學(xué)JNI的時(shí)候并沒有太在意,只滿足于Java能調(diào)用C代碼就行,而并沒有深究。今天這篇文章將學(xué)習(xí)JNI本地函數(shù)中如何與Java代碼中的字符串、數(shù)組相互訪問(或轉(zhuǎn)換)。通過這篇文章的學(xué)習(xí),相信會(huì)對(duì)JNIEnv有進(jìn)一步了解。

1. 從一個(gè)簡(jiǎn)單的例子開始

先創(chuàng)建一個(gè)Java類:com/huachao/java/HelloJNI.java,并聲明本地方法:private native String sayHello(String name);

package com.huachao.java;

/**
 * Created by HuaChao on 2017/01/13.
 */
public class HelloJNI {
    static {
        // hello.dll (Windows) or libhello.so (Unixes)
        System.loadLibrary("HelloJNI");     }

    private native String sayHello(String name);

    public static void main(String[] args) {
        // invoke the native method
      String rs=  new HelloJNI().sayHello("HuaChao");
      System.out.println("Java類收到來自JNI的返回:"+rs);
    }

}


編譯一下,找到HelloJNI.class,點(diǎn)擊右鍵,選擇External Tools>Generate Header File,如下(這個(gè)過程有疑問的請(qǐng)先轉(zhuǎn)移至《IntelliJ IDEA平臺(tái)下JNI編程(一)—HelloWorld篇》):

生成頭文件

此時(shí)在jni目錄中得到com_huachao_java_HelloJNI.h如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_huachao_java_HelloJNI */

#ifndef _Included_com_huachao_java_HelloJNI
#define _Included_com_huachao_java_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_huachao_java_HelloJNI
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

然后繼續(xù)在jni目錄中新建HelloJNI.c文件,如下:

#include<jni.h>
#include <stdio.h>
#include "com_huachao_java_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *env, jobject thisObj, jstring name){
   char buf[128];
   /* ERROR: incorrect use of jstring as a char* pointer */
   printf("Hello %s",name);//這里會(huì)出錯(cuò)
   scanf("%s",buf); 
   return buf;//這里出錯(cuò),不能將char*作為jstring返回
}

同樣,在HelloJNI.c上點(diǎn)擊右鍵選擇External Tools>Generate DLL(這個(gè)過程有疑問的請(qǐng)先轉(zhuǎn)移至《IntelliJ IDEA平臺(tái)下JNI編程(一)—HelloWorld篇》)。再點(diǎn)擊運(yùn)行,會(huì)發(fā)現(xiàn)錯(cuò)處!?。?!主要是因?yàn)?code>printf函數(shù)的第二個(gè)參數(shù)應(yīng)當(dāng)為char*類型,而不是jstring。那怎么樣將jstring轉(zhuǎn)為char*呢?這就需要借助JNIEnv*了。將HelloJNI.c改為如下:

#include<jni.h>
#include <stdio.h>
#include "com_huachao_java_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *env, jobject thisObj, jstring name){
   char buf[128];
   const jbyte *str;
   str = (*env)->GetStringUTFChars(env, name, NULL);
   if (str == NULL) {
     return NULL; /* OutOfMemoryError already thrown */
   }
   printf("Hello %s", str);
   (*env)->ReleaseStringUTFChars(env, name, str);
   /* 假設(shè)輸入字符不超過127個(gè) */
   scanf("%s", buf);
   return (*env)->NewStringUTF(env, buf);
}

HelloJNI.c上點(diǎn)擊右鍵選擇External Tools>Generate DLL后,再運(yùn)行HelloJNI.java類如下:

JNI_Return
Java類收到來自JNI的返回:JNI_Return
Hello HuaChao

可以看到,通過JNIEnv對(duì)象我們可以將jstringchar*相互轉(zhuǎn)換。但需要注意的是,通過GetStringUTFChars函數(shù)將jsting轉(zhuǎn)為char*時(shí),有可能會(huì)從堆空間中分配新的空間,這就有可能因?yàn)閮?nèi)存不足而分配失敗,因此需要判斷是否為NULL。同時(shí),當(dāng)不需要時(shí)應(yīng)當(dāng)將這塊新分配的空間釋放,即調(diào)用ReleaseStringUTFChars函數(shù)。也可以在本地方法中構(gòu)造一個(gè)java.lang.String實(shí)例對(duì)象,通過NewStringUTF函數(shù)來構(gòu)造。

可能有人會(huì)問,為什么通過GetStringUTFChars得到的char*需要釋放內(nèi)存,而通過NewStringUTF得到的jstring對(duì)象不用釋放內(nèi)存呢?這是因?yàn)?,使?code>GetStringUTFChars得到char*是在堆中開辟了新的空間用于存儲(chǔ)字符串,用完肯定需要手動(dòng)回收,因?yàn)镴VM并不會(huì)幫你回收本地方法中開辟的空間。而使用NewStringUTF創(chuàng)建的jstring對(duì)象屬于java.lang.String實(shí)例對(duì)象,虛擬機(jī)會(huì)自動(dòng)回收,另外用于轉(zhuǎn)換為jstring的char*對(duì)象(即例子中的buf)由于是函數(shù)內(nèi)部的局部變量,當(dāng)Java_com_huachao_java_HelloJNI_sayHello執(zhí)行結(jié)束后,自然會(huì)回收其內(nèi)部所有的局部變量的空間。

從上面結(jié)果可以看出,是先輸入JNI中的scanf函數(shù)中的字符串,再打印Java類中傳入的字符串。這個(gè)順序好像跟代碼順序不一致,這具體原因我還不清楚,待查找到資料后再回來修改。

2. 字符串

除了上面小節(jié)中介紹的幾個(gè)與字符串相關(guān)的函數(shù)以外,JNIEnv中還定義了很多其他的與字符串相關(guān)操作的函數(shù)。

通過GetStringChars函數(shù)得到的本地字符串(char*)是以Unicode編碼的數(shù)據(jù),我們知道UTF-8編碼的字符串一般是以\0作為結(jié)束字符,但Unicode編碼的字符串并不是這樣。為了獲取jstring類型引用的Unicode編碼的字符串中字符數(shù)量,可以通過調(diào)用GetStringLength函數(shù);而獲取jstring引用的字符串有多少個(gè)字節(jié)則調(diào)用ANSI C語言中的strlen函數(shù),或者是 JNIEnvGetStringUTFLength函數(shù)。

我們看看 GetStringChars函數(shù)原型:

const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

如果返回的字符串是從原始的java.lang.String實(shí)例中拷貝的數(shù)據(jù),則第三個(gè)參數(shù)isCopy指向的內(nèi)存會(huì)被設(shè)置為 JNI_TRUE,反之,如果返回的字符串是通過直接指向java.lang.String實(shí)例中的內(nèi)存空間,則isCopy指向的內(nèi)存會(huì)被設(shè)置為JNI_FALSE。當(dāng)isCopy指向的內(nèi)存存儲(chǔ)的是JNI_FALSE,那么不能對(duì)返回的char*中的內(nèi)容進(jìn)行修改,因?yàn)樵?code>Java中String是不可變的對(duì)象。

大部分情況下,直接將NULL作為isCopy參數(shù),因?yàn)榇蟛糠智闆r都不需要關(guān)心JVM是從java.lang.String中拷貝的字符串還是直接將指針指向原始的字符串。

一般情況下,不可預(yù)測(cè)虛擬機(jī)是否采用拷貝java.lang.String實(shí)例。因此你需要假定GetStringChars函數(shù)花費(fèi)時(shí)間和空間 來創(chuàng)建新的本地字符串(char*)。在JVM垃圾回收過程中,為了避免內(nèi)存空間碎片化,對(duì)象可能需要發(fā)生移動(dòng)。如果GetStringChars函數(shù)是通過直接將指針指向java.lang.String實(shí)例中的字符串,那么垃圾回收器則不再對(duì)java.lang.String實(shí)例對(duì)象進(jìn)行移動(dòng),即java.lang.String實(shí)例對(duì)象被一直固定在內(nèi)存的某一位置。如果過多的對(duì)象被固定在內(nèi)存中而不被移動(dòng),則會(huì)導(dǎo)致有很多內(nèi)存碎片。因此,每次調(diào)用GetStringChars函數(shù)時(shí),JVM需要判斷,決策是采用拷貝還是采用直接修改指針。

調(diào)用GetStringChars函數(shù)后,當(dāng)你不再使用該字符串時(shí),還需要記得調(diào)用ReleaseStringChars 。物理isCopy指向的內(nèi)容是 JNI_TRUE還是JNI_FALSE,都應(yīng)當(dāng)調(diào)用ReleaseStringChars 。 如果GetStringChars采用的是拷貝方式,則ReleaseStringChars釋放拷貝字符串占用的空間;如果GetStringChars是直接修改指針的方式,則將java.lang.String實(shí)例對(duì)象取消固定(即可被在內(nèi)存中移動(dòng))。

JNI 函數(shù) 描述 版本
GetStringChars ReleaseStringChars 獲取/釋放指向Unicode編碼字符串的指針,返回的可能是java.lang.String字符串的拷貝 JDK1.1
GetStringUTFChars ReleaseStringUTFChars 獲取/釋放指向UTF-8編碼字符串的指針,返回的可能是java.lang.String字符串的拷貝 JDK1.1
GetStringLength 返回Unicode編碼的字符串中字符的個(gè)數(shù) JDK1.1
GetStringUTFLength 返回UTF-8編碼的字符串中字節(jié)的個(gè)數(shù)(不包含結(jié)尾的\0 JDK1.1
NewString 創(chuàng)建java.lang.String實(shí)例,并且包含給定的Unicode編碼的C字符串 JDK1.1
NewStringUTF 創(chuàng)建java.lang.String實(shí)例,并且包含給定的UTF-8編碼的C字符串 JDK1.1
GetStringCritical ReleaseStringCritical 獲取一個(gè)指向Unicode編碼字符串的指針,返回的可能是java.lang.String字符串的拷貝,本地代碼在Get/ ReleaseStringCritical之間不能阻塞 Java 2 SDK1.2
GetStringRegion SetStringRegion 從C中已經(jīng)分配好的緩存中復(fù)制/賦值Unicode編碼的字符串 Java 2 SDK1.2
GetStringUTFRegion SetStringUTFRegion 從C中已經(jīng)分配好的緩存中復(fù)制/賦值UTF-8編碼的字符 Java 2 SDK1.2

3. 數(shù)組

3.1 基本類型數(shù)組

JNI將基本類型數(shù)組與對(duì)象數(shù)組區(qū)分對(duì)待,基本類型數(shù)組主要是元素為基本類型,對(duì)象數(shù)組元素是引用類型數(shù)組。如下:

int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;

iarrfarr是基本類型數(shù)組,而oarrarr2是對(duì)象數(shù)組。

本地方法中訪問基本類型數(shù)組就像訪問字符串一樣需要借助JNI中的函數(shù),如下為一個(gè)簡(jiǎn)單的例子:對(duì)int數(shù)組元素求和:

class IntArray {
    
    private native int sumArray(int[] arr);
    
    public static void main(String[] args) {
        IntArray p = new IntArray();
        int arr[] = new int[10];
        for (int i = 0; i < 10; i++) {
            arr[i] = i;
        }
        int sum = p.sumArray(arr);
        System.out.println("sum = " + sum);
    }
    
    static {
        System.loadLibrary("IntArray");
    }
}

而在本地代碼中,不能像如下代碼那樣:

/* 以下代碼有錯(cuò)誤 */
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int i, sum = 0;
    for (i = 0; i < 10; i++) {
        sum += arr[i];
    }
}

上面代碼是有問題的,你必須使用JNI函數(shù)來訪問基本來下數(shù)組中的元素,如下所示:

JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    jint buf[10];
    jint i, sum = 0;
    (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
    for (i = 0; i < 10; i++) {
        sum += buf[i];
    }
    return sum;
}

上面例子中,使用了GetIntArrayRegion 函數(shù)來復(fù)制數(shù)組中的元素到C語言中的緩存中(buf),其中第三個(gè)參數(shù)表示起始下標(biāo),第四個(gè)參數(shù)表示復(fù)制的元素個(gè)數(shù)。 只要元素拷貝到C緩存中,本地代碼就可以直接使用緩存中的數(shù)組了。 前面例子中沒有異常檢查,這是因?yàn)槲覀冎罃?shù)組長(zhǎng)度是10,所有不會(huì)越界。

JNI支持一套數(shù)組的Get和Set函數(shù)==> Get/Release<Type>ArrayElements (如: Get/ReleaseIntArrayElements),用于本地代碼直接獲取基本類型數(shù)組的指針。由于垃圾回收器的底層實(shí)現(xiàn)可能不支持?jǐn)?shù)組對(duì)象在內(nèi)存中固定不動(dòng),所以在垃圾回收過程中數(shù)組在內(nèi)存位置發(fā)生變化,JVM返回的指針是原始的基本類型數(shù)組的拷貝的地址。 上面代碼可以改為如下:

JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    jint *carr;
    jint i, sum = 0;
    carr = (*env)->GetIntArrayElements(env, arr, NULL);
    if (carr == NULL) {
        return 0; /* exception occurred */
    }
    for (i=0; i<10; i++) {
        sum += carr[i];
    }
    (*env)->ReleaseIntArrayElements(env, arr, carr, 0);
    return sum;
}

GetArrayLength 函數(shù)返回?cái)?shù)組中的元素個(gè)數(shù),數(shù)組的固定長(zhǎng)度在第一次分配內(nèi)存時(shí)確定。函數(shù) Get/ReleasePrimitiveArrayCritical允許虛擬機(jī)在訪問原始數(shù)組時(shí)禁用垃圾回收器。你應(yīng)該像使用 Get/ReleaseStringCritical一樣小心地使用Get/ReleasePrimitiveArrayCritical。在Get/ReleasePrimitiveArrayCritical之間的代碼必須不能調(diào)用任何JNI函數(shù)或執(zhí)行任何阻塞操作,因?yàn)檫@可能會(huì)導(dǎo)致應(yīng)用死鎖。

一般使用Get/Release<type>ArrayElements都是安全的,虛擬機(jī)要么直接返回?cái)?shù)組元素的指針,幺妹返回?cái)?shù)組元素拷貝后的地址指針。
| JNI Function| Description | Since |
|:----|:----|:---|
|Get<Type>ArrayRegion Set<Type>ArrayRegion| 從本地分配的基本類型數(shù)組中或者復(fù)制/賦值| JDK1.1|
|Get<Type>ArrayElements Release<Type>ArrayElements|獲取基本類型數(shù)組指針,可能返回的是拷貝|JDK1.1|
|GetArrayLength| 返回?cái)?shù)組中元素個(gè)數(shù) |JDK1.1|
|New<Type>Array| 根據(jù)指定的長(zhǎng)度創(chuàng)建數(shù)組 |JDK1.1|
|GetPrimitiveArrayCritical ReleasePrimitiveArrayCritical|獲取或釋放基本類型數(shù)組指針,可能會(huì)禁用垃圾回收,可能返回的是數(shù)組的拷貝 |Java 2 SDK1.2|

3.2 對(duì)象數(shù)組

函數(shù)GetObjectArrayElement返回指定下標(biāo)的元素,而 SetObjectArrayElement函數(shù)更新指定下包的元素。不像基本類型數(shù)組,我們無法一次性獲取所有的對(duì)象數(shù)組中的元素或者是拷貝多個(gè)元素。字符串和數(shù)組都是引用類型,可以通過 Get/SetObjectArrayElement來訪問數(shù)組中的字符串和數(shù)組中的數(shù)組。 如下代碼示例為本地方法創(chuàng)建二維int數(shù)組后返回到Java代碼中,并且打印該二維數(shù)組數(shù)組內(nèi)容:

class ObjectArrayTest {
    private static native int[][] initInt2DArray(int size);
    
    public static void main(String[] args) {
        int[][] i2arr = initInt2DArray(3);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.print(" " + i2arr[i][j]);
            }
            System.out.println();
        }
    }
    
    static {
        System.loadLibrary("ObjectArrayTest");
    }
}

對(duì)應(yīng)的本地代碼實(shí)現(xiàn)如下:

JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size) {
    jobjectArray result;
    int i;
    jclass intArrCls = (*env)->FindClass(env, "[I");
    if (intArrCls == NULL) {
        return NULL; /* exception thrown */
    }
    result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
    if (result == NULL) {
        return NULL; /* out of memory error thrown */
    }
    for (i = 0; i < size; i++) {
        jint tmp[256]; /* make sure it is large enough! */
        int j;
        jintArray iarr = (*env)->NewIntArray(env, size);
        if (iarr == NULL) {
            return NULL; /* out of memory error thrown */
        }
        for (j = 0; j < size; j++) {
            tmp[j] = i + j;
        }
        (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
        (*env)->SetObjectArrayElement(env, result, i, iarr);
        (*env)->DeleteLocalRef(env, iarr);
    }
    return result;
}

本地方法中,先調(diào)用了JNI函數(shù)FindClass來獲取二維數(shù)組中元素類型(Class)的引用,上一章中我們介紹過類型映射,我們知道[I表示的是Java中int[]對(duì)象的類型。如果FindClass返回NULL,說明類加載失?。赡苁且?yàn)轭愇募淮嬖诨蛘呤荗OM)。接下來NewObjectArray 函數(shù)分配一個(gè)數(shù)組,其元素類型為intArrCls只向的引用類型。NewObjectArray 函數(shù)只能分配一維數(shù)組,我們將一維數(shù)組作為其元素類型,這樣就構(gòu)成了二維數(shù)組。JVM并沒有指定多維數(shù)組的數(shù)據(jù)結(jié)構(gòu),二維數(shù)組只是元素類型為數(shù)組的數(shù)組。

運(yùn)行結(jié)果如下:

0 1 2
1 2 3
2 3 4

上面例子中最外面的循環(huán)后面調(diào)用了DeleteLocalRef ,這是為了防止虛擬機(jī)一直持有JNI中的引用(如例子中的iarr)導(dǎo)致OOM。

最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 花了幾天時(shí)間研究了下JNI,基本上知道如何使用了。照我的觀點(diǎn)JNI還是不難的,難得只是我們一份嘗試的心。 學(xué)習(xí)過程...
    皇小弟閱讀 1,754評(píng)論 0 1
  • 開發(fā)者使用JNI時(shí)最常問到的是JAVA和C/C++之間如何傳遞數(shù)據(jù),以及數(shù)據(jù)類型之間如何互相映射。本章我們從整數(shù)等...
    738bc070cd74閱讀 1,098評(píng)論 0 1
  • 對(duì)于入門級(jí)Android菜鳥的我來說,從配置到開發(fā)JNI是一個(gè)煎熬的過程,但還是取得了最終的成功。這里主要是整個(gè)過...
    杰嗒嗒的阿杰閱讀 3,851評(píng)論 16 23
  • 我不是王子,連勇士也差的遠(yuǎn)。 喜歡的情緒被悄悄隱藏。 歡樂氣氛偷偷掩蓋怯懦的落寞。 文武英俊的標(biāo)簽貼不到我身上。 ...
    景迪瓦閱讀 442評(píng)論 0 0
  • 很久沒有安安靜靜的坐下來寫點(diǎn)東西了。大學(xué)時(shí)候特別喜歡手指在鍵盤上敲擊的感覺,就像指尖在跳舞,文思也順勢(shì)泉涌。而現(xiàn)在...
    丟啊丟啊丟大罐閱讀 337評(píng)論 0 0

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