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基本数据类型
Java的基本数据类型可以直接与C/C++的响应基本数据类型映射:
JNI引用类型
与基本数据类型不同,引用类型对原生方法是不透明的,他们的内部数据结构并不直接向原生代码公开。
获取域ID和方法ID均分别需要域描述符合方法描述符,域描述符符合方法描述符都可以通过下表中的java类型签名映射获得。
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值。
- 1
- 2
注意,在内存溢出的情况下,NewString函数将返回NULL以通知原生代码虚拟机中有异常抛出。
java字符串转换成C字符串
为了在原生代码中使用java字符串,需要先将java字符串转换成C字符串,我们使用GetStringChars函数可以将Unicode格式的java字符串转换成C字符串,使用GetStringUTFChars函数可以将UTF-8格式的Java字符串转换成C字符串。这些函数的第三个参数均为可选参数,该可选参数名是isCopy,它让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
释放字符串
通过JNI GetStringChars 函数GetStringUTFChars函数获得的C字符串在原生代码中使用完成之后需要正确的释放,否则将会引起内存泄漏。通常我们使用ReleaseStringChars函数释放Unicode格式的字符串,使用ReleaseUTFStringChars函数释放UTF-8格式的字符串.
- 1
数组操作
JNI把java数组当成引用类型来处理,JNI提供必要的函数访问和处理Java数组。
创建数组
用NewArray函数在原生代码中创建数组实例,其中可以是Int、Char和Boolean等。例如:
- 1
- 2
- 3
- 4
- 5
注意,在内存溢出的情况下,NewArray函数将返回NULL以通知原生代码虚拟机中有异常抛出。
访问数组元素
JNI提供两种访问java数组元素的方法,可以将数组的代码赋值成C数组或者让JNI提供直接执行数组元素的指针。
对副本的操作
1.java数组转C数组
GetArrayRegion函数将给定的基本Java数组赋值到给对你给的C数组中,例如:
- 1
- 2
- 3
2.C数组转java数组
原生代码可以像使用普通的C数组一样使用和修改数组元素。当原生代码想将所做的修改提交给java数组时,可以使用SetArrayRegion函数将C数组复制回java数组中。例如:
- 1
注意:当数组很大时,对数组做复制操作会引起性能问题。
对直接指针的操作
3.java数组转C数组
原生代码可以使用GetArrayElements函数获取执行数组元素的直接指针。例如:
- 1
- 2
- 3
- 4
其中,第三个&isCopy参数为可选参数,让调用者确定返回的C字符串地址指向副本还是指向堆中的固定对象。
因为可以像普通的C数组一样访问和处理数组元素,因此JNI没提供访问和处理数组元素的方法,JNI要求原生代码用完这些指针后立刻释放,否则会出现内存溢出。可以使用JNI提供的ReleaseArrayElements函数释放GetArrayElements返回的C数组。例如:
- 1
- 2
其中第四个参数是释放模式。
释放模式动作0将内容复制回来并释放原生数组JNI_COMMIT将内容复制回来但是不释放原生数组,一般用于周期性的更新一个java数组JNI_ABORT释放原生数组但不用将内容复制回来
NIO操作
JNI提供了在原生代码中使用NIO(I/O)的函数,与数组操作相比更适合原生代码和java应用程序之间传送大量数据。
创建直接字节缓冲区
原生代码可以创建java应用程序使用的直接字节缓冲区,该过程是以提供一个原生C字节数组为基础,例如:
- 1
- 2
- 3
- 4
直接字节缓冲区获取
java应用程序中也可以创建直接字节缓冲区,在原生代码中调用GetDirectBufferAddress函数可以获取原生自己数组的内存地址。例如:
- 1
- 2
- 3
JNI访问java对象属性
- 1
- 2
- 3
- 4
- 5
获取域ID
JNI提供了用域ID访问两类域的方法,可以通过给定实例的class对象获取域ID,用GetObjectClass函数可以获得class对象,例如:
- 1
- 2
- 3
- 4
- 5
- 6
1.使用GetFieldID获取实例域的ID
- 1
- 2
2.使用GetStaticFieldID获取静态域的ID
- 1
- 2
获取域
在获得域ID之后,可以用GetField函数获得实际的实例域,例如:
1.获得实例域
- 1
- 2
2.获得静态域
- 1
- 2
- 3
两个函数的最后一个参数是java中表示域类型的域描述符,其中”Ljava/lang/String;”表明域类型是Sting。
JNI调用Java方法
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
获取方法ID
JNI提供了用方法ID访问两类方法的途径,可以用给定实例的class对象获得方法ID。用GetMethodID函数获得实例方法的方法ID,例如:
- 1
- 2
用GetStaticMethodID函数获得静态域的方法ID,例如:
- 1
- 2
- 3
两个函数的最后一个参数均表示方法描述符,在Java中表示方法签名。
调用方法
可以以方法ID为参数通过CallMethod类函数调用实际的实例方法,例如:
1.调用实例方法
- 1
- 2
- 3
2.调用静态方法
- 1
- 2
JNI调用Java静态方法案例
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
JNI异常处理
调用throwingMethod方法时,accessMethod原生方法需要显示地做异常处理。JNI提供了ExceptionOccurred函数查询虚拟机中是否有挂起的现象。例如,原生代码中的异常处理:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
抛出异常
- 1
- 2
- 3
- 4
- 5
- 6
JNI也允许原生代码抛出异常。因为异常是java类,应该先用FindClass函数找到异常类。用ThrowNew函数可以初始化且抛出新的异常,例如:
- 1
- 2
- 3
- 4
- 5
- 6
JNI的局部引用和全局引用和弱全局引用
局部引用
大多数JNI函数返回局部引用。局部引用不能在后续的调用中被缓存及重用,主要是因为它们的使用期限仅限于原生方法,一旦原生函数返回,局部引用即被释放。例如,使用FindClass函数返回一个局部引用,当原生方法返回时,它被自动释放,也可以用DeleteLocalRef函数显示释放原生代码:
- 1
- 2
- 3
- 4
- 5
根据JNI的规范,虚拟机应该允许原生代码创建最少16个局部引用
全局引用
全局引用在原生方法的后续调用过程中依然有效,除非它们被原生代码显示释放。
1.创建全局引用
可以用NewGlobalRef函数将局部引用初始化为全局引用,例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
2.删除全局引用
当原生代码不再需要一个全局引用时,可以随时用DeleteLocalRef函数释放它。
- 1
弱全局引用
弱全局引用和全局引用一样,在原生方法的后续调用过程中依然有效。与全局引用不同,弱全局引用并不阻止潜在的对象被垃圾回收。
1.创建弱全局引用
用NewWeakGlobalRef函数对弱全局引用进行初始化,例如:
- 1
- 2
- 3
2.弱全局引用的有效性校验
可以使用IsSameObject函数检验一个弱全局引用是否仍然指向活动的类实例,例如:
- 1
- 2
- 3
- 4
- 5
- 6
删除弱全局引用
可以随时使用DeleteWeakGlobalRef函数释放弱全局引用。
- 1