前言
JNI 的全稱是:Java Native Interface,即連接 Java 虛擬機(jī)和本地代碼的接口,它允許 Java 和本地代碼之間互相調(diào)用沛膳,在 Android 平臺(tái)揩徊,此處的本地代碼是指使用 C/C++ 或匯編語(yǔ)言編寫(xiě)的代碼,編譯后將以動(dòng)態(tài)鏈接庫(kù)(.so)的形式供 Java 虛擬機(jī)加載那婉,并按 JNI 規(guī)范互相調(diào)用。如果工作中需要大量運(yùn)用 JNI党瓮,強(qiáng)烈建議通讀 《JNI官方規(guī)范》详炬,并結(jié)合 Google 的《JNI Tips》 一節(jié)以了解在 Android 平臺(tái)的 JNI 實(shí)現(xiàn)有什么限制和不同。
如果只是想快速上手寞奸,同時(shí)規(guī)避一些常見(jiàn)問(wèn)題呛谜,可以先閱讀本文——本文的定位是操作手冊(cè)在跳,告知新手怎樣做及為什么,并提供一些最佳實(shí)踐建議呻率。
1 從 Java 調(diào)用 Native
1.1 通過(guò) javah 生成頭文件:
1.1.1 Java 層實(shí)現(xiàn)
public class HelloJNI {
static {
System.loadLibrary("hello"); // Load native library at runtime
}
// Declare a native method sayHello() that receives nothing and returns void
public native void sayHello();
}
1.1.2 Native 實(shí)現(xiàn)
HelloJNI.h
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
HelloJNI.c
#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
// Implementation of native method sayHello() of HelloJNI class
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
printf("Hello World!\n");
}
Tips: jni.h 里會(huì)使用 #if defined(__cplusplus)
來(lái)為 JNIEnv 提供不同的 typedef硬毕,盡量不要同時(shí)在 C 和 C++ 兩種語(yǔ)言包含的頭文件里都引用 JNIEnv,避免在兩種語(yǔ)言間傳遞 JNIEnv 導(dǎo)致類型不兼容礼仗。
1.2 注冊(cè) JNI 函數(shù)表
1.2.1 Java 層實(shí)現(xiàn)(略)
1.2.2 Native 實(shí)現(xiàn)
HellocJNI.c
#include <jni.h>
// Package name of Java class
static const char *const PACKAGE_NAME = "java/HelloJNI";
void JNICALL nativeSayHello(JNIEnv*, jobject) {
printf("Hello World!\n");
}
// Native method table
static JNINativeMethod methods[] = {
/* {"Method name", "Signature", FunctionPointer}, */
{ "sayHello", "()V", (void*)nativeSayHello },
};
jint registerNativeMethods(JNIEnv* env, const char *class_name, JNINativeMethod *methods, int num_methods) {
jclass clazz = env->FindClass(class_name);
if (NULL != clazz) {
return env->RegisterNatives(clazz, methods, num_methods);
}
return JNI_ERR;
}
// Invoked when System.loadLibrary()
jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
return JNI_ERR;
}
if (JNI_OK != registerNativeMethods(env, PACKAGE_NAME, methods, 1)) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
相比起第一種方式方法名以包名為前綴的做法吐咳,上面源碼中的 PACKAGE_NAME 可以很容易修改,更加靈活通用元践,推薦使用韭脊。
Tips: 注冊(cè) Native 方法的合適時(shí)機(jī)是上面代碼里的 jint JNI_OnLoad(JavaVM* vm, void* reserved)
函數(shù),它會(huì)在 Java 層 System.loadLibrary()
加載動(dòng)態(tài)鏈接庫(kù)之后被首先調(diào)用单旁,適用于執(zhí)行初始化邏輯沪羔。
2 Native 調(diào)用 Java
2.1 持有 JNIEnv 指針
從 Native 層調(diào)用 Java 方法,前提是 Native 持有 JNIEnv 指針象浑,通過(guò)類似以下代碼即可調(diào)用 Java 方法:
jstring getPackageName(JNIEnv* env, jobject contextObject) {
if (NULL != env && NULL != contextObject) {
jclass contextClazz = env->FindClass("android/content/Context");
jmethodID methodId = env->GetMethodID(contextClazz, "getPackageName", "()Ljava/lang/String;");
return (jstring) env->CallObjectMethod(contextObject, methodId);
}
return NULL;
}
Tips: GetMethodID/GetFieldID/CallXXXMethod 等方法均不接受 NULL 參數(shù)蔫饰,否則程序會(huì)異常退出,在獲取非文檔化的類或成員后一定要先對(duì)返回值進(jìn)行判空再使用愉豺。
2.2 沒(méi)有 JNIEnv 指針
JNIEnv 實(shí)例保存在線程本地存儲(chǔ) TLS(Thread-Local Storage)中篓吁,因此不能在線程間直接共享 JNIEnv 指針。如果線程的 TLS 里存有 JNIEnv 實(shí)例蚪拦,只是沒(méi)有引用該實(shí)例的指針杖剪,可以通過(guò) JavaVM 指針調(diào)用 GetEnv() 來(lái)獲取指向線程自有 JNIEnv 的指針。因?yàn)?Android 下的 JavaVM 實(shí)例是全進(jìn)程唯一的驰贷,所以可以被所有線程共享盛嘿。
還有一種更特殊的情況:即線程根本沒(méi)有 JNIEnv 實(shí)例(如代碼中通過(guò) pthread_create() 創(chuàng)建的原生線程),這種情況下需要先調(diào)用 JavaVM->AttachCurrentThread()
將線程依附于 JavaVM 以獲得 JNIEnv 實(shí)例(Attach 到 VM 后就被視為 Java 線程)括袒。當(dāng)線程退出時(shí)要配對(duì)調(diào)用 JavaVM->DetachCurrentThread()
以釋放 JVM 里的資源次兆。
Tips: 為避免 DetachCurrentThread 未配對(duì)調(diào)用,可以通過(guò) int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
創(chuàng)建一個(gè) TLS 數(shù)據(jù)的 key箱熬,并注冊(cè)一個(gè) destructor 回調(diào)函數(shù)类垦,它會(huì)在線程退出前被調(diào)用,因此很適合用于執(zhí)行類似 DetachCurrentThread 的清理工作城须。另外還可以使用 key 調(diào)用 pthread_setspecific 函數(shù),將 JNIEnv 指針保存到 TLS 中米苹,這樣一來(lái)不僅可隨用隨取糕伐,而且當(dāng) destructor 函數(shù)被調(diào)用時(shí) JNIEnv 指針會(huì)作為參數(shù)傳入,方便調(diào)用 Java 層的一些清理方法蘸嘶。部分示例如下:
JavaVM* gVM; // Global VM reference
pthread_key_t gKey; // Global TLS data key
void onThreadExit(void* tlsData) {
JNIEnv* env = (JNIEnv*)tlsData;
// Do some JNI calls with env if needed ...
gVM->DetachCurrentThread();
}
// Invoked when System.loadLibrary()
jint JNI_OnLoad(JavaVM *vm, void *) {
// ignore some initialize code ...
gVM = vm;
// Create thread-specific data key and register thread-exit callback
pthread_key_create(&gKey, onThreadExit);
return JNI_VERSION_1_6;
}
JNIEnv* getJNIEnv(JavaVM* vm) {
JNIEnv *env = (JNIEnv *) pthread_getspecific(gKey); // gKey created by pthread_key_create() before
if (NULL == env) {
if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6)) {
if (JNI_OK == vm->AttachCurrentThread(&env, NULL)) {
pthread_setspecific(gKey, env); // Save JNIEnv* to TLS with gKey
}
}
}
return env;
}
3 對(duì)象引用
3.1 本地引用
每個(gè)傳給 Native 方法的參數(shù)(對(duì)象)良瞧,和幾乎所有 JNI 函數(shù)返回的對(duì)象都是本地引用(Local reference)陪汽。這意味著它們只在當(dāng)前線程的當(dāng)前 native 方法內(nèi)有效,一旦該方法返回則失效(哪怕被引用的對(duì)象仍然存在)褥蚯。所以正常情況下開(kāi)發(fā)者無(wú)須手動(dòng)調(diào)用 DeleteLocalRef 釋放挚冤,除非以下幾種情況:
- Native 方法內(nèi)創(chuàng)建大量的本地引用,例如在循環(huán)中反復(fù)創(chuàng)建赞庶,因?yàn)樘摂M機(jī)保存本地引用的空間是有限的(Android 為512個(gè))训挡,一旦循環(huán)中創(chuàng)建的引用數(shù)超出限制就會(huì)導(dǎo)致異常:ReferenceTable overflow (max=512);
- 通過(guò) AttachCurrentThread() 依附到 JVM 的線程內(nèi)的所有本地引用均不會(huì)被自動(dòng)釋放歧强,直到調(diào)用 DetachCurrentThread() 才會(huì)統(tǒng)一釋放澜薄,為避免線程中創(chuàng)建太多本地引用建議及時(shí)做手動(dòng)釋放;
- Native 方法本地引用了一個(gè)非常大的對(duì)象摊册,用完后還要進(jìn)行較長(zhǎng)時(shí)間的其它運(yùn)算才能返回肤京,本地引用會(huì)阻止該對(duì)象被 GC。為降低 OutOfMemory(OOM) 風(fēng)險(xiǎn)用完后應(yīng)該及時(shí)手動(dòng)釋放茅特。
上面所說(shuō)的對(duì)象是指 jobject 及其子類忘分,包括 jclass, jstring, jarray,不包括 GetStringUTFChars 和 GetByteArrayElements 這類函數(shù)的返回值(皆返回原始數(shù)據(jù)指針)白修,也不包括 jmethodID 和 jfieldID妒峦,這兩者在 Android 下只要類加載之后就一直有效。
Tips: GetStringUTFChars
/ Get<PrimitiveType>ArrayElements
等函數(shù)返回的原始數(shù)據(jù)指針可以跨線程使用熬荆,并且必須手動(dòng)調(diào)用對(duì)應(yīng)的 ReleaseStringUTFChars
/ Release<PrimitiveType>ArrayElements
函數(shù)釋放舟山,否則會(huì)造成內(nèi)存泄漏。
3.2 全局引用
與本地引用不同卤恳,全局引用可以跨方法跨線程使用累盗,通過(guò) NewGlobalRef 或 NewWeakGlobalRef 方法創(chuàng)建之后,會(huì)一直有效直到調(diào)用 DeleteGlobalRef/DeleteWeakGlobalRef 銷毀突琳。這個(gè)特性常用于緩存一些獲取起來(lái)較耗時(shí)的對(duì)象若债,比如 jclass 通過(guò) FindClass 獲取時(shí)有反射的開(kāi)銷,對(duì)于同一個(gè)類而言獲取一次緩存起來(lái)備用會(huì)更高效:
jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Tips: 如果想在一個(gè)在加載時(shí)將 Native jclass拆融、jmethodID蠢琳、jfieldID 緩存起來(lái)備用,可以像下面代碼一樣在 Java 層的靜態(tài)域內(nèi)調(diào)用 nativeInit 方法镜豹,該方法的 Native 層實(shí)現(xiàn)可以通過(guò) FindClass傲须、GetFieldID、GetMethodID 等方法把所有后續(xù)要使用的類對(duì)象和成員都緩存起來(lái)趟脂,避免每次使用前都查找?guī)?lái)的性能開(kāi)銷泰讽。
/*
* We use a class initializer to allow the native code to cache some
* field offsets. This native function looks up and caches interesting
* class/field/method IDs.
*/
private static native void nativeInit();
static {
nativeInit();
}
3.3 引用比較
比較兩個(gè)引用是否指向同個(gè)對(duì)象需要使用 jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
方法。要注意的是 JNI 中的 NULL 指向 JVM 中的 null 對(duì)象,IsSameObject 用于弱全局引用(WeakGlobalRef)與 NULL 比較時(shí)已卸,返回值的意義表示其引用的對(duì)象是否已經(jīng)回收(JNI_TRUE 代表已回收佛玄,該弱引用已無(wú)效)。
4 線程安全
由于 Android 下的 JVM 線程底層基于 POSIX Threads累澡,因此有兩種使用對(duì)象同步(synchronized)的方式:基于 Java 的同步和基于 POSIX 的同步:
4.1 基于 Java 的同步
A. JNI 提供了類似 synchronized 語(yǔ)句的同步塊函數(shù):
B. 也可以直接在 Java 層用 synchronized 關(guān)鍵詞修飾 native 方法:
public native synchronized void sayHello();
這種用法可以確保 Java 對(duì) sayHello() 的調(diào)用是同步的梦抢,但通常不建議這么用,因?yàn)榭赡軒?lái)以下問(wèn)題:
- 對(duì)整個(gè) Native 方法做同步的粒度較大愧哟,可能影響性能奥吩;
- Native 和 Java 的方法聲明在不同的位置,可能出現(xiàn)方法聲明更改(如 synchronized 關(guān)鍵詞被刪除)翅雏,會(huì)導(dǎo)致方法不再線程安全圈驼;
- 如果該 sayHello() 在 Java 之外被其它 Native 函數(shù)調(diào)用,則不是線程安全的望几。
Tips: 在上述同步方案中 Object.wait()/notify()/notifyAll() 等方法同樣可以使用绩脆,只需從 Native 層調(diào)用對(duì)象對(duì)應(yīng)的 Java 方法即可。
4.2 基于 POSIX 的同步
無(wú)論是通過(guò) pthread 或者 Java 創(chuàng)建的線程橄抹,均可使用 pthread 提供的線程控制函數(shù)來(lái)實(shí)現(xiàn) Native 層的同步靴迫,如: pthread_mutex_lock/pthread_mutex_unlock/pthread_cond_wait/pthread_cond_signal
等等。
5 字符編碼
Java 內(nèi)部是使用 UTF-16 處理字符楼誓,但 JNI 對(duì)外提供了一套函數(shù)用于將 UTF-16 轉(zhuǎn)換為 UTF-8 的一個(gè)變種 Modified UTF-8(以 0xc0 0x80 而不是 0x00 來(lái)編碼 \u0000)玉锌,使用這個(gè)變種的好處是能兼容以 0x00 作為結(jié)束符的 C 字符處理函數(shù),缺點(diǎn)是與標(biāo)準(zhǔn)或其它 UTF-8 變種之間有細(xì)微的差異疟羹,存在潛在的兼容性問(wèn)題主守。所以在從網(wǎng)絡(luò)或文件讀入文本后,必須確認(rèn)或處理為符合 Modified UTF-8 編碼才能傳給 NewStringUTF 方法榄融,否則可能無(wú)法得到預(yù)期的結(jié)果参淫。
6 數(shù)組訪問(wèn)
6.1 隨機(jī)訪問(wèn)數(shù)組
對(duì)于對(duì)象數(shù)組(Array of objects) JNI 提供了 GetObjectArrayElement/SetObjectArrayElement
函數(shù)允許每次訪問(wèn)數(shù)組中的一個(gè)對(duì)象。而對(duì)于原始類型的 Java 數(shù)組則提供了映射為 C 數(shù)組的 NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy)
函數(shù)族 愧杯,讓我們可以像訪問(wèn) C 數(shù)組那樣讀寫(xiě) Java 數(shù)組的內(nèi)容涎才,該函數(shù)族的完整列表如下:
PrimitiveType | ArrayType | NativeType |
---|---|---|
GetBooleanArrayElements() | jbooleanArray | jboolean |
GetByteArrayElements() | jbyteArray | jbyte |
GetCharArrayElements() | jcharArray | jchar |
GetShortArrayElements() | jshortArray | jshort |
GetIntArrayElements() | jintArray | jint |
GetLongArrayElements() | jlongArray | jlong |
GetFloatArrayElements() | jfloatArray | jfloat |
GetDoubleArrayElements() | jdoubleArray | jdouble |
如果調(diào)用成功 Get<PrimitiveType>ArrayElements
函數(shù)族會(huì)返回指向 Java 數(shù)組的堆地址或新申請(qǐng)的副本的指針(視 JVM 的具體實(shí)現(xiàn),在 ART 里數(shù)組的堆空間若可被移動(dòng)則返回副本力九,可以傳遞非 NULL 的 isCopy 指針來(lái)確認(rèn)返回值是否副本)耍铜,如果指針指向是 Java 數(shù)組的堆地址而非副本,在 Release<PrimitiveType>ArrayElements
之前此 Java 數(shù)組都無(wú)法被 GC 回收跌前,所以 Get<PrimitiveType>ArrayElements
和 Release<PrimitiveType>ArrayElements
必須配對(duì)調(diào)用以避免內(nèi)存泄漏棕兼。另外 Get<PrimitiveType>ArrayElements
可能因內(nèi)存不足創(chuàng)建副本失敗而返回 NULL,應(yīng)先對(duì)返回值判空后再使用抵乓。
Release<PrimitiveType>ArrayElements
原型如下:
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);
它最后一個(gè)參數(shù) mode 僅對(duì) elems 為副本時(shí)有效程储,它可以用于避免一些非必要的副本拷貝蹭沛,共有以下三種取值:
- 0:將 elems 內(nèi)容回寫(xiě)到 Java 數(shù)組并釋放 elems 占用的空間臂寝;
- JNI_COMMIT:將 elems 內(nèi)容回寫(xiě)到 Java 數(shù)組章鲤,但不釋放 elems 的空間;
- JNI_ABORT:不回寫(xiě) elems 內(nèi)容到 Java 數(shù)組咆贬,釋放 elems 的空間败徊。
一般來(lái)說(shuō) mode 參數(shù)直接傳 0 是最安全的選擇,這樣不論 Get<PrimitiveType>ArrayElements
返回的是否副本都不會(huì)發(fā)生泄漏掏缎。但也有一些情況為了性能等因素考慮會(huì)使用非零值皱蹦,比方說(shuō)對(duì)于一個(gè)尺寸很大的數(shù)組,如果獲取指針之后通過(guò) isCopy 確認(rèn)是副本眷蜈,且之后沒(méi)有修改過(guò)內(nèi)容沪哺,那么完全可以使用 JNI_ABORT 避免回寫(xiě)以提高性能。
另一種可能的情況是 Native 修改數(shù)組和 Java 讀取數(shù)組在交替進(jìn)行(如多線程環(huán)境)酌儒,如果通過(guò) isCopy 確認(rèn)獲取的數(shù)組是副本辜妓,可以通過(guò) JNI_COMMIT 調(diào)用 Release<PrimitiveType>ArrayElements
來(lái)提交修改早像,由于 JNI_COMMIT 不會(huì)釋放副本艺配,所以最終還需要使用別的 mode 值再調(diào)用 Release 以避免副本泄漏。
Tips: 一種常見(jiàn)的錯(cuò)誤用法是當(dāng) isCopy 為 false 時(shí)跳過(guò)使用 Release贰镣,此時(shí)雖未創(chuàng)建副本榴啸,但 Java 數(shù)組的堆內(nèi)存被引用后會(huì)阻止 GC 回收孽惰,因此也必須配對(duì)調(diào)用 Release 函數(shù)。
6.2 塊拷貝
上一節(jié)講解了如何訪問(wèn) Java 數(shù)組鸥印,考慮一下這種場(chǎng)景:Native 層需要從/往 Java 數(shù)組拷貝一塊內(nèi)容勋功,根據(jù)上面的內(nèi)容很容易寫(xiě)出以下代碼:
jbyte* data = env->GetByteArrayElements(javaArray, NULL);
if (data != NULL) {
memcpy(buffer, data, len);
env->ReleaseByteArrayElements(javaArray, data, JNI_ABORT);
}
先獲取指向 Java 數(shù)組堆內(nèi)存(或者副本)的指針,將頭 len 個(gè)字節(jié)拷貝到 buffer 后調(diào)用 Release 釋放库说。由于沒(méi)有改變數(shù)組內(nèi)容狂鞋,因此使用 JNI_ABORT 避免回寫(xiě)開(kāi)銷。
但其實(shí)有更簡(jiǎn)單的做法璃弄,就是調(diào)用塊拷貝函數(shù):
env->GetByteArrayRegion(javaArray, 0, len, buffer);
相比前一種方式要销,塊拷貝有以下優(yōu)點(diǎn):
- 只需要一次 JNI 調(diào)用,減少開(kāi)銷夏块;
- 無(wú)需創(chuàng)建副本或引用 Java 數(shù)組的內(nèi)存(不影響 GC)
- 降低編程出錯(cuò)的風(fēng)險(xiǎn)——不會(huì)因漏調(diào)用 Release 函數(shù)而引起泄漏疏咐。
對(duì)于字符串也有類似的拷貝函數(shù),下面是原型:
// Region copy for Array.
// Throw ArrayIndexOutOfBoundsException if one of the indexes in the region is not valid
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);
// Region copy for String.
// Throws StringIndexOutOfBoundsException on index overflow
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
前兩個(gè)函數(shù)族的 PrimitiveType脐供、ArrayType浑塞、NativeType 之定義請(qǐng)參考上一節(jié)的表格。
6.3 性能敏感場(chǎng)景
上面兩種數(shù)組訪問(wèn)方式都會(huì)涉及到拷貝(Get<PrimitiveType>ArrayElements
雖不一定創(chuàng)建副本政己,但開(kāi)發(fā)者無(wú)法控制)酌壕,在性能敏感的場(chǎng)景下拷貝帶來(lái)的耗時(shí)往往不可接受,因此需要一種無(wú)拷貝的方式來(lái)訪問(wèn)數(shù)組。在 Android 下可以使用以下兩種方式:
6.3.1 臨界訪問(wèn)
void* GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);
如上所示卵牍,JNI 提供了數(shù)組臨界訪問(wèn)函數(shù)果港,雖然從參數(shù)上仍保留了 iSCopy 和 mode,但使用這對(duì)函數(shù)時(shí)有非常嚴(yán)格的限制:Get 和 Release 之間被視為臨界區(qū)糊昙,臨界區(qū)里的代碼應(yīng)該盡快執(zhí)行完辛掠,而且不允許調(diào)用其它 JNI 函數(shù),以及任何可能導(dǎo)致當(dāng)前線程阻塞并等待另一個(gè) Java 線程的系統(tǒng)調(diào)用(比如當(dāng)前線程不能調(diào)用 read 函數(shù)讀取另一個(gè) Java 線程正在寫(xiě)的流)释牺。
這些嚴(yán)格的限制實(shí)際是為了便于 VM 直接返回?cái)?shù)組堆內(nèi)存的指針萝衩,比如采用 Moving GC 的 VM 可以在臨界區(qū)內(nèi)暫停 GC 來(lái)保證 Get 返回的數(shù)組地址不會(huì)改變。
6.3.2 Direct ByteBuffer
上一種方式雖然可以應(yīng)付性能敏感的場(chǎng)景但限制頗多没咙。JNI 還提供了 Direct ByteBuffer 方案猩谊,可以通過(guò) java.nio.ByteBuffer.allocateDirect
方法或 JNI 函數(shù) NewDirectByteBuffer
創(chuàng)建,它和普通的 ByteBuffer 差異在于其內(nèi)部使用的內(nèi)存不是在 Java 堆上分配的祭刚,而可以通過(guò) GetDirectBufferAddress
函數(shù)獲取地址后直接在 Native 代碼訪問(wèn)牌捷,從 Java 層訪問(wèn)可能會(huì)比較慢。
以上兩種方式的選擇取決于以下因素:
- 數(shù)據(jù)主要是在 Java 還是 C/C++ 代碼訪問(wèn)袁梗?
如果主要是在 C/C++ 里訪問(wèn)首選 DirectByteBuffer宜鸯,速度快限制少。 - 如果數(shù)據(jù)最終要被傳回 Java API遮怜,是作為什么類型的參數(shù)傳遞的淋袖?
如果 Java API 需要一個(gè) byte[] 參數(shù),那么就不要使用 DirectByteBuffer(調(diào)用其byte[] array ()
方法會(huì)拋 UnsupportedOperationException 異常)锯梁。 - 如果上兩種方式都可以使用且沒(méi)有明顯的優(yōu)劣即碗,建議優(yōu)先選用 DirectByteBuffer,沒(méi)有臨界區(qū)的限制代碼擴(kuò)展性更好陌凳,且隨著 JVM 實(shí)現(xiàn)的優(yōu)化剥懒,從 Java 層訪問(wèn)的性能也會(huì)得到提升。
7 異常處理
部分 JNI 調(diào)用可能會(huì)拋出異常合敦,當(dāng)異常發(fā)生后 Native 代碼仍可繼續(xù)執(zhí)行初橘,但此時(shí)絕大多數(shù) JNI 函數(shù)都不能被調(diào)用(調(diào)用即Crash),直到異常被 Native 或 Java 層處理充岛。一般在 Native 調(diào)用可能產(chǎn)生異常的 Java 方法都應(yīng)該進(jìn)行異常檢查和處理保檐,避免程序非正常退出。一個(gè)常見(jiàn)的異常處理邏輯如下:
// ...
env->CallVoidMethod(clazz, methodName, params); // Call a Java method which may throws exception
if (env->ExceptionCheck()) { // If exception occurred, ExceptionCheck() return JNI_TRUE
if (Native can handle exception) {
// handle it
// ...
// Clear the exception, so program can continue
env->ExceptionClear();
} else {
// Native can't handle exception, return and let Java code do that
return ;
}
}
// If not clear exception in line 8, then program will crash when it calls next JNI function:
env->NewStringUTF("WhatEver");
Tips: 只有以下 JNI 函數(shù)可以在異常未處理時(shí)調(diào)用而不會(huì)導(dǎo)致 Crash:
- DeleteGlobalRef
- DeleteLocalRef
- DeleteWeakGlobalRef
- ExceptionCheck
- ExceptionClear
- ExceptionDescribe
- ExceptionOccurred
- MonitorExit
- PopLocalFrame
- PushLocalFrame
- Release<PrimitiveType>ArrayElements
- ReleasePrimitiveArrayCritical
- ReleaseStringChars
- ReleaseStringCritical
- ReleaseStringUTFChars
8 擴(kuò)展檢查
JNI 幾乎沒(méi)有錯(cuò)誤檢查崔梗,出錯(cuò)通常都會(huì)導(dǎo)致崩潰夜只。Android 額外提供了一種名為 CheckJNI 的模式,該模式下會(huì)將 JavaVM 和 JNIEnv 的函數(shù)表指針重定向到帶檢查能力的函數(shù)表蒜魄,該表里函數(shù)會(huì)先執(zhí)行擴(kuò)展檢查再調(diào)用實(shí)際的 JNI 函數(shù)扔亥。
擴(kuò)展檢查項(xiàng)包括:
- 數(shù)組:嘗試分配一個(gè)負(fù)數(shù)長(zhǎng)度的數(shù)組场躯;
- 錯(cuò)誤的指針:將錯(cuò)誤的jarray / jclass / jobject / jstring傳遞給JNI調(diào)用,或者將NULL指針傳遞給具有不可空參數(shù)的JNI調(diào)用旅挤;
- 類名稱:將錯(cuò)誤樣式的類名傳遞給JNI調(diào)用踢关;
- 臨界調(diào)用:在臨界區(qū)中進(jìn)行 JNI 調(diào)用;
- Direct ByteBuffers:將錯(cuò)誤的參數(shù)傳遞給NewDirectByteBuffer谦铃;
- 異常:在有待處理異常時(shí)進(jìn)行 JNI 調(diào)用耘成;
- JNIEnv指針:跨線程使用 JNIEnv;
- jfieldIDs:使用 NULL jfieldID 或使用 jfieldID 設(shè)置值時(shí)類型不正確驹闰,或使用 jfieldID 設(shè)置未持有該 jfieldID 的類成員等;
- jmethodIDs:同 jfieldIDs撒会;
- 引用:在錯(cuò)誤的引用類型上調(diào)用 DeleteGlobalRef/DeleteLocalRef嘹朗;
- Release modes:調(diào)用 Release 時(shí)傳入錯(cuò)誤的 mode 參數(shù)(例如傳入除 0,JNI_ABORT诵肛,JNI_COMMIT 之外的值)屹培;
- 類型安全:Native 方法返回一個(gè)與聲明不兼容的類型;
- UTF-8:將一個(gè)非法的 Modified UTF-8 字符串傳給 JNI 調(diào)用怔檩。
以下方式可以打開(kāi)擴(kuò)展檢查能力:
- 如果使用模擬器褪秀,則默認(rèn)開(kāi)啟了全局 CheckJNI;
- 如果編譯的是Debug版本的App薛训,也默認(rèn)開(kāi)啟了媒吗;
- root過(guò)的手機(jī)可以用以下命令開(kāi)啟:
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
- 未 root 的可以用:
adb shell setprop debug.checkjni 1
通過(guò)以下 Logcat 內(nèi)容可以確認(rèn)是否開(kāi)啟成功:
D AndroidRuntime: CheckJNI is ON