好久没搞NDK的东西了,第一次学习NDK还是14年的时候,最近又需要使用,所以复习一下。本篇笔记主要记录使用Java原生接口技术实现Java应用程序和原生代码之间通信
JNI是Java程序设计语言功能最强的特征,它允许Java类的某些方法原生实现,同时让它们能够像普通Java方法一样被调用和使用。这些原生方法也可以使用Java对象,使用方法与Java代码使用Java对象的方法相同。原生方法可以创建新的Java对象或者使用Java应用程序创建的对象,这些Java应用程序可以检查、修改和调用这些对象的方法以执行任务。
1.Java代码如何调用原生方法
2.声明原生方法
3.在共享库中载入原生模块
4.在C/C++中实现原生方法
loadLibrary的参数也不包含共享库的位置。Java库路径,也就是系统属性java.library.path保存loadLibirary方法在共享库搜索的目录列表,Android上的Java库路径包含/vendor/lib和/system/lib。
需要强调的是,loadLibrary在扫描Java库路径时,一旦发现同名的库,立即加载共享库。因为Java库路径的第一组目录是Android系统目录,
为了避免与系统库命名冲突,强烈建议Android开发人员为每个共享库选择唯一的名字。
C源代码文件以jni.h头文件包含语句开关,这个头文件中包含JNI数据类型和函数的定义。
方法声明
JNIEXPORT jstring
JNICALL
Java_com_generalandroid_algorithmwithndk_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
第一个参数JNIEnv是指向可用JNI函数表的接口指针;第二个参数jobject是Java对象引用。
JNIEnv接口指针
原生代码通过JNIEnv接口指针提供的各种函数来使用虚拟机的功能。JNIEnv是一个指向线程-局部数据的指针,而线程-局部数据中包含指向
函数表的指针。实现原生方法的函数将JNIEnv接口指针作为它们的第一个参数。(注意:传递给每一个原生方法调用的JNIEnv接口指针在与方法调用相关的线程中也有效,但是它不能被缓存以及被其他线程使用。)
原生代码是C与原生代码是C++其调用JNI函数的语法不同。C代码中,JNIEnv是指向JNINativeInterface结构的指针,为了访问任何一个JNI函数,该指针需要首先被解引用。因为C代码中的JNI函数不了解当前的JNI环境,JNIEnv实例应该作为第一个参数传递给每一个JNI函数调用调用者,调用格式如下:
return (*env)->NewStringUTF(env,"Hello from JNI!");
在C++代码中,JNIEnv实际上是C++类实例,JNI函数以成员函数的形式存在。因为JNI方法已经访问了当前的JNI环境,因此JNI方法调用不要求JNIEnv实例作参数。
return env->NewStringUTF("Hello from JNI");
(2)实例方法与静态方法
Java程序设计语言有两类方法:实例方法和静态方法。实例方法与类实例相关,它们只能在类实例中调用。静态方法不与类实例相关,它们可以在静态上下文直接调用。静态方法和实例方法均可以声明为原生的,可以通过JNI技术以原生代码的形式提供它们的实现。原生实例方法通过第二个参数获取实例引用,该参数是jobject类型的。原生静态方法通过第二个参数获取类引用,该参数是jclass类型的。
JNI提供了自己的数据类型从而让原生代码了解Java数据类型。
Java中有两种数据类型:
1.基本数据类型:布尔型、字节型、字符型、短整型、整型、长整型、浮点型和双精度型。
2.引用类型:字符串类、数组类及其他类。
Java基本数据类型
Java类型 | JNI类型 | C/C++类型 | 大小 |
Boolean | Jboolean | unsigned_char | 无符号8位 |
Byte | Jbyte | char | 有符号8位 |
Char | Jchar | unsigned_short | 无符号16位 |
Short | Jshort | short | 有符号16位 |
Int | Jint | int | 有符号32位 |
Long | Jlong | long_long | 有符号64位 |
Float | Jfloat | float | 32位 |
Double | Jdouble | double | 64位 |
Java引用类型
Java类型 | 原生类型 |
java.lang.Class | jclass |
java.lang.Thowable | jthrowable |
java.lang.String | jstring |
Other objects | jobjects |
java.lang.Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
Other arrays | Jarray |
引用类型以不透明的引用方式传递给原生代码,而不是以原生数据类型的形式呈现,因此引用类型不能直接使用和修改。JNI提供了与这些引用类型密切相关的一组API,这些API通过JNIEnv接口指针提供给原生函数。
字符串操作
JNI把Java字符串当成引用类型来处理。这些引用类型并不像原生C字符串一样可以直接使用,JNI提供了Java字符串与C字符串之间相互转换的必要函数。因为Java字符串对象是不可变的,因此JNI不提供任何修改现有的Java字符串内容的函数。
JNI支持Unicode编码格式和UTF-8编码格式的字符串,还提供两组函数通过JNIEnv接口指针处理这些字符串编码。
创建字符串
可以在原生代码中用NewString函数构建Unicode编码格式的字符串实例,用NewStringUTF函数构建UTF-8编码格式的字符串实例。
在内存溢出的情况下,这些函数返回NULL以通知原生代码虚拟机中抛出异常,这样原生代码就会停止运行。
把Java字符串转换成C字符串
为了在原生代码中使用Java字符串,需要先将Java字符串转换成C字符串。用GetStringChars函数可以将Unicode格式的Java字符串
转换成C字符串,用GetStringUTFChars函数可以将UTF-8格式的Java字符串转换成C字符串。这些函数的第三个参数均为可选参数,该
可选参数名是isCopy,它让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。
释放字符串
通过JNI GetStringChars 函数和GetStringUTFChars函数获得的C字符串在原生代码中使用完之后需要正确地释放,否则将会引起内部泄露。
JNI提供了ReleaseStringChars函数释放Unicode编码格式的字符串,而用ReleaseStringUTFChars函数释放UTF-8编码格式的字符串。
数组操作
JNI把Java数组当成引用类型来处理,JNI提供必要的函数访问和处理Java数组。
1.创建数组
用New<Type>Array函数在原生代码中创建数组实例,其中<Type>可以是Int、Char和Boolean等,例如NewIntArray。
与NewString函数一样,在内存溢出的情况下,New<Type>Array函数将返回NULL以通知原生代码虚拟机中有异常抛出,
这样原生代码就会停止运行。
访问数组元素
JNI提供两种访问Java数组元素的方法,可以将数组的代码复制成C数组或者让JNI提供直接指向数组元素的指针。
对副本的操作
Get<Type>ArrayRegion函数将给定的基本Java数组复制到给定的C数组中,如下:
jint nativeArray[10];
(*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
原生代码可以像使用普通的C数组一样使用和修改数组元素。当原生代码想将所做的修改提交给Java数组时,可以使用Set<Type>ArrayRegion函数将C数组复制回Java数组中。如下:
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);
当数组很大时,为了对数组进行操作而复制数组会引起性能问题。在这种情况下,如果可能的话,原生代码应该只获取或设置数组元素区域而不是获取整个数组。另外,JNI提供了不同的函数集以获得数组元素而非其副本的直接指针。
对直接指针的操作
可能的话,原生代码可以用Get<Type>ArrayElements函数获取指向数组元素的直接指针。函数带有三个参数,第三个参数是可选参数,该可选参数名是isCopy,让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。
如下:
jint * nativeDirectArray;
jboolean isCopy;
nativeDirectArray=(*env)->GetIntArrayElements(env,javaArray,&isCopy);
因为可以像普通的C数组一样访问和处理数组元素,因此JNI没提供访问和处理数组元素的方法,JNI要求原生代码用完这些指针立即释放,否则
会出现内存溢出。原生代码可以使用JNI提供的Release<Type>ArrayElements函数释放Get<Type>ArrayElements函数返回的C数组。
(*env)->ReleaseIntArrayElements(env,javaArray,nativeDirectArray,0);
该函数带有四个参数,第四个参数是释放模式,如下:
释放模式 | 动作 |
0 | 将内容复制回来并释放原生数组 |
JNI_COMMIT | 将内容复制回来但是不释放原生数组,一般用于周期性地更新一个Java数组 |
JNI_ABORT | 释放原生数组但不用将内容复制回来 |
NIO操作
原生I/O(NIO)在缓冲管理区、大规模网络和文件I/O及字符集支持方面的性能有所改进。JNI提供了在原生代码中使用NIO的函数 。
与数组操作相比,NIO缓冲区的数据传送性能较好,更适合在原生代码和Java应用程序之间传送大量数据。
1.创建直接字节缓冲区
原生代码可以创建Java应用程序使用的直接字节缓冲区,该过程是以提供一个原生C字节数组为基础。
如下:
unsigned char * buffer=(unsigned char *)malloc(1024);
....
jobject directBuffer;
directBuffer=(*env)->NewDirectByteBuffer(env,buffer,1024);
注意:原生方法中的内存分配超出了虚拟机的管理范围,且不能用虚拟机的垃圾回收器回收原生方法中的内存。原生函数应该
通过释放未使用的内存分配以避免内存泄露来正确管理内存。
2.直接字节缓冲区获取
Java应用程序中也可以创建直接字节缓冲区,在原生代码中调用GetDirectBufferAddress函数可以获得原生字节数组的内存地址。如下:
unsigned char * buffer;
buffer=(unsigned char *)(*env)->GetDirectBufferAddress(env,directBuffer);
访问域
Java有两类域:实例域和静态域。类的每个实例都有自己的实例域副本,而一个类的所有实例共享同一个静态域。
JNI提供了访问两类域的函数。
获取域ID
JNI提供了用域ID访问两类域的方法,可以通过给定实例的class对象获取域ID,用GetObjectClass函数可以获得class对象。
jclass clazz;
clazz=(*env)->GetOjectClass(env,instance);
有两个获得域ID的函数分别适用于不同类型域,GetFieldId函数用于获取实例域,GetStaticFieldId用于获取静态域ID。这两个函数均
返回jfieldID类型的域ID。
jfieldID instanceFieldId;
instanceFieldId=(*env)->GetFieldID(env,clazz,"instanceField","Ljava/lang/String;");
jfieldID staticFieldId;
staticFieldId=(*env)->GetStaticFieldID(env,clazz,"staticField","Ljava/lang/String");
两个函数的最后一个参数是Java中表示域类型的
域描述符。
注意:为了提高应用程序的性能,可以缓存域ID。一般总是缓存使用最频繁的域ID。
获取域
在获得域ID之后,可以用Get<Type>Field函数获得实际的实例域,用GetStatic<Type>Field 函数获得静态域。
jstring instanceField;
instanceField=(*env)->GetObjectField(env,instance,instanceFieldId);
jstring staticField;
staticField=(*env)->GetStaticObjectField(env,clazz,staticFieldId);
在内存溢出的情况下,这些函数均返回NULL,此时原生代码不会继续执行。
注意:
获得单个阈值需要调用两到三个JNI函数,原生代码回到Java中获取每个单独的域值,这给应用程序增加了额外的负担,进而导致性能下降。
强烈建议将所有需要的参数传递给原生方法调用,而不是让原生代码回到Java中。
调用方法
与域一样,Java中有两类方法:实例方法和静态方法。JNI提供访问两类方法的函数。
获取方法ID
JNI提供了用方法ID访问两类方法的途径,可以用给定实例的class对象获得方法ID。用GetMethodID函数获得实例方法的方法ID。
用GetStaticMethodID函数获得静态域的方法ID。两个函数均返回jmethodID类型的方法ID。
jmethodID instanceMethodId;
instanceMethodId=(*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String");
jmethodID staticMethodId;
staticMethodId=(*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String")
与字段ID获取方法一样,两个函数的最后一个参数均表示
方法描述符,在Java中它表示方法签名。
注意:为了提升应用程序的性能,可以缓存方法ID。一般总是缓存使用最频繁的方法ID。
调用方法
可以以方法ID为参数通过Call<Type>Method类函数调用实际的实例方法,用CallStatic<Type>Field类函数调用静态方法。
jstring instanceMethodResult;
instanceMethodResult=(*env)->CallStringMethod(env,instance,instanceMethodId);
jstring staticMethodResult;
staticMethodResult=(*env)->CallStaticStringMethod(env,clazz,staticMethodId);
在内存溢出的情况下,这些函数均返回NULL,此时原生代码不会继续执行。
注意:Java和原生代码之间的转换是代价较大的操作,强烈建议规划Java代码和原生代码的任务时考虑这种代价,最小化这种转
换可以大大提高应用程序的性能。
域和方法描述符
Java类型签名映射
Java类型 | 签名 |
Boolean | Z |
Byte | B |
Char | C |
Short | S |
Int | I |
Long | J |
Float | F |
Double | D |
fully-qualified-class | Lfully-qualified-class |
type[] | [type |
method type | (arg-type)ret-type |
原生代码不易产生异常,处理域和调用Java方法可能导致Java异常。
异常处理
异常处理是Java程序设计语言的重要功能,JNI中的异常行为与Java中的有所不同,在Java中,当抛出一个异常时,虚拟机停止执行代码块
并进入调用栈反向检查能处理特定类型异常的异常处理程序代码块,这也叫做捕获异常。虚拟机清除异常并将控制权交给异常处理程序。相比之下,JNI要求开发人员在异常发生后显示地实现异常处理流。
捕获异常
JNIEnv接口提供了一组与异常相关的函数集,在运行过程中可以使用Java类查看这些函数。JNI提供了ExceptionOccurred函数查询虚拟机中是否有挂起的异常。在使用完之后,异常处理程序需要用ExceptionClear函数显式地清除异常。
jthrowable ex;
....
(*env)->CallVoidMethod(env,instance,throwingMethodId);
ex=(*env)->ExceptionOccurred(env);
if(0!=ex){
(*env)->ExceptionClear(env);
}
抛出异常
JNI也允许原生代码抛出异常。因为异常是Java类,应该先用FindClass函数找到异常类,用ThrowNew函数可以初始化且抛出新的异常。
jclass clazz;
....
clazz =(*env)->FindClass(env,"java/lang/NullPointerException");
if(0!=clazz){
(*env)->ThrowNew(env,clazz,"Exception message");
}
因为原生函数的代码执行不受虚拟机的控制,因此抛出异常并不会停止原生函数的执行并把控制权转交给异常处理程序。到抛出异常时,原生函数应该释放所有已分配的原生资源,例如内存及合适的返回值等。通过JNIEnv接口获得的引用是局部引用且一旦返回原生函数,它们自动地被虚拟机释放。
局部和全局引用
引用在Java程序设计中扮演非常重要的角色。虚拟机通过追踪类实例的引用并收回不再引用的垃圾来管理类实例的使用期限。因为原生代码不是一个管理环境,因此
JNI提供了一组函数允许原生代码显式地管理对象引用及使用期间原生代码。JNI支持三种引用:局部引用、全局引用和弱全局引用。
局部引用
大多数JNI函数返回局部引用。局部引用不能在后续的调用中被缓存及重用,主要因为它们的使用期限仅限于原生方法,一旦原生函数返回,局部引用即被释放。
全局引用
全局引用在原生方法的后续调用过程中依然有效,除非它们被原生代码显示释放。
创建全局引用
可以用NewGlobalRef函数将局部引用初始化为全局引用,如:
jclass localClazz;
jclass globalClazz;
....
localClazz=(*env)->FindClass(env,"java/lang/String");
globalClazz=(*env)->NewGlobalRef(env,localClazz);
....
(*env)->DeleteLocalRef(env,localClazz);
删除全局引用
当原生代码不再需要一个全局引用时,可以随时用DeleteGlobalRef函数释放它。如:
(*env)->DeleteGlobalRef(env,globalClazz);
弱全局引用
全局引用的另一种类型是弱全局引用。与全局引用一样,弱全局引用在原生方法的后续调用过程中依然有效。与全局引用不同,弱全局
引用并不阻止潜在的对象被垃圾收回。
创建弱全局引用
可以用NewWeakGlobalRef函数对弱全局引用进行初始化。
jclass weakGlobalClazz;
weakGlobalClazz=(*env)->NewWeakGlobalRef(env,localClazz);
弱全局引用的有效性检验
可以用IsSameObject函数检验一个弱全局引用是否仍然指向活动的类实例,如下:
if(JNI_FALSE==(*env)->IsSameObject(env,weakGlobalClazz,NULL)){
//对象仍然处于活动状态且可以使用
}else{
//对象被垃圾回收器收回,不能使用
}
删除弱全局引用
可以随时用DeleteWeakGlobalRef函数释放弱全局引用,如下:
(*env)->DeleteWeakGlobalRef(env,weakGlobalClazz);
全局引用显示释放前一直有效,它们可以被其他原生函数及原生线程使用。
备注说明:文中内容摘自Android NDK C++高级编程这本书。