JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植;从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互
本地代码与 Java 虚拟机之间是通过 JNI 函数实现相互操作的;JNI 函数通过接口指针来获得,本地方法将 JNI 接口指针当作参数来接受;虚拟机保证在从相同的 Java 线程中对本地方法进行多次调用时,传递给本地方法的接口指针是相同的,本地方法被不同的 Java 线程调用时,它接受不同的 JNI接口指针
使用JNI和算法进行交互,主要是为了提高算法的性能,最大化的利用机器硬件资源
JNI是Java NativeInterface(Java本地接口)的缩写;JNI作为java和操作系统间的一个直接接口,可以通过JNI使得java直接调用操作系统的资源
目前JNI只能通过c/C++实现,因为jni只是对操作系统资源调用的一个桥接过程;所以理论上在windows下只要是dll文件均可以被调用;java代码编译之后是运行在一个jvm里,所以java的任何操作对操作系统而言都是隔着一层虚拟机外壳,这点也正是java的优点,帮助java实现了“Write Once, Run Everywhere”的可移植性
但是使用了jni之后必须要明白这个“Write Once, Run Everywhere”要被打破,必须要实现不同的操作系统的各种jni版本
JNI的开发调用过程可以用下图来完整表示:
JNI中数据类型分为两种,一种是基础数据类型,另一种是引用类型;在调用Java native方法将实参传递给C/C++函数的时候,会自动将java形参的数据类型自动转换成C/C++相应的数据类型,所以我们在写JNI程序的时候,必须要明白它们之间数据类型的对应关系;下面先介绍基础数据类型
JNI中的基本类型也称为Native基本类型
Java基本数据类型与JNI数据类型的映射关系如下:
Java数据类型 | JNI数据类型 | C/C++数据类型 |
---|---|---|
boolean | jboolean | unsigned char |
byte | jbyte | signed char |
char | jchar | unsigned short |
short | jshort | short |
int | jint | int |
long | jlong | long long |
float | jfloat | float |
double | jdouble | double |
void | void | void |
修改 JniHello.java
/**
* Created by act64 on 2017/5/25.
*/
public class JniHello {
static{
System.loadLibrary("nativehello");
}
public static void main(String[]arg){
JniHello jniHello =new JniHello();
System.out.println( jniHello.hello("world"));
}
public native String hello(String str);
}
为了方便搞明白数据类型和参数以及对于的描述符,还是使用javah生成
//在terminal里面输入
javac JniHello.java
javah JniHello
这样目录下就生成了JniHello.h文件
打开 JniHello.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class JniHello */
#ifndef _Included_JniHello
#define _Included_JniHello
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JniHello
* Method: hello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniHello_hello
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
我们在意的是Signature 和JNICALL Java_JniHello_hello的参数和返回值,将这些参数运用到hello.c中
#include
#include
jstring c_hello(JNIEnv * env, jobject mJobject,jstring val){
const jbyte *str;
str = (*env)->GetStringUTFChars(env, val, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("java IN %s", str);
(*env)->ReleaseStringUTFChars(env, val, str);
return (*env)->NewStringUTF(env, "helloJNIStr");
}
const JNINativeMethod methods[]={
{"hello","(Ljava/lang/String;)Ljava/lang/String;",(jstring *)c_hello},
};
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
JNIEnv *env;
jclass cls;
if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_4)) {
return JNI_ERR; /* JNI version not supported */
}
cls = (*env)->FindClass(env, "JniHello");
if (cls == NULL) {
return JNI_ERR;
}
if((*env)->RegisterNatives(env,cls,methods,1)<0){
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
然后编译文件
gcc -fPIC -I /usr/lib/jvm/java-1.7.0-openjdk-amd64/include -shared -o libnativehello.so hello.c
export LD_LIBRARY_PATH=./
javac JniHello.java
java JniHello
结果如下
JAVA以其跨平台的特性深受人们喜爱,而又正由于它的跨平台的目的,使得它和本地机器的各种内部联系变得很少,约束了它的功能
解决JAVA对本地操作的一种方法就是JNI;JAVA通过JNI调用本地方法,而本地方法是以库文件的形式存放的(在 WINDOWS平台上是DLL文件形式,在UNIX机器上是SO文件形式)。通过调用本地的库文件的内部方法,使JAVA可以实现和本地机器的紧密联系, 调用系统级的各接口方法
“一次编程,到处使用”的Java软件概念原本就是针对网上嵌入式小设备提出的,几经周折,目前SUN公司已推出了 J2ME(Java 2 P1atform Micro Edition)针对信息家电的Java版本,其技术日趋成熟,开始投入使用
SUN公司Java虚拟机(JVM)技术的有序开放,使得Java软件真正 实现跨平台运行,即Java应用小程序能够在带有JVM的任何硬软件系统上执行;加上Java语言本身所具有的安全性、可靠性和可移植性等特点,对实现瘦 身上网的信息家电等网络设备十分有利,同时对嵌入式设备特别是上网设备软件编程技术产生了很大的影响。也正是由于JNI解决了本机平台接口调用问题,于是 JNI在嵌入式开发领域也是如火如荼
局部引用
● 通过 NewLocalRef 和各种 JNI 接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)
● 会阻止 GC 回收所引用的对象,不在本地函数中跨函数使用,不能跨线程使用。函数返回后局部引用所引用的对象会被JVM 自动释放,或调用 DeleteLocalRef 释放。(*env)->DeleteLocalRef(env,local_ref)
全局引用
● 调用 NewGlobalRef 基于局部引用创建,会阻 GC 回收所引用的对象
● 可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。(*env)->DeleteGlobalRef(env,g_cls_string)
弱全局引用
● 调用 NewWeakGlobalRef 基于局部引用或全局引用创建,不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用
● 引用不会自动释放,在 JVM 认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef 手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)
大多数JNI函数返回局部引用。局部引用不能在后续的调用中被缓存及重用,主要是因为它们的使用期限仅限于原生方法,一旦原生函数返回,局部引用即被释放
例如,使用FindClass函数返回一个局部引用,当原生方法返回时,它被自动释放,也可以用DeleteLocalRef函数显式释放原生代码:
jclass clazz
clazz = (*env)->FindClass(env,"java/lang/String");
……
(*env)->DeleteLocalRef(env,clazz);
根据JNI的规范,虚拟机应该允许原生代码创建最少16个局部引用
全局引用在原生方法的后续调用过程中依然有效,除非它们被原生代码显示释放
创建全局引用
可以用NewGlobalRef函数将局部引用初始化为全局引用,例如:
1 jclass localclazz
2 jclass globalclazz
3 ……
4 localclazz = (*env)->FindClass(env,"java/lang/String");
5 globalclazz = (*env)->NewGlobalRef(env,localclazz );
6 ……
7 (*env)->DeleteLocalRef(env,localclazz );
删除全局引用
当原生代码不再需要一个全局引用时,可以随时用DeleteLocalRef函数释放它
(*env)->DeleteLocalRef(env,globalclazz );
弱全局引用和全局引用一样,在原生方法的后续调用过程中依然有效。与全局引用不同,弱全局引用并不阻止潜在的对象被垃圾回收
创建弱全局引用
用NewWeakGlobalRef函数对弱全局引用进行初始化,例如:
1 jclass weakGlobalclazz
2 weakGlobalclazz = (*env)->NewWeakGlobalRef(env,localclazz);
弱全局引用的有效性校验
可以使用IsSameObject函数检验一个弱全局引用是否仍然指向活动的类实例。如果基本对象没被垃圾收集,则返回JNI_FALSE,否则返回JNI_TRUE。例如:
1 if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalClazz,NULL)){
2 /*对象仍然处于活动状态且可以使用*/
3 }else{
4 /*对象被垃圾回收期收回,不能使用*/
5 }
删除弱全局引用
可以随时使用DeleteWeakGlobalRef函数释放弱全局引用。
(*env)->DeleteLocalRef(env,weakGlobalClazz);
需要完整代码的同学可以点击底层源码免费获取
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
PS:有问题欢迎指正,可以在评论区留下你的建议和感受;
欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下