IntelliJ IDEA平臺下JNI編程(三)—字符串、數組

轉載請註明出處:【huachao1001的專欄:http://blog.csdn.net/huachao1001/article/details/54407327】

在前面HelloWorld篇中,自動生成的頭文件對本地方法聲明的形參列表中的第一個參數即爲JNIEnv *。那麼JNIEnv到底能用來做什麼?初學JNI的時候並沒有太在意,只滿足於Java能調用C代碼就行,而並沒有深究。今天這篇文章將學習JNI本地函數中如何與Java代碼中的字符串、數組相互訪問(或轉換)。通過這篇文章的學習,相信會對JNIEnv有進一步瞭解。

1. 從一個簡單的例子開始

先創建一個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,點擊右鍵,選擇External Tools>Generate Header File,如下(這個過程有疑問的請先轉移至《IntelliJ IDEA平臺下JNI編程(一)—HelloWorld篇》):

生成頭文件

此時在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

然後繼續在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);//這裏會出錯
   scanf("%s",buf); 
   return buf;//這裏出錯,不能將char*作爲jstring返回
}

同樣,在HelloJNI.c上點擊右鍵選擇External Tools>Generate DLL(這個過程有疑問的請先轉移至《IntelliJ IDEA平臺下JNI編程(一)—HelloWorld篇》)。再點擊運行,會發現錯處!!!!主要是因爲printf函數的第二個參數應當爲char*類型,而不是jstring。那怎麼樣將jstring轉爲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);
   /* 假設輸入字符不超過127個 */
   scanf("%s", buf);
   return (*env)->NewStringUTF(env, buf);
}

HelloJNI.c上點擊右鍵選擇External Tools>Generate DLL後,再運行HelloJNI.java類如下:

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

可以看到,通過JNIEnv對象我們可以將jstringchar*相互轉換。但需要注意的是,通過GetStringUTFChars函數將jsting轉爲char*時,有可能會從堆空間中分配新的空間,這就有可能因爲內存不足而分配失敗,因此需要判斷是否爲NULL。同時,當不需要時應當將這塊新分配的空間釋放,即調用ReleaseStringUTFChars函數。也可以在本地方法中構造一個java.lang.String實例對象,通過NewStringUTF函數來構造。

可能有人會問,爲什麼通過GetStringUTFChars得到的char*需要釋放內存,而通過NewStringUTF得到的jstring對象不用釋放內存呢?這是因爲,使用GetStringUTFChars得到char*是在堆中開闢了新的空間用於存儲字符串,用完肯定需要手動回收,因爲JVM並不會幫你回收本地方法中開闢的空間。而使用NewStringUTF創建的jstring對象屬於java.lang.String實例對象,虛擬機會自動回收,另外用於轉換爲jstring的char*對象(即例子中的buf)由於是函數內部的局部變量,當Java_com_huachao_java_HelloJNI_sayHello執行結束後,自然會回收其內部所有的局部變量的空間。

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

2. 字符串

除了上面小節中介紹的幾個與字符串相關的函數以外,JNIEnv中還定義了很多其他的與字符串相關操作的函數。

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

我們看看 GetStringChars函數原型:

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

如果返回的字符串是從原始的java.lang.String實例中拷貝的數據,則第三個參數isCopy指向的內存會被設置爲 JNI_TRUE,反之,如果返回的字符串是通過直接指向java.lang.String實例中的內存空間,則isCopy指向的內存會被設置爲JNI_FALSE。當isCopy指向的內存存儲的是JNI_FALSE,那麼不能對返回的char*中的內容進行修改,因爲在JavaString是不可變的對象。

大部分情況下,直接將NULL作爲isCopy參數,因爲大部分情況都不需要關心JVM是從java.lang.String中拷貝的字符串還是直接將指針指向原始的字符串。

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

調用GetStringChars函數後,當你不再使用該字符串時,還需要記得調用ReleaseStringChars 。物理isCopy指向的內容是 JNI_TRUE還是JNI_FALSE,都應當調用ReleaseStringChars 。 如果GetStringChars採用的是拷貝方式,則ReleaseStringChars釋放拷貝字符串佔用的空間;如果GetStringChars是直接修改指針的方式,則將java.lang.String實例對象取消固定(即可被在內存中移動)。

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

3. 數組

3.1 基本類型數組

JNI將基本類型數組與對象數組區分對待,基本類型數組主要是元素爲基本類型,對象數組元素是引用類型數組。如下:

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

iarrfarr是基本類型數組,而oarrarr2是對象數組。

本地方法中訪問基本類型數組就像訪問字符串一樣需要藉助JNI中的函數,如下爲一個簡單的例子:對int數組元素求和:

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");
    }
}

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

/* 以下代碼有錯誤 */
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函數來訪問基本來下數組中的元素,如下所示:

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 函數來複制數組中的元素到C語言中的緩存中(buf),其中第三個參數表示起始下標,第四個參數表示複製的元素個數。 只要元素拷貝到C緩存中,本地代碼就可以直接使用緩存中的數組了。 前面例子中沒有異常檢查,這是因爲我們知道數組長度是10,所有不會越界。

JNI支持一套數組的Get和Set函數==> Get/Release<Type>ArrayElements (如: Get/ReleaseIntArrayElements),用於本地代碼直接獲取基本類型數組的指針。由於垃圾回收器的底層實現可能不支持數組對象在內存中固定不動,所以在垃圾回收過程中數組在內存位置發生變化,JVM返回的指針是原始的基本類型數組的拷貝的地址。 上面代碼可以改爲如下:

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 函數返回數組中的元素個數,數組的固定長度在第一次分配內存時確定。函數 Get/ReleasePrimitiveArrayCritical允許虛擬機在訪問原始數組時禁用垃圾回收器。你應該像使用 Get/ReleaseStringCritical一樣小心地使用Get/ReleasePrimitiveArrayCritical。在Get/ReleasePrimitiveArrayCritical之間的代碼必須不能調用任何JNI函數或執行任何阻塞操作,因爲這可能會導致應用死鎖。

一般使用Get/Release<type>ArrayElements都是安全的,虛擬機要麼直接返回數組元素的指針,幺妹返回數組元素拷貝後的地址指針。

JNI Function Description Since
Get<Type>ArrayRegion Set<Type>ArrayRegion 從本地分配的基本類型數組中或者複製/賦值 JDK1.1
Get<Type>ArrayElements Release<Type>ArrayElements 獲取基本類型數組指針,可能返回的是拷貝 JDK1.1
GetArrayLength 返回數組中元素個數 JDK1.1
New<Type>Array 根據指定的長度創建數組 JDK1.1
GetPrimitiveArrayCritical ReleasePrimitiveArrayCritical 獲取或釋放基本類型數組指針,可能會禁用垃圾回收,可能返回的是數組的拷貝 Java 2 SDK1.2

3.2 對象數組

函數GetObjectArrayElement返回指定下標的元素,而 SetObjectArrayElement函數更新指定下包的元素。不像基本類型數組,我們無法一次性獲取所有的對象數組中的元素或者是拷貝多個元素。字符串和數組都是引用類型,可以通過 Get/SetObjectArrayElement來訪問數組中的字符串和數組中的數組。 如下代碼示例爲本地方法創建二維int數組後返回到Java代碼中,並且打印該二維數組數組內容:

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");
    }
}

對應的本地代碼實現如下:

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;
}

本地方法中,先調用了JNI函數FindClass來獲取二維數組中元素類型(Class)的引用,上一章中我們介紹過類型映射,我們知道[I表示的是Java中int[]對象的類型。如果FindClass返回NULL,說明類加載失敗(可能是因爲類文件不存在或者是OOM)。接下來NewObjectArray 函數分配一個數組,其元素類型爲intArrCls只向的引用類型。NewObjectArray 函數只能分配一維數組,我們將一維數組作爲其元素類型,這樣就構成了二維數組。JVM並沒有指定多維數組的數據結構,二維數組只是元素類型爲數組的數組。

運行結果如下:

0 1 2
1 2 3
2 3 4

上面例子中最外面的循環後面調用了DeleteLocalRef ,這是爲了防止虛擬機一直持有JNI中的引用(如例子中的iarr)導致OOM。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章