鍍金池/ 教程/ Java/ JNI 字符串處理
Android NDK 開發(fā)環(huán)境
Android NDK 簡(jiǎn)介
JNI 字符串處理
JVM 查找 native 方法的規(guī)則
JNI 開發(fā)流程
JNI 局部引用、全局引用和弱全局引用
JNI 數(shù)據(jù)類型與 Java 數(shù)據(jù)類型的映射關(guān)系
JNI 概述
JNI 調(diào)用性能測(cè)試及優(yōu)化
C/C++ 訪問 Java 實(shí)例變量和靜態(tài)變量
JNI 調(diào)用構(gòu)造方法和父類實(shí)例方法
C/C++ 訪問 Java 實(shí)例方法和靜態(tài)方法
開發(fā)自己的 NDK 程序
JNI 訪問數(shù)組

JNI 字符串處理

處理字符串

從第三章中可以看出 JNI 中的基本類型和 Java 中的基本類型都是一一對(duì)應(yīng)的,接下來先看一下 JNI 的基本類型定義:

typedef unsigned char   jboolean;  
typedef unsigned short  jchar;  
typedef short           jshort;  
typedef float           jfloat;  
typedef double          jdouble;  
typedef int jint;  
#ifdef _LP64 /* 64-bit Solaris */  
typedef long jlong;  
#else  
typedef long long jlong;  
#endif  

typedef signed char jbyte;  

基本類型很容易理解,就是對(duì) C/C++ 中的基本類型用 typedef 重新定義了一個(gè)新的名字,在 JNI 中可以直接訪問。JNI 把 Java 中的所有對(duì)象當(dāng)作一個(gè)C指針傳遞到本地方法中,這個(gè)指針指向 JVM 中的內(nèi)部數(shù)據(jù)結(jié)構(gòu),而內(nèi)部的數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中的存儲(chǔ)方式是不可見的。只能從 JNIEnv 指針指向的函數(shù)表中選擇合適的 JNI 函數(shù)來操作 JVM 中的數(shù)據(jù)結(jié)構(gòu)。第三章的示例中,訪問 java.lang.String 對(duì)應(yīng)的 JNI 類型 jstring 時(shí),沒有像訪問基本數(shù)據(jù)類型一樣直接使用,因?yàn)樗?Java 是一個(gè)引用類型,所以在本地代碼中只能通過 GetStringUTFChars 這樣的 JNI 函數(shù)來訪問字符串的內(nèi)容。

下面先看一個(gè)例子:

Sample.java:

package com.study.jnilearn;  

public class Sample {  

    public native static String sayHello(String text);  

    public static void main(String[] args) {  
        String text = sayHello("yangxin");  
        System.out.println("Java str: " + text);  
    }  

    static {  
        System.loadLibrary("Sample");  
    }  
}  

com_study_jnilearn_Sample.h和Sample.c:

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

#ifndef _Included_com_study_jnilearn_Sample  
#define _Included_com_study_jnilearn_Sample  
#ifdef __cplusplus  
extern "C" {  
#endif  
/* 
 * Class:     com_study_jnilearn_Sample 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello  
  (JNIEnv *, jclass, jstring);  

#ifdef __cplusplus  
}  
#endif  
#endif  

// Sample.c  
#include "com_study_jnilearn_Sample.h"  
/* 
 * Class:     com_study_jnilearn_Sample 
 * Method:    sayHello 
 * Signature: (Ljava/lang/String;)Ljava/lang/String; 
 */  
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello  
  (JNIEnv *env, jclass cls, jstring j_str)  
{  
    const char *c_str = NULL;  
    char buff[128] = {0};  
    jboolean isCopy;    // 返回JNI_TRUE表示原字符串的拷貝,返回JNI_FALSE表示返回原字符串的指針  
    c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy);  
    printf("isCopy:%d\n",isCopy);  
    if(c_str == NULL)  
    {  
        return NULL;  
    }  
    printf("C_str: %s \n", c_str);  
    sprintf(buff, "hello %s", c_str);  
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);  
    return (*env)->NewStringUTF(env,buff);  
}  

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

http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/images/result.png" alt="" />

示例解析

訪問字符串

sayHello 函數(shù)接收一個(gè) jstring 類型的參數(shù) text,但 jstring 類型是指向 JVM 內(nèi)部的一個(gè)字符串,和 C 風(fēng)格的字符串類型 char* 不同,所以在 JNI 中不能通把 jstring 當(dāng)作普通 C 字符串一樣來使用,必須使用合適的 JNI 函數(shù)來訪問 JVM 內(nèi)部的字符串?dāng)?shù)據(jù)結(jié)構(gòu)。

GetStringUTFChars(env, j_str, &isCopy) 參數(shù)說明:

  • env:JNIEnv 函數(shù)表指針
  • j_str:jstring 類型(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ī)定。但我們?cè)陂_發(fā)當(dāng)中,并不關(guān)心這個(gè)值是多少,通常情況下這個(gè)參數(shù)填 NULL 即可。

因?yàn)?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)容。

異常檢查

調(diào)用完 GetStringUTFChars 之后不要忘記安全檢查,因?yàn)?JVM 需要為新誕生的字符串分配內(nèi)存空間,當(dāng)內(nèi)存空間不夠分配的時(shí)候,會(huì)導(dǎo)致調(diào)用失敗,失敗后 GetStringUTFChars 會(huì)返回 NULL,并拋出一個(gè)OutOfMemoryError 異常。JNI 的異常和 Java 中的異常處理流程是不一樣的,Java 遇到異常如果沒有捕獲,程序會(huì)立即停止運(yùn)行。而 JNI 遇到未決的異常不會(huì)改變程序的運(yùn)行流程,也就是程序會(huì)繼續(xù)往下走,這樣后面針對(duì)這個(gè)字符串的所有操作都是非常危險(xiǎn)的,因此,我們需要用 return 語句跳過后面的代碼,并立即結(jié)束當(dāng)前方法。

釋放字符串

在調(diào)用 GetStringUTFChars 函數(shù)從 JVM 內(nèi)部獲取一個(gè)字符串之后,JVM 內(nèi)部會(huì)分配一塊新的內(nèi)存,用于存儲(chǔ)源字符串的拷貝,以便本地代碼訪問和修改。即然有內(nèi)存分配,用完之后馬上釋放是一個(gè)編程的好習(xí)慣。通過調(diào)用ReleaseStringUTFChars 函數(shù)通知 JVM 這塊內(nèi)存已經(jīng)不使用了,你可以清除了。注意:這兩個(gè)函數(shù)是配對(duì)使用的,用了 GetXXX 就必須調(diào)用 ReleaseXXX,而且這兩個(gè)函數(shù)的命名也有規(guī)律,除了前面的 Get 和 Release 之外,后面的都一樣。

創(chuàng)建字符串

通過調(diào)用 NewStringUTF 函數(shù),會(huì)構(gòu)建一個(gè)新的 java.lang.String 字符串對(duì)象。這個(gè)新創(chuàng)建的字符串會(huì)自動(dòng)轉(zhuǎn)換成 Java 支持的 Unicode 編碼。如果 JVM 不能為構(gòu)造 java.lang.String 分配足夠的內(nèi)存,NewStringUTF 會(huì)拋出一個(gè) OutOfMemoryError 異常,并返回 NULL。在這個(gè)例子中我們不必檢查它的返回值,如果NewStringUTF 創(chuàng)建 java.lang.String 失敗,OutOfMemoryError 這個(gè)異常會(huì)被在 Sample.main 方法中拋出。如果 NewStringUTF 創(chuàng)建 java.lang.String 成功,則返回一個(gè) JNI 引用,這個(gè)引用指向新創(chuàng)建的java.lang.String 對(duì)象。

其它字符串處理函數(shù)

GetStringCharsReleaseStringChars

這對(duì)函數(shù)和 Get/ReleaseStringUTFChars 函數(shù)功能差不多,用于獲取和釋放以 Unicode 格式編碼的字符串。后者是用于獲取和釋放 UTF-8 編碼的字符串。

GetStringLength

由于 UTF-8 編碼的字符串以'\0'結(jié)尾,而 Unicode 字符串不是。如果想獲取一個(gè)指向 Unicode 編碼的 jstring 字符串長(zhǎng)度,在 JNI 中可通過這個(gè)函數(shù)獲取。

GetStringUTFLength

獲取 UTF-8 編碼字符串的長(zhǎng)度,也可以通過標(biāo)準(zhǔn) C 函數(shù) strlen 獲取。

GetStringCriticalReleaseStringCritical

提高 JVM 返回源字符串直接指針的可能性。

Get/ReleaseStringChars 和 Get/ReleaseStringUTFChars 這對(duì)函數(shù)返回的源字符串會(huì)后分配內(nèi)存,如果有一個(gè)字符串內(nèi)容相當(dāng)大,有 1M 左右,而且只需要讀取里面的內(nèi)容打印出來,用這兩對(duì)函數(shù)就有些不太合適了。此時(shí)用 Get/ReleaseStringCritical 可直接返回源字符串的指針應(yīng)該是一個(gè)比較合適的方式。不過這對(duì)函數(shù)有一個(gè)很大的限制,在這兩個(gè)函數(shù)之間的本地代碼不能調(diào)用任何會(huì)讓線程阻塞或等待 JVM 中其它線程的本地函數(shù)或 JNI 函數(shù)。因?yàn)橥ㄟ^ GetStringCritical 得到的是一個(gè)指向 JVM 內(nèi)部字符串的直接指針,獲取這個(gè)直接指針后會(huì)導(dǎo)致暫停 GC 線程,當(dāng) GC 被暫停后,如果其它線程觸發(fā) GC 繼續(xù)運(yùn)行的話,都會(huì)導(dǎo)致阻塞調(diào)用者。所以在 Get/ReleaseStringCritical 這對(duì)函數(shù)中間的任何本地代碼都不可以執(zhí)行導(dǎo)致阻塞的調(diào)用或?yàn)樾聦?duì)象在 JVM 中分配內(nèi)存,否則,JVM 有可能死鎖。另外一定要記住檢查是否因?yàn)閮?nèi)存溢出而導(dǎo)致它的返回值為 NULL,因?yàn)?JVM 在執(zhí)行 GetStringCritical 這個(gè)函數(shù)時(shí),仍有發(fā)生數(shù)據(jù)復(fù)制的可能性,尤其是當(dāng) JVM 內(nèi)部存儲(chǔ)的數(shù)組不連續(xù)時(shí),為了返回一個(gè)指向連續(xù)內(nèi)存空間的指針,JVM 必須復(fù)制所有數(shù)據(jù)。下面代碼演示這對(duì)函數(shù)的正確用法:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello  
  (JNIEnv *env, jclass cls, jstring j_str)  
{  
    const jchar* c_str= NULL;  
    char buff[128] = "hello ";  
    char* pBuff = buff + 6;  
    /* 
     * 在GetStringCritical/RealeaseStringCritical之間是一個(gè)關(guān)鍵區(qū)。 
     * 在這關(guān)鍵區(qū)之中,絕對(duì)不能呼叫JNI的其他函數(shù)和會(huì)造成當(dāng)前線程中斷或是會(huì)讓當(dāng)前線程等待的任何本地代碼, 
     * 否則將造成關(guān)鍵區(qū)代碼執(zhí)行區(qū)間垃圾回收器停止運(yùn)作,任何觸發(fā)垃圾回收器的線程也會(huì)暫停。 
     * 其他觸發(fā)垃圾回收器的線程不能前進(jìn)直到當(dāng)前線程結(jié)束而激活垃圾回收器。 
     */  
    c_str = (*env)->GetStringCritical(env,j_str,NULL);   // 返回源字符串指針的可能性  
    if (c_str == NULL)  // 驗(yàn)證是否因?yàn)樽址截悆?nèi)存溢出而返回NULL  
    {  
        return NULL;  
    }  
    while(*c_str)   
    {  
        *pBuff++ = *c_str++;  
    }  
    (*env)->ReleaseStringCritical(env,j_str,c_str);  
    return (*env)->NewStringUTF(env,buff);  
}  

JNI 中沒有 Get/ReleaseStringUTFCritical 這樣的函數(shù),因?yàn)樵谶M(jìn)行編碼轉(zhuǎn)換時(shí)很可能會(huì)促使 JVM 對(duì)數(shù)據(jù)進(jìn)行復(fù)制,因?yàn)?JVM 內(nèi)部表示的字符串是使用 Unicode 編碼的。

GetStringRegionGetStringUTFRegion

分別表示獲取 Unicode 和 UTF-8 編碼字符串指定范圍內(nèi)的內(nèi)容。這對(duì)函數(shù)會(huì)把源字符串復(fù)制到一個(gè)預(yù)先分配的緩沖區(qū)內(nèi)。下面代碼用 GetStringUTFRegion 重新實(shí)現(xiàn) sayHello 函數(shù):

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello  
  (JNIEnv *env, jclass cls, jstring j_str)  
{  
    jsize len = (*env)->GetStringLength(env,j_str);  // 獲取unicode字符串的長(zhǎng)度  
    printf("str_len:%d\n",len);  
    char buff[128] = "hello ";  
    char* pBuff = buff + 6;  
    // 將JVM中的字符串以u(píng)tf-8編碼拷入C緩沖區(qū),該函數(shù)內(nèi)部不會(huì)分配內(nèi)存空間  
    (*env)->GetStringUTFRegion(env,j_str,0,len,pBuff);  
    return (*env)->NewStringUTF(env,buff);  
}

GetStringUTFRegion 這個(gè)函數(shù)會(huì)做越界檢查,如果檢查發(fā)現(xiàn)越界了,會(huì)拋出StringIndexOutOfBoundsException 異常,這個(gè)方法與 GetStringUTFChars 比較相似,不同的是,GetStringUTFRegion 內(nèi)部不分配內(nèi)存,不會(huì)拋出內(nèi)存溢出異常。

注意:GetStringUTFRegion 和 GetStringRegion 這兩個(gè)函數(shù)由于內(nèi)部沒有分配內(nèi)存,所以 JNI 沒有提供ReleaseStringUTFRegion 和 ReleaseStringRegion 這樣的函數(shù)。

字符串操作總結(jié)

總結(jié):

  • 對(duì)于小字符串來說,GetStringRegion 和 GetStringUTFRegion 這兩對(duì)函數(shù)是最佳選擇,因?yàn)榫彌_區(qū)可以被編譯器提前分配,而且永遠(yuǎn)不會(huì)產(chǎn)生內(nèi)存溢出的異常。當(dāng)你需要處理一個(gè)字符串的一部分時(shí),使用這對(duì)函數(shù)也是不錯(cuò)。因?yàn)樗鼈兲峁┝艘粋€(gè)開始索引和子字符串的長(zhǎng)度值。另外,復(fù)制少量字符串的消耗 也是非常小的。
  • 使用 GetStringCritical 和 ReleaseStringCritical 這對(duì)函數(shù)時(shí),必須非常小心。一定要確保在持有一個(gè)由 GetStringCritical 獲取到的指針時(shí),本地代碼不會(huì)在 JVM 內(nèi)部分配新對(duì)象,或者做任何其它可能導(dǎo)致系統(tǒng)死鎖的阻塞性調(diào)用。
  • 獲取 Unicode 字符串和長(zhǎng)度,使用 GetStringChars 和 GetStringLength 函數(shù)。
  • 獲取 UTF-8 字符串的長(zhǎng)度,使用 GetStringUTFLength 函數(shù)。
  • 創(chuàng)建 Unicode 字符串,使用 NewStringUTF 函數(shù)。
  • 從 Java 字符串轉(zhuǎn)換成 C/C++ 字符串,使用 GetStringUTFChars 函數(shù)。
  • 通過 GetStringUTFChars、GetStringChars、GetStringCritical 獲取字符串,這些函數(shù)內(nèi)部會(huì)分配內(nèi)存,必須調(diào)用相對(duì)應(yīng)的 ReleaseXXXX 函數(shù)釋放內(nèi)存。