JNI自用手册2 - 基础篇
文章目录
JNI使用手册,其二,基础篇。
JNI基本概念
JNI定义
- JNI (Java Native Interface)
- 是一种在Java虚拟机控制下执行代码的标准机制;
- 是native code的编程接口;
- 是允许Java代码和其他语言代码进行双向交互;
JNI优势和劣势
- 优势
- 能够重用native代码;
- 实现可用类库中缺少的功能;
- 提高执行性能或其他与环境相关的系统特性;
- 满足跨包调用、绕过Java安全性检查等特殊情况;
- 劣势
- Java到Native Code的上下文切换耗时、低效;
- 会触发JVM崩溃和内存泄漏;
JNI元素
JNI提供两种主要数据结构,”JavaVM“和”JNIEnv“,其本质上都是指向函数表指针的指针。
JavaVM
- JavaVM是JVM在JNI层的代表,JNI全局只有一个;
- JavaVM提供执行接口函数,使用这些函数可以创建和销毁JavaVM;
- 理论上每个进程可以有多个JavaVM,但Android只运行一个;
JNIEnv
- JNIEnv是线程相关的结构体,代表Java在本线程的运行环境,每个线程都有一个,JNI中可能有多个JNIEnv;
- 通过JNIEnv可以调用Java代码,操作传入的Java对象;
- JNIEnv用于线程本地存储,不能在线程之间共享JNIEnv变量。若无法获取到某个线程的JNIEnv,则需要共享JavaVM,调用GetEnv来获取JNIEnv;
- Native方法的参数,比Java函数声明多出两个,如(JNIEnv *, jobject, jstring),JNIEnv作为第一个参数
- 若原生函数非静态函数,则第二个参数是对对象的引用;
- 若原生函数是静态函数,则第二个参数是对Java类的引用;
JNIEnv图示
- JNIEnv接口指针,指向一个线程相关的结构,类似C++的虚函数表,该结构包含了指向函数表的指针,函数表的每个入口包含一个指向JNI函数的指针;
- JNI接口指针仅在当前线程中起作用,指针不能从一个线程进入另一个线程,但可以在不同的线程中调用本地方法;不要跨线程传递JNI接口指针;
JNI线程模型
线程基础
- Native线程由操作系统调度;Java线程由JavaVM调度;
- Native线程受具体操作系统的限制,Java线程可以跨平台,若两边的线程和同步概念存在不一致,程序将无法正常执行;
线程执行
- Native代码执行在其调入的Java方法所在的调用栈中;
- 调用栈从Java层到Native层,JNI不会改变调用栈,也不会改变线程环境,除非开发者指定;
- Android不会挂起正在执行本地代码的线程,如果当前垃圾回收器正在运行,或者调试器遇到问题需要挂起,Android在下次JNI调用时才会暂停线程;
线程转换
- Java层调用Native方法
- 将会创建一个栈帧(Stack Frame)存储JNIEnv指针等VM信息
- 线程结束时无需调用DetachCurrentThread;
- Native层通过pthread_create等方式创建的线程调用Java方法
- 需要调用AttachCurrentThread或AttachCurrentThreadAsDaemon,依附到JavaVM上,从而获取到JNIEnv指针;
- Native中线程的创建,由pthread_create转换为Android::createJavaThread;
- 在已经attach的线程上调用AttachCurrentThread是个多余操作;
- 线程退出时需调用DetachCurrentThread;
- Java层调用Native方法
JNI调用和传递
Native方法命名
- 前缀
Java_
- 完整类名(类名中的
.
用_
代替) - 下划线
_
- 方法名(方法名中的特殊字符需要转义)
- 参数签名(非必须,有重载方法的时候才需要),如果有重载的本地方法,需要再添加两个下划线
__
,然后再添加方法签名(由java字段描述符描述,用_
代替描述符中的包名分割/
符,签名中的特殊字符需要转义)
转义字符
转义符 | 说明 |
---|---|
_0XXXX | 一个Unicode字符XXXX。注意小写是用来表示非ascii Unicode字符, 如:_0abcd与_0ABCD不相同 |
_1 | 字符_ |
_2 | 参数签名中的字符; |
_3 | 参数签名中的字符[ |
域描述符
- 基本类型
Java类型 | 本地类型(JNI) | 域描述符 | 描述 |
---|---|---|---|
boolean | jboolean | Z | 无符号8bit |
byte | jbyte | B | 有符号8bit |
char | jchar | C | 无符号16bit |
short | jshort | S | 有符号16bit |
int | jint | I | 有符号32bit |
long | jlong | J | 有符号64bit |
float | jfloat | F | 32bit |
double | jdouble | D | 64bit |
void | void | V | N/A |
- 数组,
[
,如:int[]-> [I - 对象
- 以
L
开头,以;
结尾,中间是用/
隔开的包及类名。如:Ljava/lang/String; - 如果是嵌套类,则用
$
来表示嵌套。如:Landroid/os/FileUtils$FileStatus;
- 以
JNI对象管理
对象传递
- 基础数据类型直接映射,如int映射成jint,在Java和Native之间是采用值传递;
- 引用类型以一种不透明的引用方式向Native传递对象;
内存回收
- Java以类似引用计数的方式管理对象;
- JVM必须要追踪所有传到Native Code的Java对象;
- Native Code在使用Java对象时,需要确保该对象在使用过程中不被回收;
- Native Code需要能够通知JVM不再需要某些Java对象;
- JVM在适当的时机触发GC(Garbage Collection)操作,清理不再使用的对象;
对象映射
- 当线程从Java环境切换到Native Code上下文,JVM分配一块内存,为每一个从Java到Native Method的过渡控制建立一个注册表,表中存放本次native method执行中创建的所有局部引用;
- 运行native method的线程堆栈,记录局部引用表的内存位置;
- 局部引用表实现局部引用到Java对象的映射;当Native Code中引用到一个Java对象,JVM在表中创建一个局部引用,引用计数+1,阻止了所引用的对象被回收;
- DeleteLocalRef释放局部引用,从表中删除引用,即减少相应Java对象的引用计数;
- 在native method执行完毕后,所有的局部引用被删除,生命周期结束;
- 局部引用表存在容量限制,初始大小64,最大为512;超出会引起报错:local reference table overflow (max=512);
局部引用
- 传入native方法的所有参数,通过NewLocalRef和各种JNI接口创建的对象都是局部引用;
- 不能跨函数、跨线程使用;
- 尽早调用DeleteLocalRef,避免潜在内存泄漏;
- 可以用PushLocalFrame和PopLocalFrame更方便地管理局部引用;
- 理解局部引用和native code中局部变量的区别;
- 存储位置,一个在线程堆栈,一个在局部引用表;
- 生命周期;
- 访问限制,局部引用需要借助JNI函数实现间接访问;
// Java public Object func() {...} // JNI jobject obj = env->CallObjectMethod(...);
另一种局部引用管理方式
jobject func(JNIEnv *env, ...) { jobject result; if (env->PushLocalFrame(10) < 0) { /* 调用PushLocalFrame获取10个局部引用失败 */ return NULL; } ... result = ...; // 创建局部引用result if (...) { /* 返回前先弹出栈顶的frame */ result = env->PopLocalFrame(result); return result; } ... result = env->PopLocalFrame(result); /* 正常返回 */ return result; }
全局引用
- 全局引用表容量存在限制,初始大小为512,最大为51200;
- 调用NewGlobalRef基于局部引用创建,长期保存一个对象避免被自动GC;
- 可以跨函数、跨线程使用;
- 会阻止JVM自动GC回收该对象;必须调用DeleteGlobalRef手动释放;
- NewGlobalRef操作了同一个对象,但是引用并不是同一个;
- jfield和jmethod是透明类型,GetStringUtfChars和GetByteArrayElements返回的原始数据指针,都不是对象;
jobject localRef = xxx; jobject globalRef = env->NewGlobalRef(localRef); env->DeleteLocalRef(localRef);
弱全局引用
- 调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止JVM自动GC回收该对象;
- 引用不会自动释放,需要调用DeleteWeakGlobalRef手动释放;或者在JVM认为应该回收时被回收释放,但其在引用表中所占用的内存不会被回收;
- 可以跨函数、跨线程;
引用比较
- env->IsSameObject(obj1,obj2),可以判定两个引用是否指向同一对象,相同返回JNI_TRUE(1),否则返回JNI_FALSE(0);
- JNI中的NUL引用,指向Java中的null对象;
- IsSameObject用于弱全局引用与NULL的比较时,返回值意义不同,JNI_TRUE表示指向的引用已被回收,JNI_FALSE表示指向一个活动对象;