JNI与NDK的关系
https://www.jianshu.com/p/0d45d59352e5
https://www.jianshu.com/p/b4431ac22ec2
https://www.cnblogs.com/hejunlin/p/12747263.html
https://blog.csdn.net/shaohuazuo/article/details/43309625
https://www.jianshu.com/p/5f5d7f132c53
-
JNIEnv概念 :
JNIEnv是一个线程相关的结构体, 该结构体代表了 Java 在本线程的运行环境。通过JNIEnv可以调用到一系列JNI系统函数。 -
JNIEnv线程相关性:
每个线程中都有一个 JNIEnv 指针。JNIEnv只在其所在线程有效, 它不能在线程之间进行传递。
注意:在C++创建的子线程中获取JNIEnv,要通过调用JavaVM的AttachCurrentThread函数获得。在子线程退出时,要调用JavaVM的DetachCurrentThread函数来释放对应的资源,否则会出错。
将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回
类型描述符。对于,没有返回值的,用V(表示void型)表示。举例如下:
Java层方法 JNI函数签名
String test ( ) Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I
void set (byte[ ] bytes) ([B)V
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);
作者:private_object
链接:https://www.jianshu.com/p/0d45d59352e5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。