Java 之JNI基礎篇(三)

從源碼開始

上一篇博客講了Java代碼如何調用本地C代碼,這一篇則主要講C代碼如何訪問Java的變量和方法,那麼我們繼續從JDK源碼入手,從源碼中學習,學會JNI真正的使用方式和使用場景,而不是想當然的寫幾個簡單的demo,那樣是沒有意義的,知道API和會用API並不是一回事,源碼的JNI使用方式是高效的經得起考驗的,如果我們僅僅只是看了幾個demo,查了一下文檔就去寫JNI,那樣很可能就是在給自己埋坑。

好了,進入主題,看到我們今天的主角,使用Java的zip壓縮與解壓,之所以從這裏入手,是因爲此處的代碼邏輯比較簡單,且正真實現壓縮與解壓的是本地C庫,所以非常適合學習JNI,以及Java與C的大數據量交互。
打開openjdk的Java源碼/jdk/src/share/classes/java/util/zip/Deflater.java
這裏寫圖片描述

可以看到,在該類的開頭就做了一個介紹,並貼心的給出了一段demo,實際上這個demo太過於簡單,真實的代碼中並不是這樣用,我們這裏就以此demo做個小測試,簡單改造一下

try {
        // Encode a String into bytes
        String inputString = "Beautiful is better than ugly.Explicit is better than implicit.";

        byte[] input = inputString.getBytes();
        System.out.println("原始長度:"+input.length);

        // Compress the bytes
        byte[] output = new byte[100];
        Deflater compresser = new Deflater();
        compresser.setInput(input);
        compresser.finish();
        int compressedDataLength = compresser.deflate(output);
        compresser.end();
        System.out.println("壓縮之後:"+compressedDataLength);

        // Decompress the bytes
        Inflater decompresser = new Inflater();
        decompresser.setInput(output, 0, compressedDataLength);
        byte[] result = new byte[100];
        int resultLength = decompresser.inflate(result);
        decompresser.end();
        System.out.println("解壓長度:"+resultLength);
        // Decode the bytes into a String
        String outputString = new String(result, 0, resultLength);
        System.out.println(outputString);
    } catch (Exception ex) {
        ex.printStackTrace();
    } 

這一段程序主要做了兩件事,先壓縮字節,再解壓恢復,這裏壓縮使用Deflater 類,而解壓則用到Inflater 類,從名字也很好理解。大家可以運行上述代碼,看看一行字符串,壓縮之後減少了多少字節。

首先將上述程序分開來看,先看壓縮部分,主要幹事的代碼如下

Deflater compresser = new Deflater();
compresser.setInput(input);
compresser.finish();
int compressedDataLength = compresser.deflate(output);
compresser.end();

源碼其實不是很多,這裏僅摘取相關部分

private final ZStreamRef zsRef;
private byte[] buf = new byte[0];
private int off, len;
private int level, strategy;
private boolean setParams;
private boolean finish, finished;

...

    static {
        /* Zip library is loaded from System.initializeSystemClass */
        initIDs();
    }

    /**
     * Sets input data for compression. This should be called whenever
     * needsInput() returns true indicating that more input data is required.
     * @param b the input data bytes
     * @param off the start offset of the data
     * @param len the length of the data
     * @see Deflater#needsInput
     */
    public void setInput(byte[] b, int off, int len) {
        if (b== null) {
            throw new NullPointerException();
        }
        if (off < 0 || len < 0 || off > b.length - len) {
            throw new ArrayIndexOutOfBoundsException();
        }
        synchronized (zsRef) {
            this.buf = b;
            this.off = off;
            this.len = len;
        }
    }

    /**
     * Sets input data for compression. This should be called whenever
     * needsInput() returns true indicating that more input data is required.
     * @param b the input data bytes
     * @see Deflater#needsInput
     */
    public void setInput(byte[] b) {
        setInput(b, 0, b.length);
    }

...

    /**
     * When called, indicates that compression should end with the current
     * contents of the input buffer.
     */
    public void finish() {
        synchronized (zsRef) {
            finish = true;
        }
    }

...

    /**
     * Compression flush mode used to achieve best compression result.
     *
     * @see Deflater#deflate(byte[], int, int, int)
     * @since 1.7
     */
    public static final int NO_FLUSH = 0;

    public int deflate(byte[] b) {
        return deflate(b, 0, b.length, NO_FLUSH);
    }

    public int deflate(byte[] b, int off, int len, int flush) {
        if (b == null) {
            throw new NullPointerException();
        }
        if (off < 0 || len < 0 || off > b.length - len) {
            throw new ArrayIndexOutOfBoundsException();
        }
        synchronized (zsRef) {
            ensureOpen();
            if (flush == NO_FLUSH || flush == SYNC_FLUSH ||
                flush == FULL_FLUSH)
                return deflateBytes(zsRef.address(), b, off, len, flush);
            throw new IllegalArgumentException();
        }
    }

...

    private static native void initIDs();
    private native static long init(int level, int strategy, boolean nowrap);
    private native static void setDictionary(long addr, byte[] b, int off, int len);
    private native int deflateBytes(long addr, byte[] b, int off, int len,
                                    int flush);
    private native static int getAdler(long addr);
    private native static long getBytesRead(long addr);
    private native static long getBytesWritten(long addr);
    private native static void reset(long addr);
    private native static void end(long addr);

簡單瀏覽一下代碼就知道,前幾行代碼其實都是一些準備工作,真正做壓縮的是deflate方法,而對應的其實是native修飾的deflateBytes方法。

我們打開/jdk/src/share/native/java/util/zip/Deflater.c 源碼 ,按照Java層的代碼調用順序看

JNI訪問Java成員變量

  • Java靜態代碼塊
static {
       initIDs();
}

本地實現

JNIEXPORT void JNICALL
Java_java_util_zip_Deflater_initIDs(JNIEnv *env, jclass cls)
{
    levelID = (*env)->GetFieldID(env, cls, "level", "I");
    strategyID = (*env)->GetFieldID(env, cls, "strategy", "I");
    setParamsID = (*env)->GetFieldID(env, cls, "setParams", "Z");
    finishID = (*env)->GetFieldID(env, cls, "finish", "Z");
    finishedID = (*env)->GetFieldID(env, cls, "finished", "Z");
    bufID = (*env)->GetFieldID(env, cls, "buf", "[B");
    offID = (*env)->GetFieldID(env, cls, "off", "I");
    lenID = (*env)->GetFieldID(env, cls, "len", "I");
}

這個初始化方法是幹什麼的呢?我們從最直觀的角度就能判斷,這個方法應該是在獲取類的成員變量,因爲這個方法裏出現了Deflater類的成員變量level、strategy、finish等等,再看到函數名GetFieldID,如果非常熟悉Java反射機制的話,那麼對於Java的Field域是再熟悉不過了,這一段代碼就跟Java反射獲取類成員是高度相似的,唯一的不同是,這裏獲取的Field的ID

在JNI官方文檔中查詢GetFieldID
這裏寫圖片描述

通過閱讀文檔,我們可以知道,本地方法訪問Java實例變量,有兩個步驟。 首先,調用GetFieldID函數獲取字段的ID,第二步,在以上示例代碼中則是調用GetObjectClass函數來訪問實例變量。

看到函數原型

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); 

前三個參數都很好理解,由於Java中initIDs方法是static方法,所有這裏第二個參數傳的是jclass類型,主要是最後一個參數不好理解,該參數實際上是一個Field描述符,或者理解爲一種類型簽名,實際上它是一個字符串,8種基本類型和引用類型都有對應的類型簽名,先看一下八種基本類型,最後再做一下總結

Z  boolean  
B  byte  
C  char  
S  short  
I  int  
J  long  
F  float  
D  double  

其中帶”[“的表示數組,如示例中bufID = (*env)->GetFieldID(env, cls, "buf", "[B");

回到Deflater.c 源碼,在函數的外層定義了這些全局靜態變量,由此很好理解在Java靜態代碼塊所做的事情,實際上就是在類加載的時候,將類成員的FieldId保存到本地代碼的全局靜態變量中,這樣做的目的就是提高效率,以免本地代碼訪問類成員時頻繁獲取FieldId

static jfieldID levelID;
static jfieldID strategyID;
static jfieldID setParamsID;
static jfieldID finishID;
static jfieldID finishedID;
static jfieldID bufID, offID, lenID;
  • 真正處理壓縮的方法
    在Deflater類的構造方法中也做了一些事情,主要是對native層的zlib庫的一些準備,關於zlib庫如何使用就不在本文討論範圍了,實際上也就是函數的調用,看一下zlib庫的示例很好理解,接下來看到關鍵代碼
JNIEXPORT jint JNICALL
Java_java_util_zip_Deflater_deflateBytes(JNIEnv *env, jobject this, jlong addr,
                                         jarray b, jint off, jint len, jint flush)
{
    z_stream *strm = jlong_to_ptr(addr);

    /* 通過之前加載的FieldId 獲取類成員變量*/
    jarray this_buf = (*env)->GetObjectField(env, this, bufID);
    jint this_off = (*env)->GetIntField(env, this, offID);
    jint this_len = (*env)->GetIntField(env, this, lenID);
    jbyte *in_buf;
    jbyte *out_buf;
    int res;
    if ((*env)->GetBooleanField(env, this, setParamsID)) {
        int level = (*env)->GetIntField(env, this, levelID);
        int strategy = (*env)->GetIntField(env, this, strategyID);

        /* 獲取待壓縮的字節數組*/
        in_buf = (*env)->GetPrimitiveArrayCritical(env, this_buf, 0);
        if (in_buf == NULL) {
            // Throw OOME only when length is not zero
            if (this_len != 0)
                JNU_ThrowOutOfMemoryError(env, 0);
            return 0;
        }

        /* 獲取壓縮後輸出的字節數組*/
        out_buf = (*env)->GetPrimitiveArrayCritical(env, b, 0);
        if (out_buf == NULL) {
            (*env)->ReleasePrimitiveArrayCritical(env, this_buf, in_buf, 0);
            if (len != 0)
                JNU_ThrowOutOfMemoryError(env, 0);
            return 0;
        }

        /* 調用zlib庫做壓縮之前,需要做的一些準備工作*/
        strm->next_in = (Bytef *) (in_buf + this_off);
        strm->next_out = (Bytef *) (out_buf + off);
        strm->avail_in = this_len;
        strm->avail_out = len;

        /* C庫中,真正做壓縮的方法,調用完成後返回一個結果值,用以判斷是否成功*/
        res = deflateParams(strm, level, strategy);
        /* 釋放資源*/
        (*env)->ReleasePrimitiveArrayCritical(env, b, out_buf, 0);
        (*env)->ReleasePrimitiveArrayCritical(env, this_buf, in_buf, 0);

        switch (res) {
        case Z_OK:
            (*env)->SetBooleanField(env, this, setParamsID, JNI_FALSE);
            this_off += this_len - strm->avail_in;
            (*env)->SetIntField(env, this, offID, this_off);
            (*env)->SetIntField(env, this, lenID, strm->avail_in);
            return len - strm->avail_out;
        case Z_BUF_ERROR:
            (*env)->SetBooleanField(env, this, setParamsID, JNI_FALSE);
            return 0;
        default:
            JNU_ThrowInternalError(env, strm->msg);
            return 0;
        }
    } else {
        /* 此處省略另一種策略下的壓縮邏輯,實際上這個else中的邏輯與if中的類似,只是具體調用的方法不同*/
    }
}
從這段源碼中,可以學會大數據量數組的傳遞,以及在本地方法中訪問Java變量並修改,特別關鍵的一點是可以學習標準的JNI編寫套路,這裏Java層的數組並不是直接通過參數傳遞到本地代碼中的,而是先將字節數組的引用保存到成員變量中,在本地代碼中去獲取這個類成員變量,從而獲取到該數組的指針,前面的博客已經詳細說明過,`GetPrimitiveArrayCritical`並不一定獲取到原始數組的指針,也可能是副本的指針,所以一定要調用`ReleasePrimitiveArrayCritical`進行緩衝區釋放,回寫數組。

訪問Java成員變量小結

從這個源碼示例進行引申,將JNI官方文檔中,對於Java實例變量與靜態變量做一個歸納總結
  • 獲取實例變量的FieldID

函數原型

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); 
  • 通過FieldID獲取實例變量值
    根據8種基本類型和引用類型,分別對應不同的函數

函數原型

NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID); 

這裏寫圖片描述

  • 修改實例變量的值

函數原型

void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID,NativeType value); 

這裏寫圖片描述

除了實例變量,還有用static修飾的靜態變量,看看靜態變量如何訪問

  • 獲取靜態變量的FieldID
    可以看到,和獲取實例變量的使用是一樣的,只是函數名不同

函數原型

jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); 
  • 通過FieldID獲取靜態變量值
    獲取靜態變量值和獲取實例變量值稍有一點不同,可以看該函數的第二參數不是jobject類型,而是jclass類型,這說明該函數調用的時候需要一個字節碼對象,而不是類的實例對象

函數原型

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz,jfieldID fieldID); 

這裏寫圖片描述

  • 修改靜態變量的值

函數原型

void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value); 

這裏寫圖片描述

在JNI本地代碼中創建Java對象和反調Java方法

由於Java的JNI接口被設計出來,主要是用於提升Java性能,複用已有的C/C++庫,所以在JDK中沒有很好的示例學習在本地代碼中創建Java對象和反調Java方法,然而這樣的情況在Android NDK開發中則大爲不同,JNI成爲了本地運行環境和Java層的重要橋樑,因而JNI在Android平臺被發揚光大了。在後續的Android NDK相關博客中再從源碼角度學習,這裏先從官方文檔示例入手學習

首先我們回顧一下Java反射機制,在我的Java入門基礎博客裏曾有寫過

所謂反射,簡單說就是通過一個class文件對象來使用該class文件中的構造方法,成員變量,成員方法。獲取Class類對象通常有三種方法

//1.Object類中的getClass()方法
Person p = new Person();
Class c1 = p.getClass();

//2.通過數據類型的一個靜態的class屬性
Class c2= Person.class;

//3.通過Class類的一個靜態方法forName()
Class c3 = Class.forName("com.test.Person");

在Java中,通常得到了Class對象就可以反射構造方法從而創建一個該類的實例對象,看到文檔給出的示例

jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
    jclass stringClass;
    jmethodID cid;
    jcharArray elemArr;
    jstring result;

    /*獲取jclass對象*/
    stringClass = (*env)->FindClass(env, "java/lang/String");
    if (stringClass == NULL) {
        return NULL; 
    }

    /*獲取 String(char[]) 構造函數的MethodID*/
    cid = (*env)->GetMethodID(env, stringClass,"<init>", "([C)V");

    if (cid == NULL) {
        return NULL;
    }

    /*創建一個char[]數組作爲String類的內容 */
    elemArr = (*env)->NewCharArray(env, len);

    if (elemArr == NULL) {
        return NULL; 
    }
    (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars);

    /*創建一個java.lang.String對象 */
    result = (*env)->NewObject(env, stringClass, cid, elemArr);

    /* 釋放本地引用*/
    (*env)->DeleteLocalRef(env, elemArr);
    (*env)->DeleteLocalRef(env, stringClass);
    return result;
}

從這個示例中可以看出,本地代碼創建對象的步驟和Java反射其實是一致的
1. 獲得字節碼文件對象,即獲得Class對象
2. 獲得類的成員屬性、方法或者構造函數
3. 調用構造函數創建對象

我們通過查詢文檔,學習示例中的相關方法

  • 獲取jclass對象
    FindClass函數類似Java中的forName方法,傳入完整包名類名,注意中間使用”/”分隔符,該方法加載的是CLASSPATH路徑下的類

函數原型

jclass FindClass(JNIEnv *env, const char *name);

除了該函數外,還有另一個函數也可以用來或許jclass對象,這一點和Java的getClass()方法也是高度相似的,都是通過一個實例對象獲取自身的class對象

jclass GetObjectClass(JNIEnv *env, jobject obj); 
  • 獲取MethodID
    與獲取FieldID一樣,調用Java的方法,也需要先得到MethodID

函數原型

jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); 

該函數的使用與GetFieldID是一樣的,最後兩個參數傳入方法名和方法簽名,需要注意的一點是,如果獲取的是構造方法,那麼方法名統一使用"<init>",這裏還有一個知識點就是方法簽名的使用,我們在最後做一個總結

  • 調用構造方法創建對象

函數原型

jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID,...); 

可以看到這個函數後面是不定參,傳入的正是Java構造方法中需要的參數。處理NewObject函數用來創建對象,還有另外兩個函數可用,只是傳參的形式稍有不同,一個是傳入jvalue數組,另一個則傳入va_list 類型

jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args); 
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args); 

接下再看看如何調用Java成員方法,先看的官方示例

class InstanceMethodCall {
    private native void nativeMethod();
    private void callback() {
        System.out.println("In Java");
    }
    public static void main(String args[]) {
        InstanceMethodCall c = new InstanceMethodCall();
        c.nativeMethod();
    }
    static {
        System.loadLibrary("InstanceMethodCall");
    }
}
JNIEXPORT void JNICALL
Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj)
{
    jclass cls = (*env)->GetObjectClass(env, obj);
    jmethodID mid = (*env)->GetMethodID(env, cls, "callback", "()V");
    if (mid == NULL) {
        return; /* method not found */
    }
    printf("In C\n");
    (*env)->CallVoidMethod(env, obj, mid);
}

與訪問成員變量步驟相同,也是兩步,首先獲得Java方法的MethodID,最後調用JNI函數執行該方法。此示例中,調用的是CallVoidMethod函數,它表示執行一個無返回值的Java方法。我們知道Java方法返回值除了Void類型,還可以是8種基本類型和引用類型,下面看到Call函數的原型

<NativeType> Call<Type>Method(JNIEnv *env,jobject obj, jmethodID methodID, ...);

後面的不定參表示被調用的Java方法需要傳入的參數
這裏寫圖片描述

除了實例方法,Java還有靜態方法,靜態方法的調用步驟與實例方法也是相同的

  • 獲取MethodID
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig); 
  • 根據MethodID執行Java方法
<NativeType> CallStatic<Type>Method(JNIEnv *env, jclass clazz,jmethodIDmethodID, ...);

這裏寫圖片描述

在本地代碼調用Java方法中,最後還有一個點需要注意,即在父類中定義但在子類中已被覆蓋的實例方法。在Java中我們直接通過關鍵字super.來調用,在JNI中也有單獨處理,而調用步驟和上面講的也是相同的,都是先獲取MethodID,只是執行Java方法的函數是不同的

NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...); 

這裏寫圖片描述

Java類型簽名與方法簽名總結

在前面無論是訪問Java成員變量還是調用Java方法,都涉及到一個描述符的東西,也就是簽名,它是一個字符串,有固定的寫法規則

  • 類型簽名
    這裏寫圖片描述
    這裏寫圖片描述

可以看到,引用類型必須以L開頭,並帶上完整包名類名,其包名中的分隔符使用/,而數組則必須以[開頭,如果是二維數組,則用[[,三維數組用[[[,最後還有需要特別注意的一點,簽名的後面一定要以;結尾

  • 方法簽名
    與簽名的類型前不同的是,方法簽名是有返回值的,其固定格式如下

    (形參列表)返回值類型
    

    需要注意的是,形參之間是沒有分隔符號的,緊跟在一起
    這裏寫圖片描述

當我們手寫方法簽名的時候很容易出錯,JDK是有提供工具自動生成簽名的,就和javah工具一樣,生成簽名的工具是javap命令,如需生成InstanceMethodCall類的簽名,執行如下命令
javap -s -p InstanceMethodCall

JNI字符串與對象數組的操作

在上一篇博客我們談了基本類型數組的使用,這一篇的最後就補充一下關於字符串和對象類型數組的使用

  • 字符串操作
    先看到文檔的示例代碼
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
    char buf[128];
    const jbyte *str;
    str = (*env)->GetStringUTFChars(env, prompt, NULL);
    if (str == NULL) {
        return NULL; /* OutOfMemoryError already thrown */
    }
    printf("%s", str);
    (*env)->ReleaseStringUTFChars(env, prompt, str);

    /* We assume here that the user does not type more than 127 characters */
    scanf("%s", buf);
    return (*env)->NewStringUTF(env, buf);
}

實際上在C語言中是沒有字符串類型的,字符串就是一個字符數組,因此對於字符串的操作,和數組是非常類似的,但是爲了和Java的類型對接起來,JNI對Java的字符串提供了專有的函數操作,我們學習了前面的基本類型數組的操作,對於字符串的操作函數就非常好理解了。

從官方示例中可以看出來,這段代碼進行了兩種操作,1.獲取Java層傳下來的字符串,並轉換爲本地代碼中的字符數組;2.在本地代碼中創建一個Java字符串對象返回。

const jbyte * GetStringUTFChars(JNIEnv *env,jstring string, jboolean *isCopy);

如果有可能,該函數會返回一個指向Java字符串的UTF-8類型字符數組的指針,最後一個參數,我們在之前講數組操作時已經講過,注意傳值方式,傳入的是一個指針。它我們之前探討的數組操作一樣,也有可能會返回一個副本的指針,因此在最後一定要調用對應的Release函數釋放副本,回寫改變後的值到Java原始字符串中。

void ReleaseStringUTFChars(JNIEnv *env,jstring string, const char *utf);

與我們之前創建的字符串對象不同,這裏沒有去獲取MethodID然後調用構造方法去創建,而是使用NewStringUTF函數,從UTF-8字符數組構去造一個新的String對象

jstring NewStringUTF(JNIEnv *env, const char *bytes); 

除了這些函數之外,還有一些字符串操作的函數,這些函數基本上與數組操作函數完全對應

JNI Function Description Since
GetStringChars
ReleaseStringChars
以Unicode格式獲取或釋放指向字符串內容的指針。 可能會返回字符串的副本。 JDK1.1
GetStringUTFChars
ReleaseStringUTFChars
獲取或釋放指向UTF-8格式的字符串內容的指針。可能返回字符串的副本。 JDK1.1
GetStringLength 返回Unicode字符串中的字符長度。 JDK1.1
GetStringUTFLength 獲取 UTF-8 編碼字符串的長度,也可以通過標準 C 函數 strlen 獲取 JDK1.1
NewString 通過給定的Unicode C字符串創建一個Java String字符串 JDK1.1
NewStringUTF 通過給定的UTF-8編碼C字符串創建一個Java String字符串 JDK1.1
GetStringCritical
ReleaseStringCritical
獲取Unicode格式的字符串內容的指針。 可能會返回字符串的副本。 與我們之前數組操作一樣,在Get / ReleaseStringCritical調用之間不得進行阻塞操作 JDK1.2
GetStringRegion
SetStringRegion
以Unicode格式將字符串的內容複製到預分配的C緩衝區或從預分配的C緩衝區中回寫修改的內容到原始字符串中。 JDK1.2
GetStringUTFRegion
SetStringUTFRegion
以UTF-8 格式將字符串的內容複製到預分配的C緩衝區或從預分配的C緩衝區中回寫修改的內容到原始字符串中。 JDK1.2
  • 對象數組
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index); 
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value); 
JNIEXPORT jobjectArray JNICALL Java_com_test_modifyStringArr(JNIEnv * env, jclass jc,jobjectArray strArray )
{
    jstring jstr;
    jsize len = (*env)->GetArrayLength(env, strArray);
    char **pstr = (char **) malloc(len*sizeof(char *));

    int i=0;      
    for (i=0 ; i< len ;i++){
        jstr = (*env)->GetObjectArrayElement(env, strArray, i);
        *pstr[i] = (char *)(*env)->GetStringUTFChars(env, jstr, 0);                     
    }    

    jclass stringClass = (*env)->FindClass(env, "java/lang/String");
    jobjectArray  strArr = (*env)->NewObjectArray(env, len , stringClass, NULL); 
    if( strArr  == NULL){
        return NULL;
    }   

     for (i=0 ; i< len ;i++){
        jstring js = (*env)->NewStringUTF(env,*pstr[i])
        (*env)->SetObjectArrayElement(env, strArr , i, iarr,js);
     }  

     free(pstr);
     return strArr ;
}
  • JNI字符串操作亂碼問題
    由於編碼不統一就會出現亂碼問題,本地代碼中,中文等使用的是Unicode 編碼,而在Java中通常使用UTF-8編碼,解決亂碼問題需要進行編碼轉換,在本地代碼進行編碼轉換通常會比較麻煩。一般有兩種比較簡便的解決方法
    1.調用Java層String的構造方法指定編碼表進行轉換 String(byte[] data, String charsetName)
    2.使用LibIconv庫,在本地代碼中進行轉換。在NDK開發中已經支持了該庫,如需在Windows上使用,可進入官網下載靜態或動態庫 地址 該庫的使用也非常簡單

到此,Java中的JNI的基本使用結束,還有一部分知識點,將在接下來的Android JNI編程中結合NDK 進行總結

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