天天看點

JNI/NDK開發指南(四)——字元串處理

轉載請注明出處:http://blog.csdn.net/xyang81/article/details/42066665

從第三章中可以看出JNI中的基本類型和Java中的基本類型都是一一對應的,接下來先看一下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;
           

基本類型很容易了解,就是對C/C++中的基本類型用typedef重新定義了一個新的名字,在JNI中可以直接通路。

       JNI把Java中的所有對象當作一個C指針傳遞到本地方法中,這個指針指向JVM中的内部資料結構,而内部的資料結構在記憶體中的存儲方式是不可見的。隻能從JNIEnv指針指向的函數表中選擇合适的JNI函數來操作JVM中的資料結構。第三章的示例中,通路java.lang.String對應的JNI類型jstring時,沒有像通路基本資料類型一樣直接使用,因為它在Java是一個引用類型,是以在本地代碼中隻能通過GetStringUTFChars這樣的JNI函數來通路字元串的内容。

       下面先看一個例子:

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

運作結果如下:

JNI/NDK開發指南(四)——字元串處理

示例解析:

1> 通路字元串

       sayHello函數接收一個jstring類型的參數text,但jstring類型是指向JVM内部的一個字元串,和C風格的字元串類型char*不同,是以在JNI中不能通把jstring當作普通C字元串一樣來使用,必須使用合适的JNI函數來通路JVM内部的字元串資料結構。

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

     env:JNIEnv函數表指針

    j_str:jstring類型(Java傳遞給本地代碼的字元串指針)

isCopy:取值JNI_TRUE和JNI_FALSE,如果值為JNI_TRUE,表示傳回JVM内部源字元串的一份拷貝,并為新産生的字元串配置設定記憶體空間。如果值為JNI_FALSE,表示傳回JVM内部源字元串的指針,意味着可以通過指針修改源字元串的内容,不推薦這麼做,因為這樣做就打破了Java字元串不能修改的規定。但我們在開發當中,并不關心這個值是多少,通常情況下這個參數填NULL即可。

       因為Java預設使用Unicode編碼,而C/C++預設使用UTF編碼,是以在本地代碼中操作字元串的時候,必須使用合适的JNI函數把jstring轉換成C風格的字元串。JNI支援字元串在Unicode和UTF-8兩種編碼之間轉換,GetStringUTFChars可以把一個jstring指針(指向JVM内部的Unicode字元序列)轉換成一個UTF-8格式的C字元串。在上例中sayHello函數中我們通過GetStringUTFChars正确取得了JVM内部的字元串内容。

2> 異常檢查

       調用完GetStringUTFChars之後不要忘記安全檢查,因為JVM需要為新誕生的字元串配置設定記憶體空間,當記憶體空間不夠配置設定的時候,會導緻調用失敗,失敗後GetStringUTFChars會傳回NULL,并抛出一個OutOfMemoryError異常。JNI的異常和Java中的異常處理流程是不一樣的,Java遇到異常如果沒有捕獲,程式會立即停止運作。而JNI遇到未決的異常不會改變程式的運作流程,也就是程式會繼續往下走,這樣後面針對這個字元串的所有操作都是非常危險的,是以,我們需要用return語句跳過後面的代碼,并立即結束目前方法。

3> 釋放字元串

       在調用GetStringUTFChars函數從JVM内部擷取一個字元串之後,JVM内部會配置設定一塊新的記憶體,用于存儲源字元串的拷貝,以便本地代碼通路和修改。即然有記憶體配置設定,用完之後馬上釋放是一個程式設計的好習慣。通過調用ReleaseStringUTFChars函數通知JVM這塊記憶體已經不使用了,你可以清除了。注意:這兩個函數是配對使用的,用了GetXXX就必須調用ReleaseXXX,而且這兩個函數的命名也有規律,除了前面的Get和Release之外,後面的都一樣。

4> 建立字元串

       通過調用NewStringUTF函數,會建構一個新的java.lang.String字元串對象。這個新建立的字元串會自動轉換成Java支援的Unicode編碼。如果JVM不能為構造java.lang.String配置設定足夠的記憶體,NewStringUTF會抛出一個OutOfMemoryError異常,并傳回NULL。在這個例子中我們不必檢查它的傳回值,如果NewStringUTF建立java.lang.String失敗,OutOfMemoryError這個異常會被在Sample.main方法中抛出。如果NewStringUTF建立java.lang.String成功,則傳回一個JNI引用,這個引用指向新建立的java.lang.String對象。

其它字元串處理函數:

1> GetStringChars和ReleaseStringChars:這對函數和Get/ReleaseStringUTFChars函數功能差不多,用于擷取和釋放以Unicode格式編碼的字元串。後者是用于擷取和釋放UTF-8編碼的字元串。

2> GetStringLength:由于UTF-8編碼的字元串以'\0'結尾,而Unicode字元串不是。如果想擷取一個指向Unicode編碼的jstring字元串長度,在JNI中可通過這個函數擷取。

3> GetStringUTFLength:擷取UTF-8編碼字元串的長度,也可以通過标準C函數strlen擷取

4> GetStringCritical和ReleaseStringCritical:提高JVM傳回源字元串直接指針的可能性

Get/ReleaseStringChars和Get/ReleaseStringUTFChars這對函數傳回的源字元串會後配置設定記憶體,如果有一個字元串内容相當大,有1M左右,而且隻需要讀取裡面的内容列印出來,用這兩對函數就有些不太合适了。此時用Get/ReleaseStringCritical可直接傳回源字元串的指針應該是一個比較合适的方式。不過這對函數有一個很大的限制,在這兩個函數之間的本地代碼不能調用任何會讓線程阻塞或等待JVM中其它線程的本地函數或JNI函數。因為通過GetStringCritical得到的是一個指向JVM内部字元串的直接指針,擷取這個直接指針後會導緻暫停GC線程,當GC被暫停後,如果其它線程觸發GC繼續運作的話,都會導緻阻塞調用者。是以在Get/ReleaseStringCritical這對函數中間的任何本地代碼都不可以執行導緻阻塞的調用或為新對象在JVM中配置設定記憶體,否則,JVM有可能死鎖。另外一定要記住檢查是否因為記憶體溢出而導緻它的傳回值為NULL,因為JVM在執行GetStringCritical這個函數時,仍有發生資料複制的可能性,尤其是當JVM内部存儲的數組不連續時,為了傳回一個指向連續記憶體空間的指針,JVM必須複制所有資料。下面代碼示範這對函數的正确用法:

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之間是一個關鍵區。
	 * 在這關鍵區之中,絕對不能呼叫JNI的其他函數和會造成目前線程中斷或是會讓目前線程等待的任何本地代碼,
	 * 否則将造成關鍵區代碼執行區間垃圾回收器停止運作,任何觸發垃圾回收器的線程也會暫停。
	 * 其他觸發垃圾回收器的線程不能前進直到目前線程結束而激活垃圾回收器。
	 */
	c_str = (*env)->GetStringCritical(env,j_str,NULL);	// 傳回源字元串指針的可能性
	if (c_str == NULL)	// 驗證是否因為字元串拷貝記憶體溢出而傳回NULL
	{
		return NULL;
	}
	while(*c_str) 
	{
		*pBuff++ = *c_str++;
	}
	(*env)->ReleaseStringCritical(env,j_str,c_str);
	return (*env)->NewStringUTF(env,buff);
}
           

JNI中沒有Get/ReleaseStringUTFCritical這樣的函數,因為在進行編碼轉換時很可能會促使JVM對資料進行複制,因為JVM内部表示的字元串是使用Unicode編碼的。

5> GetStringRegion和GetStringUTFRegion:分别表示擷取Unicode和UTF-8編碼字元串指定範圍内的内容。這對函數會把源字元串複制到一個預先配置設定的緩沖區内。下面代碼用GetStringUTFRegion重新實作sayHello函數:

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello
  (JNIEnv *env, jclass cls, jstring j_str)
{
	jsize len = (*env)->GetStringLength(env,j_str);	// 擷取unicode字元串的長度
	printf("str_len:%d\n",len);
	char buff[128] = "hello ";
	char* pBuff = buff + 6;
	// 将JVM中的字元串以utf-8編碼拷入C緩沖區,該函數内部不會配置設定記憶體空間
	(*env)->GetStringUTFRegion(env,j_str,0,len,pBuff);
	return (*env)->NewStringUTF(env,buff);
}
           

GetStringUTFRegion這個函數會做越界檢查,如果檢查發現越界了,會抛出StringIndexOutOfBoundsException異常,這個方法與GetStringUTFChars比較相似,不同的是,GetStringUTFRegion内部不配置設定記憶體,不會抛出記憶體溢出異常。

注意:GetStringUTFRegion和GetStringRegion這兩個函數由于内部沒有配置設定記憶體,是以JNI沒有提供ReleaseStringUTFRegion和ReleaseStringRegion這樣的函數。

字元串操作總結:

1、對于小字元串來說,GetStringRegion和GetStringUTFRegion這兩對函數是最佳選擇,因為緩沖區可以被編譯器提前配置設定,而且永遠不會産生記憶體溢出的異常。當你需要處理一個字元串的一部分時,使用這對函數也是不錯。因為它們提供了一個開始索引和子字元串的長度值。另外,複制少量字元串的消耗 也是非常小的。

2、使用GetStringCritical和ReleaseStringCritical這對函數時,必須非常小心。一定要確定在持有一個由 GetStringCritical 擷取到的指針時,本地代碼不會在 JVM 内部配置設定新對象,或者做任何其它可能導緻系統死鎖的阻塞性調用

3、擷取Unicode字元串和長度,使用GetStringChars和GetStringLength函數

4、擷取UTF-8字元串的長度,使用GetStringUTFLength函數

5、建立Unicode字元串,使用NewStringUTF函數

6、從Java字元串轉換成C/C++字元串,使用GetStringUTFChars函數

7、通過GetStringUTFChars、GetStringChars、GetStringCritical擷取字元串,這些函數内部會配置設定記憶體,必須調用相對應的ReleaseXXXX函數釋放記憶體

示例代碼下載下傳位址:https://code.csdn.net/xyang81/jnilearn