從[第三章](http://blog.csdn.net/xyang81/article/details/42047899#demo)中可以看出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中的數據結構。[第三章](http://blog.csdn.net/xyang81/article/details/42047899#demo)的示例中,訪問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);
}
~~~
運行結果如下:

***示例解析:***
**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](https://code.csdn.net/xyang81/jnilearn)
- 前言
- JNI/NDK開發指南(開山篇)
- JNI/NDK開發指南(一)—— JNI開發流程及HelloWorld
- JNI/NDK開發指南(二)——JVM查找java native方法的規則
- JNI/NDK開發指南(三)——JNI數據類型及與Java數據類型的映射關系
- JNI/NDK開發指南(四)——字符串處理
- Android NDK開發Crash錯誤定位
- JNI/NDK開發指南(五)——訪問數組(基本類型數組與對象數組)
- JNI/NDK開發指南(六)——C/C++訪問Java實例方法和靜態方法
- JNI/NDK開發指南(七)——C/C++訪問Java實例變量和靜態變量
- JNI/NDK開發指南(八)——調用構造方法和父類實例方法
- JNI/NDK開發指南(九)——JNI調用性能測試及優化
- JNI/NDK開發指南(十)——JNI局部引用、全局引用和弱全局引用
- Android JNI局部引用表溢出:local reference table overflow (max=512)
- JNI/NDK開發指南(十一)——JNI異常處理