45

JNI基础知识

 4 years ago
source link: https://chsmy.github.io/2019/05/12/technology/JNI基础知识/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JNI(Java Native Interface),它是java中的一套接口,用来跟c和c++通信。

JNI中的数据类型

java中的数据类型和c的数据类型之间的映射关系:

java->JNI->c/c++

基本数据类型:

java JNI boolean jboolean byte jbyte char jchar short jshort int jlong long jchar float jfloat double jdouble void void

引用类型:

java JNI String jstring object jobject class jclass byte[] jByteArray object[] jobjectArray

JNI开发流程的步骤

  1. 编写native方法
  2. javah命令,生成.h头文件
  3. 复制.h头文件到CPP工程中
  4. 复制jni.h和jni_md.h文件到CPP工程中
  5. 创建jni目录
  6. 添加本地支持add native support
  7. 实现.h头文件中声明的函数
  8. 生成动态库Windows系统下是.dll文件,如果是Linux系统下是.so文件,如果是Mac系统下是.jnilib
  9. 令执行Java程序,加载动态库

java调用C,例如下面返回一个字符串

public class JniTest {
    //写一个native方法
	public native static String getStringFromC();
	
	public static void main(String[] args) {
		String text = getStringFromC();
		System.out.println(text);
	}
	//加载动态库
	static{
		System.loadLibrary("jni_study");
	}

}

C中

//函数实现
//Java_包名_类名_方法名
JNIEXPORT jstring JNICALL Java_com_chs_jni_JniTest_getStringFromC
(JNIEnv *env, jclass jcls){
	//JNIEnv 结构体指针  env是二级指针
	//代表Java运行环境,调用Java中的代码
	//将C的字符串转为一个java字符串
	return (*env)->NewStringUTF(env,"C String");
}

C调用java中的方法

访问属性,访问静态属性,访问方法,访问静态方法

先在java中定义相关的属性和方法

public class JniTest {

	//加载动态库
	static{	
		System.loadLibrary("jni_test");
	}

	public String name = "chs";
	
	public static int count = 9;
	
	public native static String getStringFromC();
	
	public native String getString2FromC(int i);
	//访问属性,返回修改之后的属性内容
	public native String accessField();
	//c调用java中的静态变量
	public native void accessStaticField();
	//c调用java中的方法
	public native void accessMethod();
	//c调用java中的静态方法
	public native void accessStaticMethod();
	//c调用java中的构造方法
	public native void accessConstructor();
	//c调用java中的父类的方法
	public native void accessNonvirtualObjectMethod();
	//中文
	public native String chuneseChars(String in)
	//传入数组
	public native void giveArray(int[] array);
	//获取数组
	public native int[] getArray(int len);
	//获取本地引用
	public native void loaclRef();
	
	public static void main(String[] args) {
		String text = getStringFromC();
		System.out.println(text);
		JniTest t = new JniTest();
		text = t.getString2FromC(6);
		System.out.println(text);
		
		t.accessField();
		t.accessStaticField();
		t.accessMethod();
		t.accessStaticMethod();
		int[] array = {9,100,10,37,5,10};
		//排序
		t.giveArray(array);
		for (int i : array) {
			System.out.println(i);
		}
		int[] array2 = t.getArray(10);
			System.out.println("------------");
		for (int i : array2) {
			System.out.println(i);
		}
		//全局引用
		t.createGlobalRef();
		System.out.println(t.getGlobalRef());
		//用完之后释放
		t.deleteGlobalRef();
		System.out.println("释放完了...");
		//System.out.println(t.getGlobalRef());
		//Java中捕捉C中的异常Exception是无法捕捉到的
		//得用Throwable捕获
		try {
			t.exeception();						
		} catch (Exception e) {
			System.out.println("发生异常:"+e.getMessage());
		}
		System.out.println("--------异常发生之后-------");
		//C中手动抛出来的异常可以捕捉到
		try {
			t.exeception();
		} catch (Exception e) {
			//e.printStackTrace();
			System.out.println(e.getMessage());
		}
		//不断调用cached方法
		for (int i = 0; i < 100; i++) {
			t.cached();
		}
	}
	
	//产生指定范围的随机数
	public int genRandomInt(int max){
		return new Random().nextInt(max); 
	}
	
	//产生UUID字符串
	public static String getUUID(){
		return UUID.randomUUID().toString();
	}

1.访问属性

这里会用到属性签名

数据类型 签名 boolean Z byte B char C short S int I long L float F double D void V object L开头,然后以/分割包的完整类型,后面再加;
比如String的签名就是Ljava/long/String Array 以[开头,在加上数组元素类型的签名,
比如int[],签名是[I,int[][]的签名是[[I,
object[]的签名是[Ljava/lang/Object
JNIEXPORT jstring JNICALL Java_com_chs_JniTest_accessField
(JNIEnv *env, jobject jobj){
	//jobj是t对象,JniTest.class
	jclass cls = (*env)->GetObjectClass(env, jobj);
	//jfieldID
	//属性名称,属性签名
	jfieldID fid = (*env)->GetFieldID(env, cls, "key", "Ljava/lang/String;");	

	//jason >> super jason
	//获取key属性的值
	//Get<Type>Field
	jstring jstr = (*env)->GetObjectField(env, jobj, fid);	
	printf("jstr:%#x\n",&jstr);

	//jstring -> c字符串
	//isCopy 是否复制(true代表赋值,false不复制)
	char *c_str = (*env)->GetStringUTFChars(env,jstr,JNI_FALSE);
	//拼接得到新的字符串
	char text[20] = "super ";
	strcat(text,c_str);

	//c字符串 ->jstring
	jstring new_jstr = (*env)->NewStringUTF(env, text);

	//修改key
	//Set<Type>Field
	(*env)->SetObjectField(env, jobj, fid, new_jstr);

	printf("new_jstr:%#x\n", &new_jstr);

	return new_jstr;
}

2.访问静态属性

JNIEXPORT void JNICALL Java_com_chs_JniTest_accessStaticField
(JNIEnv *env, jobject jobj){
	//jclass
	jclass cls = (*env)->GetObjectClass(env, jobj);
	//jfieldID
	jfieldID fid = (*env)->GetStaticFieldID(env, cls, "count", "I");
	//GetStatic<Type>Field
	jint count = (*env)->GetStaticIntField(env, cls, fid);
	count++;
	//修改
	//SetStatic<Type>Field
	(*env)->SetStaticIntField(env,cls,fid,count);
}

3.访问java方法

这里需要用到方法的签名,方法的签名的获取,可以通过javap命令,打开命令行,进入我们的java类所在的文件夹,执行javap命令 比如

javap -s -p com.chs.JniTest

执行上面的命令,就可以看到该类下面所有属性和方法的签名了。

JNIEXPORT void JNICALL Java_com_chs_JniTest_accessMethod
(JNIEnv *env, jobject jobj){
	//jclass
	jclass cls = (*env)->GetObjectClass(env, jobj);
	//jmethodID  最后一个参数是方法的签名
	jmethodID mid = (*env)->GetMethodID(env, cls, "genRandomInt", "(I)I");
	//Call<Type>Method
	jint random = (*env)->CallIntMethod(env, jobj, mid, 200);
	printf("random num:%ld",random);

4.访问静态方法

JNIEXPORT void JNICALL Java_com_chs_JniTest_accessStaticMethod
(JNIEnv *env, jobject jobj){
	//jclass
	jclass cls = (*env)->GetObjectClass(env, jobj);
	//jmethodID	
	jmethodID mid = (*env)->GetStaticMethodID(env, cls, "getUUID", "()Ljava/lang/String;");
	
	//CallStatic<Type>Method
	jstring uuid = (*env)->CallStaticObjectMethod(env, cls, mid);

	//随机文件名称 uuid.txt
	//jstring -> char*
	//第三个参数isCopy JNI_FALSE,代表java和c操作的是同一个字符串
	char *uuid_str = (*env)->GetStringUTFChars(env, uuid, NULL);
	//拼接
	char filename[100];
	sprintf(filename, "D://%s.txt",uuid_str);
	FILE *fp = fopen(filename,"w");
	fputs("i love jason", fp);
	fclose(fp);
}

5.访问构造方法

访问构造方式的时候需要传入一个” “参数

//使用java.util.Date产生一个当前的时间戳
JNIEXPORT jobject JNICALL Java_com_chs_JniTest_accessConstructor
(JNIEnv *env, jobject jobj){
    //找到要访问的class
	jclass cls = (*env)->FindClass(env, "java/util/Date");
	//找到它的构造方法
	jmethodID constructor_mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
	//实例化一个Date对象
	jobject date_obj = (*env)->NewObject(env, cls, constructor_mid);
	//找到想要调用的方法
	jmethodID mid = (*env)->GetMethodID(env, cls, "getTime", "()J");
	//调用getTime方法
	jlong time = (*env)->CallLongMethod(env, date_obj, mid);

	printf("\ntime:%lld\n",time);

	return date_obj;
}

6.调用父类的方法

Java中定义一个父类Human,和一个子类Man,父类中有个方法sayHai,子类重写该方法。

java中

Human human = new Man();

C中

JNIEXPORT void JNICALL Java_com_chs_JniTest_accessNonvirtualMethod
(JNIEnv *env, jobject jobj){
    //拿到该对象
	jclass cls = (*env)->GetObjectClass(env, jobj);
	//找到属性的ID
	jfieldID fid = (*env)->GetFieldID(env, cls, "human", "Lcom/chs/Human;");
	//获取man属性(对象
	jobject human_obj = (*env)->GetObjectField(env, jobj, fid);
	//知道父类,注意:传父类的名称
	jclass human_cls = (*env)->FindClass(env, "com/chs/Human"); 
	//执行sayHi方法
	jmethodID mid = (*env)->GetMethodID(env, human_cls, "sayHi", "()V");
	//执行CallObjectMethod,还是会调用子类的方法
	//(*env)->CallObjectMethod(env, human_obj, mid);
	//使用CallNonvirtualObjectMethod可以调用的父类的方法
	(*env)->CallNonvirtualObjectMethod(env, human_obj, human_cls, mid);
}

7.从C中返回中文乱码问题

C中字符串默认是UTF-16,想要返回到Java中不乱码,需要进行转码,C中转码非常麻烦,通过C调用Java中的String提供的类来进行转码就简单了很多。

JNIEXPORT jstring JNICALL Java_com_chs_JniTest_chineseChars
(JNIEnv *env, jobject jobj, jstring in){
	//输出
	//char *c_str = (*env)->GetStringUTFChars(env, in, NULL);
	//printf("%s\n",c_str);

	//c 转化成 jstring
	char *c_str = "我是C中的文字";
	//char c_str[] = "我是C中的文字";
	//jstring jstr = (*env)->NewStringUTF(env, c_str);
	//执行String(byte bytes[], String charsetName)构造方法需要的条件
	//1.jmethodID
	//2.byte数组
	//3.字符编码jstring
	
    //找到String类,并回去构造方法的ID
	jclass str_cls = (*env)->FindClass(env, "java/lang/String");
	jmethodID constructor_mid = (*env)->GetMethodID(env, str_cls, "<init>", "([BLjava/lang/String;)V");

	//jbyte 转换成 char 
	//jbyteArray -> char[]
	jbyteArray bytes = (*env)->NewByteArray(env, strlen(c_str));
	//byte数组赋值,C中的char跟jbyte类型是一样的
	//0->strlen(c_str),从头到尾
	//对等于,从c_str这个字符数组,复制到bytes这个字符数组
	(*env)->SetByteArrayRegion(env, bytes, 0, strlen(c_str), c_str);

	//字符编码jstring
	jstring charsetName = (*env)->NewStringUTF(env, "GB2312");

	//调用构造函数,返回编码之后的jstring
	return (*env)->NewObject(env,str_cls,constructor_mid,bytes,charsetName);
}

8.Java传数组到C中

传入一个数组并排序,操作完一定要同步回去

//比较的方法
int compare(int *a,int *b){
	return (*a) - (*b);
}
//传入
JNIEXPORT void JNICALL Java_com_chs_JniTest_giveArray
(JNIEnv *env, jobject jobj, jintArray arr){
	//jintArray -> jint指针 -> c int 数组
	jint *elems = (*env)->GetIntArrayElements(env, arr, NULL);
	//printf("%#x,%#x\n", &elems, &arr);

	//数组的长度
	int len = (*env)->GetArrayLength(env, arr);
	//排序
	qsort(elems, len, sizeof(jint), compare);	

	//同步
	//0, Java数组进行更新,并且释放C/C++数组
	//JNI_ABORT, Java数组不进行更新,但是释放C/C++数组
	//JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)
	(*env)->ReleaseIntArrayElements(env, arr, elems, JNI_COMMIT);
}

9.C返回数组到Java

操作完一定要同步回去

JNIEXPORT jintArray JNICALL Java_com_chs_JniTest_getArray(JNIEnv *env, jobject jobj, jint len){
	//创建一个指定大小的数组
	jintArray jint_arr = (*env)->NewIntArray(env, len);
	//创建jint数组
	jint *elems = (*env)->GetIntArrayElements(env, jint_arr, NULL);	
	int i = 0;
	for (; i < len; i++){
		elems[i] = i;
	}
	//同步
	(*env)->ReleaseIntArrayElements(env, jint_arr, elems, 0);	

	return jint_arr;
}

10.JNI中的引用

引用类型分为局部引用和全局引用

局部引用可以使用DeleteLocalRef手动释放对象,什么时候释放呢

  1. 当访问一个恨到的java对象,使用完成知乎,还要进行复杂的耗时操作
  2. 当创建了大量的局部引用,占用了太多的内存,这些局部引用只是暂时使用,跟后面的操作没有关联。

局部引用:

JNIEXPORT void JNICALL Java_com_chs_JniTest_localRef(JNIEnv *env, jobject jobj){
	int i = 0;
	for (; i < 5; i++){
		//创建Date对象
		jclass cls = (*env)->FindClass(env, "java/util/Date");
		jmethodID constructor_mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
		jobject obj = (*env)->NewObject(env, cls, constructor_mid);
		//省略一些操作

		//不在使用jobject对象了
		//通知垃圾回收器回收这些对象
		(*env)->DeleteLocalRef(env, obj);
		//省略一些操作...
	}
}

全局引用

全局引用可以共享数据,可以跨多个线程,使用DeleteGlobalRef释放。

jstring global_str;

//创建全局引用
JNIEXPORT void JNICALL Java_com_chs_JniTest_createGlobalRef(JNIEnv *env, jobject jobj){
	jstring obj = (*env)->NewStringUTF(env, "jni development is powerful!");
	global_str = (*env)->NewGlobalRef(env, obj);
}

//返回全局引用
JNIEXPORT jstring JNICALL Java_com_chs_JniTest_getGlobalRef(JNIEnv *env, jobject jobj){
	return global_str;
}

//释放全局引用
JNIEXPORT void JNICALL Java_com_chs_JniTest_deleteGlobalRef(JNIEnv *env, jobject jobj){
	(*env)->DeleteGlobalRef(env, global_str);
}

弱全局引用

全局引用需要手动释放,弱全局引用,跟java中的弱引用差不多,当内存不足的时候系统会释放掉这部分内存。

创建:NewWeakGlobalRef,销毁:DeleteGlobalWeakRef

11.C中异常处理

ExceptionOccurred
JNIEXPORT void JNICALL Java_com_chs_JniTest_exeception(JNIEnv *env, jobject jobj){
	jclass cls = (*env)->GetObjectClass(env, jobj);
	jfieldID fid = (*env)->GetFieldID(env, cls, "key2", "Ljava/lang/String;");
	//检测是否发生Java异常
	jthrowable exception = (*env)->ExceptionOccurred(env);
	if (exception != NULL){
		//让Java代码可以继续运行
		//清空异常信息
		(*env)->ExceptionClear(env);
		//补救措施
		fid = (*env)->GetFieldID(env, cls, "key", "Ljava/lang/String;");
	}
	//获取属性的值
	jstring jstr = (*env)->GetObjectField(env, jobj, fid);
	char *str = (*env)->GetStringUTFChars(env, jstr, NULL);
	//对比属性值是否合法
	if (_stricmp(str, "super jason") != 0){
		//认为抛出异常,给Java层处理
		jclass newExcCls = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
		(*env)->ThrowNew(env,newExcCls,"key's value is invalid!");
	}
}

12.缓存策略

如果从java中循环调用很多次下面的方法,我们让对象第一次拿到之后就缓存起来,以后再取的时候就去缓存中取,这时候可以使用局部静态变量来存储。

在实际的项目中,我们也可以在一个方法中一次初始化很多需要的全局的变量。

JNIEXPORT void JNICALL Java_com_chs_JniTest_cached(JNIEnv *env, jobject jobj){
	jclass cls = (*env)->GetObjectClass(env, jobj);	
	//获取jfieldID只获取一次
	//局部静态变量
	static jfieldID key_id = NULL;
	if (key_id == NULL){
		key_id = (*env)->GetFieldID(env, cls, "key", "Ljava/lang/String;");
		printf("--------GetFieldID-------\n");
	}
}

//初始化全局变量,动态库加载完成之后,立刻缓存起来
jfieldID key_fid;
jmethodID random_mid;
JNIEXPORT void JNICALL Java_chs_jni_JniTest_initIds(JNIEnv *env, jclass jcls){	
	key_fid = (*env)->GetFieldID(env, jcls, "key", "Ljava/lang/String;");
	random_mid = (*env)->GetMethodID(env, jcls, "genRandomInt", "(I)I");
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK