從第三章中可以看出 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ù)說明:
因?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 之外,后面的都一樣。
通過調(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ì)象。
這對(duì)函數(shù)和 Get/ReleaseStringUTFChars 函數(shù)功能差不多,用于獲取和釋放以 Unicode 格式編碼的字符串。后者是用于獲取和釋放 UTF-8 編碼的字符串。
由于 UTF-8 編碼的字符串以'\0'結(jié)尾,而 Unicode 字符串不是。如果想獲取一個(gè)指向 Unicode 編碼的 jstring 字符串長(zhǎng)度,在 JNI 中可通過這個(gè)函數(shù)獲取。
獲取 UTF-8 編碼字符串的長(zhǎng)度,也可以通過標(biāo)準(zhǔn) C 函數(shù) strlen 獲取。
提高 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 編碼的。
分別表示獲取 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é):