JNI与NDK的关系
NDK可以为我们生成了C/C++的动态链接库,JNI是java和C/C++沟通的接口,两者与android没有半毛钱关系,只因为安卓是java程序语言开发,然后通过JNI又能与C/C++沟通,所以我们可以使用NDK+JNI来实现“Java+C”的开发方式。
JNIEnv与JavaVM
JNIEnv 概念 : 是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境 ;
JNIEnv 与 JavaVM : 注意区分这两个概念;
– JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个;
– JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv;
JNIEnv 作用 :
– 调用 Java 函数 : JNIEnv 代表 Java 运行环境, 可以使用 JNIEnv 调用 Java 中的代码;
– 操作 Java 对象 : Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象;
JNIEnv 体系结构
线程相关 : JNIEnv 是线程相关的, 即 在 每个线程中 都有一个 JNIEnv 指针, 每个JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv;
*.so的入口函数
JNI_OnLoad()与JNI_OnUnload()
当Android的VM(Virtual Machine)执行到System.loadLibrary()函数时,首先会去执行C组件里的JNI_OnLoad()函数。它的用途有二:
(1)告诉VM此C组件使用那一个JNI版本。如果你的.so档没有提供JNI_OnLoad()函数,VM会默认该.so档是使用最老的JNI 1.1版本。由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能,例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。
(2)由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(),所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。
JNI字符串函数
常用的JNI函数将在后续介绍,这里给出其中的字符串操作函数的函数名以及相关描述。
GetStringChars
ReleaseStringChars 获得/释放一个Unicode格式的字符串指针,可能返回一个字符串的副本
GetStringUTFChars
ReleaseStringUTFChars 获得/释放一个UTF-8格式的字符串指针,可能返回一个字符串的副本
GetStringLength 返回Unicode格式字符串的长度
GetStringUTFLength 返回UTF-8格式字符串的长度
NewString 根据Unicode格式的C字符串创建一个Java字符串
NewStringUTF 根据UTF-8格式的C字符串创建一个Java字符串
GetStringCritical
ReleaseStringCritical 获得/释放一个Unicode格式的字符串指针,可能返回一个字符串的副本【在该函数对区间内,不能使用任何JNI函数】
GetStringRegion 将Unicode格式的String复制到预分配的缓冲区中
GetStringUTFRegion 将UTF-8格式的String复制到预分配的缓冲区中
int sprintf( char *buffer, const char *format, [ argument] … );
类似于printf,根据格式化字符串format,将后续参数列表中的参数逐个输出。不过输出目标不是标准输出终端,而是字符串buffer。
字符串操作
C字符串——>java字符串
例如:下面的函数以一个C字符串为参数,并返回一个Java字符串引用类型jstring值。
jstring javastring
javastring = (*env)->NewStringUTF(env, "I LOVE YOU !");
注意,在内存溢出的情况下,NewString函数将返回NULL以通知原生代码虚拟机中有异常抛出。
java字符串转换成C字符串
为了在原生代码中使用java字符串,需要先将java字符串转换成C字符串,我们使用GetStringChars函数可以将Unicode格式的java字符串转换成C字符串,使用GetStringUTFChars函数可以将UTF-8格式的Java字符串转换成C字符串。这些函数的第三个参数均为可选参数,该可选参数名是isCopy,它让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。例如:
const jbyte* str;
jboolean isCopy;
str = (*env)->GetStringUTFChars(env, javaString,&isCopy);
if(0 != str){
printf("java String: %s",str);
if(JNI_TRUE == isCopy){
printf("C String is a copy of the java String");
}else{
printf("C String points to actual String");
}
}
释放字符串
通过JNI GetStringChars 函数GetStringUTFChars函数获得的C字符串在原生代码中使用完成之后需要正确的释放,否则将会引起内存泄漏。通常我们使用ReleaseStringChars函数释放Unicode格式的字符串,使用ReleaseUTFStringChars函数释放UTF-8格式的字符串.
(*env)->ReleaseUTFStringChars(env,javaString,str);
数组操作
JNI把java数组当成引用类型来处理,JNI提供必要的函数访问和处理Java数组。
创建数组
用NewArray函数在原生代码中创建数组实例,其中可以是Int、Char和Boolean等。例如:
jintArray javaArray;
javaArray = (*env)->NewIntArray(env,10);
if(0 != javaArray){
/*数组使用……*/
}
注意,在内存溢出的情况下,NewArray函数将返回NULL以通知原生代码虚拟机中有异常抛出。
访问数组元素
JNI提供两种访问java数组元素的方法,可以将数组的代码赋值成C数组或者让JNI提供直接执行数组元素的指针。
对副本的操作
1.java数组转C数组
GetArrayRegion函数将给定的基本Java数组赋值到给对你给的C数组中,例如:
jint nativeArray[10];
(*evn)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
2.C数组转java数组
原生代码可以像使用普通的C数组一样使用和修改数组元素。当原生代码想将所做的修改提交给java数组时,可以使用SetArrayRegion函数将C数组复制回java数组中。例如:
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);
注意:当数组很大时,对数组做复制操作会引起性能问题。
对直接指针的操作
3.java数组转C数组
原生代码可以使用GetArrayElements函数获取执行数组元素的直接指针。例如:
jint nativeDirectArray;
jboolean isCopy;
nativeDirectArray = (*env)->GetIntArrayElements(env,javaArray,&isCopy);
其中,第三个&isCopy参数为可选参数,让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。
因为可以像普通的C数组一样访问和处理数组元素,因此JNI没提供访问和处理数组元素的方法,JNI要求原生代码用完这些指针后立刻释放,否则会出现内存溢出。可以使用JNI提供的ReleaseArrayElements函数释放GetArrayElements返回的C数组。例如:
(*env)->ReleaseIntArrayRegion(env,javaArray,nativeDirectArray,0);
其中第四个参数是释放模式。
释放模式动作0将内容复制回来并释放原生数组JNI_COMMIT将内容复制回来但是不释放原生数组,一般用于周期性的更新一个java数组JNI_ABORT释放原生数组但不用将内容复制回来
NIO操作
JNI提供了在原生代码中使用NIO(I/O)的函数,与数组操作相比更适合原生代码和java应用程序之间传送大量数据。
创建直接字节缓冲区
原生代码可以创建java应用程序使用的直接字节缓冲区,该过程是以提供一个原生C字节数组为基础,例如:
unsigned char* buffer = (unsigned char*) malloc(1024)
……
jobject directBuffer;
directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);
直接字节缓冲区获取
java应用程序中也可以创建直接字节缓冲区,在原生代码中调用GetDirectBufferAddress函数可以获取原生自己数组的内存地址。例如:
unsigned char* buffer
buffer = (unsigned char*) (*env)->GetDirectBufferAddress(env,directBuffer);
JNI访问java对象属性
// 实例域
private String instanceField = "Instance Field ";
// 静态域
private static String staticField = "Static Field ";
获取域ID
JNI提供了用域ID访问两类域的方法,可以通过给定实例的class对象获取域ID,用GetObjectClass函数可以获得class对象,例如:
jclass clazz
clazz = (*env)->GetObjectClass(env,instance);
1.使用GetFieldID获取实例域的ID
jfieldID instanceFieldId;
instanceField = (*env)->GetFieldID(env,clazz,"instanceFieldId","Ljava/lang/String;");
2.使用GetStaticFieldID获取静态域的ID
jfieldID staticFieldId;
staticFieldId = (*env)->GetStaticFieldID(env,clazz,"staticFieldId","Ljava/lang/String;");
获取域
在获得域ID之后,可以用GetField函数获得实际的实例域,例如:
1.获得实例域
jstring instanceFieldId;
instanceField = (*env)->GetObjectField(env,clazz,"instanceFieldId");
2.获得静态域
jfieldID staticField;
staticFieldId = (*env)->GetStaticObjectField(env,clazz,"staticFieldId");
两个函数的最后一个参数是java中表示域类型的域描述符,其中”Ljava/lang/String;”表明域类型是Sting。
JNI调用Java方法
public class WJavaClass{
// 实例方法
private String instanceMethod(){
return "Instance Method";
}
// 静态方法
private static String staticMethod(){
return "StaticMethod";
}
}
获取方法ID
JNI提供了用方法ID访问两类方法的途径,可以用给定实例的class对象获得方法ID。用GetMethodID函数获得实例方法的方法ID,例如:
jmethodID instanceMethodId;
instanceMethodId = (*env)->GetMethodID(env,clazz,"instanceMethod","()Ljava/lang/String;");
用GetStaticMethodID函数获得静态域的方法ID,例如:
jmethodID staticMethodId;
staticMethodId = (*env)->GetStaticMethodID(env,clazz,"staticMethod","()Ljava/lang/String;");
两个函数的最后一个参数均表示方法描述符,在Java中表示方法签名。
调用方法
可以以方法ID为参数通过CallMethod类函数调用实际的实例方法,例如:
1.调用实例方法
jstring instanceMethodResult;
instanceMethodResult = (*env)->CallStringMethod(env,instance,"instanceMethodId");
2.调用静态方法
jstring staticMethodResult;
staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,"staticMethodId");
JNI调用Java静态方法案例
public class HelloJni extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
callJavaStaticMethod();
}
public native String callJavaStaticMethod();
static {
System.loadLibrary("hello-jni");
}
// 静态方法
private static String staticMethod() {
return "StaticMethod Castiel";
}
}
#include <string.h>
#include <jni.h>
#include <android/log.h>
JNIEXPORT void JNICALL
Java_com_example_hellojni_HelloJni_callJavaStaticMethod(JNIEnv *env, jclass type) {
jclass jniClass = (*env)->FindClass(env, "com/example/hellojni/HelloJni");
if (NULL == jniClass) {
__android_log_print(ANDROID_LOG_INFO,"HelloJni","can't find jclass");
return;
}
jmethodID getMId = (*env)->GetStaticMethodID(env, jniClass, "staticMethod",
"()Ljava/lang/String;");
if (NULL == getMId) {
__android_log_print(ANDROID_LOG_INFO,"HelloJni","can't find method getStringFromStatic from JniClass");
return;
}
jstring result = (*env)->CallStaticObjectMethod(env, jniClass, getMId);
const char *resultChar = (*env)->GetStringUTFChars(env, result, NULL);
(*env)->DeleteLocalRef(env, jniClass);
(*env)->DeleteLocalRef(env, result);
__android_log_print(ANDROID_LOG_INFO,"HelloJni",resultChar);
JNI异常处理
调用throwingMethod方法时,accessMethod原生方法需要显示地做异常处理。JNI提供了ExceptionOccurred函数查询虚拟机中是否有挂起的现象。例如,原生代码中的异常处理:
jthrowable ex;
……
(*env)->CallVoidMethod(env,instance,throwingMethodId);
ex = (*env)->ExceptionOccurred(env);
if(0 != ex){
(*env)->ExceptionClear(env);
/*Exception handler*/
}
抛出异常
public class WJavaClass{
// 抛出方法
private void throwingMethod() throws NullPointerException{
throw new NullPointerException("Null Pointer");
}
}
JNI也允许原生代码抛出异常。因为异常是java类,应该先用FindClass函数找到异常类。用ThrowNew函数可以初始化且抛出新的异常,例如:
jclass clazz;
……
clazz = (*env)->FindClass(env,"java/lang/NullPointerException");
if(0 !=clazz){
(*env)->ThrowNew(env,clazz,"Exception message");
}
JNI的局部引用和全局引用和弱全局引用
局部引用
大多数JNI函数返回局部引用。局部引用不能在后续的调用中被缓存及重用,主要是因为它们的使用期限仅限于原生方法,一旦原生函数返回,局部引用即被释放。例如,使用FindClass函数返回一个局部引用,当原生方法返回时,它被自动释放,也可以用DeleteLocalRef函数显示释放原生代码:
jclass clazz
clazz = (*env)->FindClass(env,"java/lang/String");
……
(*env)->DeleteLocalRef(env,clazz);
根据JNI的规范,虚拟机应该允许原生代码创建最少16个局部引用
全局引用
全局引用在原生方法的后续调用过程中依然有效,除非它们被原生代码显示释放。
1.创建全局引用
可以用NewGlobalRef函数将局部引用初始化为全局引用,例如:
jclass localclazz
jclass globalclazz
……
localclazz = (*env)->FindClass(env,"java/lang/String");
globalclazz = (*env)->NewGlobalRef(env,localclazz );
……
(*env)->DeleteLocalRef(env,localclazz );
2.删除全局引用
当原生代码不再需要一个全局引用时,可以随时用DeleteLocalRef函数释放它。
(*env)->DeleteLocalRef(env,globalclazz );
弱全局引用
弱全局引用和全局引用一样,在原生方法的后续调用过程中依然有效。与全局引用不同,弱全局引用并不阻止潜在的对象被垃圾回收。
1.创建弱全局引用
用NewWeakGlobalRef函数对弱全局引用进行初始化,例如:
jclass weakGlobalclazz
weakGlobalclazz = (*env)->NewWeakGlobalRef(env,localclazz);
2.弱全局引用的有效性校验
可以使用IsSameObject函数检验一个弱全局引用是否仍然指向活动的类实例,例如:
if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalClazz,NULL)){
/*对象仍然处于活动状态且可以使用*/
}else{
/*对象被垃圾回收期收回,不能使用*/
}
删除弱全局引用
可以随时使用DeleteWeakGlobalRef函数释放弱全局引用。
(*env)->DeleteLocalRef(env,weakGlobalClazz);