简述JNI开发

参考:《JNI_NDK开发指南》(书籍)

对JNI开发中的一些流程/细节进行总结与记录。

前言. 关于JNI

①什么是JNI

JNI全称为Java Native Interface,主要用于实现Java和C/C++的通信。
简述JNI开发_第1张图片

②JNI的优劣

优势:

  • 能够访问一些底层/系统级接口(一般这类接口都是C/C++编写的)
  • 能够直接调用C/C++,一定程度上能够提升执行效率
  • 能够避开一些java层的限制,比如JVM的内存开销过大等等
  • 确保代码在不同的平台上方便移植

劣势:

  • 程序可靠性会降低
  • 在C/C++中通过JNI访问Java的对象方法、对象属性时,相比于Java中自行调用,效率不高

1. JNI的开发流程

  • 编写声明native方法的Java类,并加载对应native函数的动态库
    • Java中native方法的声明方式:public static native xxx(xxx);
    • Java中加载native动态库的方式:System.LoadLibrary(xxx);
  • 将Java源代码编译成class字节码文件
  • 使用javah -jni命令生成.h头文件
    • javah为jdk自带指令,-jni参数表示将class中用native声明的函数生成JNI规则的函数
  • 使用本地代码实现.h头文件中的函数
  • 将本地代码编译成动态库
    • Windows:.dll库
    • Linux/Unix:.so库
    • Mac os x:.jnilib库
  • 拷贝动态库到java.library.path本地库搜索目录下,执行Java程序

2. Java加载native动态库

有两种API可实现:

①System.loadLibrary

该API只需要指定动态库名字即可,不需要加lib前缀,也不需要加so、dll、jnilib后缀。

System.loadLibrary("LibraryName");

且java会到java.library.path系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError异常。

②System.load

该API需要指定动态库的绝对路径名,且要加上前缀和后缀。

System.load("/Users/Desktop/LibraryName.so");

③加载方式

在Java类的静态代码块中加载(static关键字),防止在未加载动态库之前就调用native方法。

public class HelloWorld
{
	static
	{
		System.loadLibrary("LibraryName");
	}
} 

Java在创建类实例时,类会先被ClassLoader先加载到Java VM中,紧接着调用类的static静态代码块,所以在此时加载动态库可有效避免native方法调用比加载动态库时更早。

3. JNI中的JNIEXPORT、JNIIMPORT和JNICALL

  • 两个关键字的定义都可以在jni_md.h下找到
  • 均用于定义与平台相关的宏
  • 用于标识函数用途
    • JNIEXPORT:(实则为C++规则)放置在函数、变量或对象的声明前面,指示编译器将其导出为动态链接库的一部分。这使得其他程序可以通过在运行时加载动态链接库并使用导出的函数、变量或对象
    • JNIIMPORT:(实则为C++规则)放置在函数、变量或对象的声明前面,指示编译器将其标记为从动态链接库中导入的符号
    • JNICALL:(实则为C++规则)一种标准的函数调用约定,也被称为 “标准调用”;具有以下性质:
      • 函数的参数按照从右到左的顺序依次入栈。这意味着最右边的参数首先被压入栈中。
      • 调用方清理栈上的参数。这意味着在函数调用结束后,由调用方负责从栈上移除函数的参数。
      • 函数的返回值通常存储在 EAX 寄存器中
//Windows下的定义
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stacall

//Linux下的定义(实际是空定义)
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL

4. JVM如何查找native方法

①按照JNI规范的命名规则

即根据JNI所约定的命名规则来指定函数的命名,具体规则如下:Java_类全路径_方法名

JNIEXPORT jstring JNICALL Jave_com_test_jni_HelloWord_func(JNIEnv* env, jclass class, jstring str);
  • 第一个jstring为返回值(string类型)
  • 函数名中com_test_jni_HelloWord,代表Java类com.test.jni.HelloWorld,func代表实际命名
  • JNIEnv,指向JVM函数表的指针
  • jclass,调用Java中native方法的实例对象

②调用JNI提供的RegsterNatives函数,将本地函数注册到JVM中

//函数原型
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
  • clazz:声明native方法的类
  • methods:JNINativeMethod结构的数组
    typedef struct {
        char *name;	//java方法名称
        char *signature;//java方法签名
        void *fnPtr;//c/c++的函数指针
    } JNINativeMethod;
    
  • nMethods:指定methods数组中的本地方法数,通常写法为
    nMethods = sizeof(methods) / sizeof(JNINativeMethod)

使用方式:

package com.test.jni;

public class A{
	static{
		System.loadLibrary("A");	
	}
	
	public static native int a(String str);
	public static native boolean b();
	public static native int c(Object obj);
	
	public static void main(String[] args){
		......
	}
}
jint a(JNIEnv *env ,jclass class, jstring str){
   ....
}

jboolean b(JNIEnv *env ,jclass class){
   ....
}

jint c(JNIEnv *env ,jclass class, jobject obj){
   ....
}

static JNINativeMethod method_table[] = 
{
	{"a", "(Ljava/lang/String;)I", (void *)a},
	{"b", "()Z", (void *)b},
	{"c", "(Ljava/lang/Object;)I", (void *)c},
};

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    ......
    jclass clz = env ->FindClass(JNIREG_CLASS);
    env ->RegisterNatives(clz, method_table, sizeof(method_table) / sizeof(JNINativeMethod));
    ......
}

5. JNI数据类型说明

Java Language Type Native Type Data Type
boolean jboolean 基本数据类型
byte jbyte 基本数据类型
char jchar 基本数据类型
short jshort 基本数据类型
int jint 基本数据类型
long jlong 基本数据类型
float jfloat 基本数据类型
double jdouble 基本数据类型
所有Java引用类型的基类 jobject 引用类型
java.lang.Class jclass 引用类型
java.lang.String jstring 引用类型
所有Java数组的基类 jarray 引用类型
Object[] jobjectArray[] 引用类型
boolean[] jbooleanArray[] 引用类型
byte[] jbyteArray[] 引用类型
char[] jcharArray[] 引用类型
short[] jshortArray[] 引用类型
int[] jintArray[] 引用类型
long[] jlongArray[] 引用类型
float[] jfloatArray[] 引用类型
double[] jdoubleArray[] 引用类型
java.lang.Throwable jthrowable 引用类型

6. JNI字符串的处理

①获取字符串

JNI通过jstring来处理字符串数据,但是jstring是指向JVM内部的字符串,和C风格的字符串类型char * 不同,因此必须使用合适的JNI函数来访问JVM内部的字符串。
因为Java默认使用unicode编码,而C/C++默认使用UTF编码,所以要注意进行编码转换。

const char* GetStringUTFChars(jstring str, jbbolean *isCopy);
  • str为需要获取的字符串
  • isCopy取值JNI_TRUE和JNI_FALSE,一般填NULL即可
    • JNI_TRUE:返回JVM内部源字符串的拷贝,并为新产生的字符串分配内存空间
    • JNI_FALSE:返回JVM内部源字符串的指针,并可以指针修改源字符串的内容

②释放字符串

通过GetStringUTFChars获取到字符串并返回的为源字符串拷贝后,在使用完毕要记得释放内存。

void ReleaseStringUTFChars(jstring str, const char* utf);
  • str为需要释放的字符串指针
  • utf为字节编码

③创建字符串

jstring NewStringUTF(const char * bytes);
  • bytes为C/C++的字符串数据源

④其他字符串处理API

  • GetStringChars、ReleaseStringChars:用于获取/释放Unicode格式的字符串
  • GetStringUTFLength、GetStringLength:用于获取UTF-8/Unicode格式的字符串长度
  • GetStringCritical、ReleaseStringCritical:用于直接返回/释放源字符串的指针
    • 获取这个指针会导致暂停GC线程,如果GC线程暂停时又被其他线程触发GC的话,会出现系统死锁的阻塞调用
  • GetStringUTFRegion、GetStringRegion:用于获取UTF-8/Unicode格式字符指定范围内的内容,并会将源字符串复制到一个预先分配的缓冲区内

7. JNI处理Java回调

①处理Java静态方法回调

  1. 调用FindClass,传入Class描述符,JVM会搜索该类并返回jclass类型(用于储存Class对象的引用)
  2. 调用GetStaticMethodID,从B类中获取callStaticMethod的ID并返回jmethodID类型(用于储存方法的引用)
  3. 调用CallStaticVoidMethod,执行B.callStaticMethod方法
  4. 释放局部引用变量
package com.test.jni;

public class A{
	static{
		System.loadLibrary("A");	
	}
	
	public static native void callJavaStaticMethod();
	
	public static void main(String[] args){
		callJavaStaticMethod();
	}
}

public class B{
	public static void callStaticMethod(String str){
		......
	}
}
JNIEXPORT void JNICALL Java_com_test_jni_A_callJavaStaticMethod(JNIEnv* env, jclass class)
{
	jclass localClass = NULL;
	jstring localStr = NULL;
	jmethodID localMethodID;
	
	//查找类
	localClass  = (*env)->FindClass(env, "com/test/jni/B");
	if (localClass == NULL){return;}
	
	//查找callStaticMethod的ID
	localMethodID = (*env)->GetStaticMethodID(env, localClass, "callStaticMethod", "(Ljava/lang/String;)V");
	if (localMethodID == NULL){return;}

	//调用callStaticMethod
	localStr = (*env)->NewStringUTF(env, "This is the Test!");
	(*env)->CallStaticVoidMethod(env, localClass, localMethodID, localStr);
	
	//删除局部引用
	(*env)->DeleteLocalRef(env, localClass);
	(*env)->DeleteLocalRef(env, localStr);
}

②处理Java实例方法回调

  1. 调用FindClass,传入Class描述符,JVM会搜索该类并返回jclass类型(用于储存Class对象的引用)
  2. 调用GetMethodID,获取类的构造函数ID来创建类对象(代表构造函数方面名称)
  3. 调用GetMethodID,从D中获取callMethod的ID并返回jmethodID类型(用于储存方法的引用)
  4. 调用NewObjecth,创建类实例对象
  5. 调用CallVoidMethod,执行D.callMethod方法
  6. 释放局部引用变量
package com.test.jni;

public class C{
	static{
		System.loadLibrary("A");	
	}
	
	public static native void callJavaInstanceMethod();
	
	public static void main(String[] args){
		callJavaInstanceMethod();
	}
}

public class D{
	public void callInstanceMethod(String str){
		......
	}
}
JNIEXPORT void JNICALL Java_com_test_jni_A_callJavaStaticMethod(JNIEnv* env, jclass class)
{
	jclass localClass = NULL;
	jobject localObj = NULL;
	jstring localStr = NULL;
	jmethodID localConstructMethodID = NULL;
	jmethodID localMethodID = NULL;
	
	//查找类
	localClass  = (*env)->FindClass(env, "com/test/jni/C");
	if (localClass == NULL){return;}
	
	//获取类的默认构造函数ID
	localConstructMethodID = (*env)->GetMethodID(env, localClass, "", "()V");
	if (localConstructMethodID == NULL){return;}

	//查找实例方法callInstanceMethod的ID
	localMethodID = (*env)->GetMethodID(env, localClass, "callInstanceMethod", "(Ljava/lang/String;)V");
	if (localMethodID == NULL){return;}

	//创建该类的实例
	localObj = (*env)->NewObject(env, localClass, localConstructMethodID);
	if (localObj == NULL){return;}

	//调用实例方法callInstanceMethod
	localStr = (*env)->NewStringUTF(env, "This is the Test!");
	(*env)->CallStaticVoidMethod(env, localClass, localMethodID, localStr);
	
	//删除局部引用
	(*env)->DeleteLocalRef(env, localClass);
	(*env)->DeleteLocalRef(env, localObj);
	(*env)->DeleteLocalRef(env, localStr);
}

③说明

  • CallStaticVoidMethod/CallVoidMethod只适用于void返回值的函数调用,不同返回值所使用的API不同:
    • CallStaticIntMethod/CallIntMethod:用于int返回值的函数
    • CallStaticObjectMethod/CallObjectMethod:用于Object返回值的函数
  • 函数结束后,JVM会释放所有局部引用变量所占用的内存空间,但是建议手动释放避免自动GC异常;在JVM中维护一个引用表(包括全局和局部对象的引用),上限为512个,如果超过这个数量会造成表溢出且JVM崩溃

8. JNI访问Java类变量

①访问Java类静态变量

  1. 调用FindClass,传入Class描述符,JVM会搜索该类并返回jclass类型(用于储存Class对象的引用)
  2. 调用GetStaticFieldID,获取类的静态变量的属性ID
  3. 调用GetStaticStringField,获取静态变量的值(String类型的)
  4. 调用GetStringUTFChars,将unicode编码的java字符串转换成C风格字符串,并调用ReleaseStringUTFChars释放申请字符串的内存空间
  5. 调用NewStringUTF,创建新的字符串(UTF8格式);调用SetStaticStringField来修改静态变量的值(String类型的)
  6. 释放局部引用变量
package com.test.jni;

public class A{
	static{
		System.loadLibrary("A");	
	}
	
	public static native void modifyStaticField();
	
	public static void main(String[] args){
		B obj = new B();
		obj.setStr("HelloWorld!");
	
		modifyStaticField();
	}
}

public class B{
	private static String str;
	
	public void getStr(){
		return str;
	}
	
	public void setStr(String str){
		B.str = str;
	}
}
JNIEXPORT void JNICALL Java_com_test_jni_A_modifyStaticField(JNIEnv* env, jclass class)
{
	jclass localClass = NULL;
	jfieldID localFid = NULL;
	jstring localStr = NULL;
	jstring localNewStr = NULL;
	const char &localCStr = NULL;
	
	//查找类
	localClass  = (*env)->FindClass(env, "com/test/jni/B");
	if (localClass == NULL){return;}
	
	//获取类的静态变量的属性ID
	localFid = (*env)->GetStaticFieldID(env, localClass, "str", "Ljava/lang/String;");
	if (localFid == NULL){return;}

	//获取静态变量的值
	localStr = (*env)->GetStaticStringField(env, localClass, localFid);
	if (localStr == NULL){return;}

	//将unicode编码的java字符串转换成C风格字符串
	localCStr = (*env)->GetStringUTFChars(env, localStr, NULL);
	if (localCStr == NULL){return;}

	//修改静态变量的值
	localNewStr = (*env)->NewStringUTF(env, "This is the Test!");
	if (localNewStr == NULL){return;}
	(*env)->SetStaticStringField(env, localClass, localMethodID, localNewStr);
	
	//删除局部引用、释放字符串变量
	(*env)->DeleteLocalRef(env, localClass);
	(*env)->DeleteLocalRef(env, localStr);
	(*env)->DeleteLocalRef(env, localNewStr);
	(*env)->ReleaseStringUTFChars(env, localStr, localCStr);
}

②访问Java类实例变量

  1. 调用GetObjectClass,直接获取类实例对象
  2. 调用GetFieldID,获取类的实例变量的属性ID
  3. 调用GetObjectField,获取实例变量的值
  4. 调用GetStringUTFChars,将unicode编码的java字符串转换成C风格字符串,并调用ReleaseStringUTFChars释放申请字符串的内存空间
  5. 调用NewStringUTF,创建新的字符串(UTF8格式);调用SetObjectField来修改实例变量的值
  6. 释放局部引用变量
package com.test.jni;

public class C{
	static{
		System.loadLibrary("C");	
	}
	
	public static native void modifyInstanceField(D obj);
	
	public static void main(String[] args){
		D obj = new D();
		obj.setStr("HelloWorld!");
	
		modifyInstanceField(obj);
	}
}

public class D{
	private String str;
	
	public void getStr(){
		return this.str;
	}
	
	public void setStr(String str){
		this.str = str;
	}
}
JNIEXPORT void JNICALL Java_com_test_jni_C_modifyInstanceField(JNIEnv* env, jclass class, jobject obj)
{
	jclass localClass = NULL;
	jfieldID localFid = NULL;
	jstring localStr = NULL;
	jstring localNewStr = NULL;
	const char &localCStr = NULL;
	
	//获取类引用
	localClass  = (*env)->GetObjectClass(env, obj);
	if (localClass == NULL){return;}
	
	//获取类的实例变量的属性ID
	localFid = (*env)->GetFieldID(env, localClass, "str", "Ljava/lang/String;");
	if (localFid == NULL){return;}

	//获取实例变量的值
	localStr = (*env)->GetObjectField(env, obj, localFid);
	if (localStr == NULL){return;}

	//将unicode编码的java字符串转换成C风格字符串
	localCStr = (*env)->GetStringUTFChars(env, localStr, NULL);
	if (localCStr == NULL){return;}

	//修改静态变量的值
	localNewStr = (*env)->NewStringUTF(env, "This is the Test!");
	if (localNewStr == NULL){return;}
	(*env)->SetObjectField(env, obj, localMethodID, localNewStr);
	
	//删除局部引用、释放字符串变量
	(*env)->DeleteLocalRef(env, localClass);
	(*env)->DeleteLocalRef(env, localStr);
	(*env)->DeleteLocalRef(env, localNewStr);
	(*env)->ReleaseStringUTFChars(env, localStr, localCStr);
}

③说明

  • 在JNI中获取类实例的引用类型字段的值,都通过GetObjectField来获取;而值类型字段获取则有对应的函数:
    • GetIntField:获取int类型字段值
    • GetBooleanField:获取boolean类型字段值
  • JNI是直接操作JVM中的数据结构,因此可以不受Java访问修饰符的限制(即可以访问Java对象中的非public属性和方法)

9. 方法签名

Java中允许重载(方法名相同,参数列表不同),因此仅通过方法名来获取具体的函数ID是不够的,需要搭配参数列表才能最终确定具体的函数。
方法签名的格式为:(形参参数类型列表)返回值;其中形参参数列表中,引用类型以L开头,后面紧跟类的全路径名(需将.全部替换成/),并以分号结尾。

Descriptor Java Language Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
“()Ljava/lang/String;” String f();
“(ILjava/lang/Class;)J” long f(int i, Class c);
“([B)V” String(byte[] bytes);

10. JNI中子类对象调用父类方法的流程

  • 声明两个类Base和A,其中A继承于Base并重写函数getType和execute
  • 在JNI中
    • 创建了A的实例对象
    • 获取Base的相关方法,并使用A实例对象进行调用
  • 成功实现:通过子类对象调用父类方法(相关数据看下面的运行结果
package com.test.jni;

public class Base{
	protected String type;
	
	public Base(String type)
	{
		this.type = type;
		System.out.println("Base Construct!");
	}
	
	public String getType(){
		System.out.println("Base.getType call!");
		return this.type;
	} 

	public void execute(){
		System.out.println("Base.execute call!");
	}
}

public class A extends Base{
	public A(String type){
		super(type); //先执行父类构造函数
		System.out.println("A Construct!");
	}
	
	@Override
	public String getType(){
		return "The type is " + this.type;
	}

	@Override
	public void execute(){
		System.out.println(this.type + "A.execute call!");
	}
}

//程序入口
public class AccessMethod{
	static{
		System.loadLibrary("AccessMethod");
	}

	public static native void callSuperInstanceMethod();
	
	public static void main(String[] args){
		callSuperInstanceMethod();
	}
}
JNIEXPORT void JNICALL Java_com_test_jni_AccessMethod_callSuperInstanceMethod(JNIEnv* env, jclass class)
{
	jclass localClass = NULL;
	jclass localBaseClass = NULL;
	jmethodID localMid_init = NULL;
	jmethodID localMid_getType = NULL;
	jmethodID localMid_execute = NULL;
	jstring localStr = NULL;
	jobject localobj = NULL;
	const char &type = NULL;
	
	//获取A类的引用
	localClass  = (*env)->FindClass(env, "com/test/jni/A");
	if (localClass == NULL){return;}
	
	//获取A的构造方法ID
	localMid_init = (*env)->GetMethodID(env, localClass, "", "(Ljava/lang/String;)V");
	if (localMid_init == NULL){return;}

	//创建一个String对象作为构造方法的参数
	localStr = (*env)->NewStringUTF(env, "Test");
	if (localStr == NULL){return;}

	//创建A对象的实例
	localobj = (*env)->NewObject(env, localClass, localMid_init, localStr);
	if (localobj == NULL){return;}

	//获取Base类的引用
	localBaseClass = (*env)->FindClass(env, "com/test/jni/Base");
	if (localClass == NULL){return;}

	//调用Base类的execute方法
	localMid_execute = (*env)->GetMethodID(env, localBaseClass, "execute", "()V");
	if (localMid_execute == NULL){return;}
	(*env)->CallNonvirtualVoidMethod(env, localobj, localBaseClass, localMid_execute);

	//调用Base类的getType方法
	localMid_getType = (*env)->GetMethodID(env, localBaseClass, "getType", "()Ljava/lang/String;");
	if (localMid_getType == NULL){return;}
	localStr = (*env)->CallNonvirtualObjectMethod(env, localobj, localBaseClass, localMid_getType);

	//JNI输出相关数据
	type = (*env)->GetStringUTF(env, localStr , NULL);
	printf("[JNI] The type is %s\n", type);
	
	//删除局部引用、释放字符串变量
	(*env)->ReleaseStringUTFChars(env, localStr, type);
	(*env)->DeleteLocalRef(env, localClass);
	(*env)->DeleteLocalRef(env, localBaseClass);
	(*env)->DeleteLocalRef(env, localStr);
	(*env)->DeleteLocalRef(env, localObj);
}

运行结果:

Base Construct!
A Construct!
Base.execute call!
Base.getType call!
[JNI] The type is Test

①要点

  • JNI的NewObject方法做了两件事:
    • 创建一个未初始化的对象并分配内存空间
    • 调用对象的构造函数初始化对象
  • JNI提供了CallNonvirtualxxxMethod来支持调用不同返回值类型的实例方法
    • CallNonvirtualObjectMethod:返回引用对象的实例方法
    • CallNonvirtualVoidMethod:无返回值的实例方法
    • CallNonvirtualInttMethod:返回Int类型的实例方法

11. JNI性能优化

①JNI查找类、函数ID、字段ID

  • FindClass:查找类引用
  • GetMethodID、GetStaitcMethodID:查找实例/静态函数ID
  • GetFieldID、GetStaticFieldID:查找类实例/静态字段ID

以上三类JNI函数的性能消耗是比较大的,其中FindClass只需查找一次便可重复使用,而其他两类的调用可能会在JVM中完成消耗大量资源,因为字段和方法存在从超类继承的可能性,这会导致JVM从下往上遍历类层次结构来最终找到它们。

因此需要通过缓存的方式来减少性能消耗,以JDK1.5为例,缓存前后的大概耗时:

缓存Class 缓存FieldID 耗时(ms)
× × 79172
× 50765
× 25015
2125

可以看出,都缓存和都不缓存之间的性能差异在40倍左右。

②类静态初始化缓存

在调用一个类的方法/属性之前,JVM会先检查类是否已经加载到内存中,如果没有则会先加载,然后再调用该类的静态初始化代码块;
因此,在静态初始化该类中缓存该类中的字段ID和方法ID是一个可行的方案。

package com.test.jni;

public class AccessCache{
	public static native void initIDAndCache();
	public native void nativeMethod();
	public void callback(){
		System.out.println("AccessCache.callback!");
	}
	
	public static void main(String[] args){
		AccessCache ac = new AccessCache();
		ac.nativeMethod();
	}
	
	static{
		System.loadLibrary("AccessCache");
		initIDAndCache();
	}
}
//全局引用
jmethoID Mid_AccessCache_Callback;

JNIEXPORT void JNICALL Java_com_test_jni_AccessCache_initIDAndCache(JNIEnv* env, jclass class)
{
	Mid_AccessCache_Callback = (*env)->GetMethodID(env, cls, "callback", "()V"); 
}

JNIEXPORT void JNICALL Java_com_test_jni_AccessCache_nativeMethod(JNIEnv* env, jobject obj)
{
	(*env)->CallVoidMethod(env, obj, Mid_AccessCache_Callback);
}

流程说明:

  • JVM加载AccessCache到内存后,会调用该类的静态初始化代码块(即static代码块)
  • 先调用loadLibrary,再调用initIDAndCache
  • Java_com_test_jni_AccessCache_initIDAndCache会获取需要缓存的函数ID并存入全局变量中
  • Java中调用nativeMethod后,会执行JNI的Java_com_test_jni_AccessCache_nativeMethod,并使用当前缓存的全局变量,调用Java的callback函数

要点:

  • initIDAndCache属于类静态函数,nativeMethod属于类成员函数,因此在JNI中,第二个接收参数会有差异:
    • initIDAndCache在JNI中第二个参数为jclass,代表类
    • nativeMethod在JNI中第二个参数为jobject,代表类实例对象

12. JNI的引用问题

在Java中,一个对象没有被其他变量所引用的话,就随时可能会被GC回收;而在JNI中,就要特别注意从JVM中获取到的引用是否在使用的时候已经被GC回收由于native不能直接通过引用来操作JVM内部的数据结构,因此需要调用对应的JNI接口来间接操作所引用的数据结构。

简而言之,就是我们需要在JNI关注Java的引用的生命周期,并对其做适当的回收处理。

①JNI的三种引用

局部引用(Local Reference)
  • 通过NewLocalRef以及其他JNI接口(比如FindClass、NewObject等)所创建的对象
  • 会阻止GC回收所引用的对象
  • 不允许在native层跨函数、跨线程使用
  • 函数返回局部引用后,会被JVM自动释放
  • 可调用DeleteLocalRef进行手动释放
全局引用(Global Reference)
  • 通过NewGlobalRef所创建的对象
  • 会阻止GC回收所引用的对象
  • 允许在native层跨函数、跨线程使用
  • 不会被JVM自动释放
  • 只能调用DeleteGlobalRef进行手动释放
弱全局引用(Weak Global Reference)
  • 通过NewWeakGlobalRef所创建的对象
  • 不会阻止GC回收所引用的对象
  • 允许在native层跨函数、跨线程使用
  • 不会被JVM自动释放,JVM只会在一些情况下才会自动回收(比如内存不足的情况下)
  • 可调用DeleteWeakGlobalRef进行手动释放

②避免将局部引用在静态变量中缓存起来

局部引用的作用域只是在本函数中,当局部引用通过返回值返回到Java后,如果Java层没有使用过返回的局部引用,则GC会将该引用自动释放(即使在native层用静态变量缓存也无法阻止回收);因此静态变量中储存的就是一个野指针(被GC回收后的内存地址),当下次进行访问的时候就会造成程序崩溃。

③建议养成手动释放局部引用的习惯

通常情况下,GC会自动回收掉局部引用,但是由于Android上的JNI局部引用表有限制(上限512个),因此如果函数内部会生成大量的局部引用时,要注意在适当的时机进行清理(DeleteLocalRef),而不是等到GC来自动回收。

for (i = 0; i < 10000; i ++)
{
	jstring str = (*env)->GetObjectArrayElement(env, arr, i);
	.....
	//使用完毕后要立即释放,否则一旦超过512个局部引用上限则会程序崩溃
	(*env)->DeleteLocalRef(env, str);
}

总而言之,一旦发现局部引用对象后续不再使用,应当立即进行释放。

④可以使用EnsureLocalCapacity来确保当前局部引用数量是否足够使用

当JNI函数中需要较多的局部引用时,为了避免程序在执行过程中因局部引用不足造成崩溃,可以调用EnsureLocalCapacity来提前获取足够的局部引用。

默认情况下,JVM会支持当前函数至少16个局部引用,只有需要使用超过16个局部引用的时候才考虑使用EnsureLocalCapacity函数扩充。

jint len = 30;
if((*env)->EnsureLocalCapacity(env, len) < 0)
{
	//内存不足/局部引用剩余不足
}
else
{
	//成功创建len个局部引用
	....
}

⑤可以通过Push/PopLocalFrame来避免局部引用漏释放

//函数原型
jint PushLocalFrame(JNIEnv *env , jint capacity);
jobject(JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);

在管理局部引用的声明周期中,Push/PopLocalFrame是非常方便且安全的。可以通过PushLocalFrame来生成一个当前函数内的局部引用栈,栈数量由参数capacity指定,如果返回值为0说明创建成功。

等到函数执行结束前(return时),必须调用PopLocalFrame来释放局部引用栈内的所有局部引用。

相关使用示例:

JNIEXPORT jobject JNICALL Java_com_test_jni_xx_func(JNIEnv* env, jclass class)
{
	jobject obj = NULL;
 	jint capacity = 10;
	if((*env)->PushLocalFrame(env, capacity) < 0)
	{
		//调用失败,可能是内存不足,此时不需要调用PopLocalFrame
		return NULL;
	}
	
	obj = ....;
	
	if(....)
	{
		....
		obj = (*env)->PopLocalFrame(env, obj); //如果有需要,返回前可以先弹出栈顶的frame
		return obj; 
	}
	else
	{
		....
		(*env)->PopLocalFrame(env, NULL); //没有需求,可以不返回栈顶的frame并直接释放
		return NULL; 
	}
}

⑥可以通过弱全局引用来避免GC回收它引用的对象

弱全局引用并非一个长期持有的对象,通常在native层需要临时保存Java层对象的时候使用。

弱全局引用可以用于避免GC回收它引用的对象,因此一定程度上可以保存一些数据;但是对于类对象的引用来说,还要考虑当前这个弱全局引用对象的原引用对象是否已经GC掉了,因为原引用对象被回收,则对应的弱全局引用对象也将不可使用。

⑦引用比较

(*env)->IsSameObject(env, obj_1, obj_2);

可以通过IsSameObject来比较给定两个引用的指向是否相同:如果返回JNI_TRUE(1),说明obj_1和obj_2指向相同的对象,如果返回JNI_FALSE(0),说明两者指向不同对象。

该接口也支持引用对象和NULL进行比较,来判断当前引用对象的指向是否为NULL;但是要注意:

  • JNI的NULL引用指向的是JVM的null对象
  • 弱全局引用与NULL比较,如果返回JNI_TRUE(1)说明指向的引用已经被回收,如果返回JNI_FALSE(0)说明指向的引用仍然是个活动对象

13. 一些等价写法

①Java

static native和native static

在 Java 中,修饰符的顺序通常是固定的,遵循特定的语法规则。对于 native static 这种特定的修饰符顺序颠倒, Java 编译器对 native 和 static 修饰符的顺序并没有强制要求,Java 编译器允许在方法声明中交换 native 和 static 的位置,不会引发语法错误或警告。

②JNI

(*env)->xxx(env, …)与env->(…)
  • (*env)->xxx(env, …)是通过解引用指针访问结构体成员的方式,其中 (*env) 表示解引用指针获取到 JNIEnv 结构体本身,然后使用箭头操作符 -> 访问结构体成员函数或成员变量,参数 env 用于传递当前 JNI 环境的指针
  • env->(…)是直接通过指针访问结构体成员的方式,使用箭头操作符 -> 直接访问结构体成员函数或成员变量,不需要再进行解引用操作

14. JNI开发中的Android.mk文件

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
//以下为编译模块,必须在CLEAR_VARS和BUILD_xxxx之间执行
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_MODULE := xxx
LOCAL_SRC_FILES := xxx
include $(BUILD_xxxx)

①LOCAL_PATH

用于在开发树中查找源文件,宏函数my-dir由编译系统提供,用于返回当前路径(即Android.mk所在路径)

②include $

用于定义多个编译模块,支持CLEAR_VARS、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY等;且必须以include $(CLEAR_VARS)为起始,include $(BUILD_xxx)为终止。

CLEAR_VARS

由编译系统提供,指定让GNU MAKEFILE来清除LOCAL_XXX变量(比如LOCAL_MODULE等),不包括LOCAL_PATH,这样的清理是必要的,因为所有的编译控制文件都在同一个GNU MAKE执行环节中,所有的变量都是全局的。

BUILD_SHARED_LIBRARY

表示编译成动态库。

BUILD_STATIC_LIBRARY

表示编译成静态库。

BUILD_EXECUTABLE

表示编译成C的可执行程序。

BUILD_PACKAGE

表示编译生成 package/app/下的apk。

BUILD_PREBUILT

表示生成预编译可执行文件的makefile,主要用于生成手机上可执行程序。

BUILD_STATIC_JAVA_LIBRARY

表示编译出来jar包。

③LOCAL_MODULE

该变量必须定义。
用于确认编译的目标对象,表示Android.mk中的每一个模块,会自动生成前缀和后缀;比如命名xxx,将会生成libxxx.so的动态库文件(如果命名为libxxx,则编译系统会自动识别并忽略lib前缀,因此生成的也libxxx.so)

④LOCAL_SRC_FILES

包含将要编译打包进模块的C/C++源代码文件(比如.c/.cpp等),也可以支持C/C++的静态库文件(比如.a);
如果需要引入多个文件,则在两个文件中以空格分隔即可。

⑤LOCAL_EXPORT_C_INCLUDES

用于指定模块的公共 C/C++ 头文件的搜索路径。

当一个模块依赖于另一个模块时,需要通过 LOCAL_EXPORT_C_INCLUDES 将被依赖模块的头文件路径导出,以便编译器能够正确地解析依赖模块的头文件。

⑤LOCAL_C_INCLUDES

用于指定模块的私有 C/C++ 头文件的搜索路径。它只在当前模块的编译过程中生效,不会被导出给其他模块使用。

当一个模块需要引用自己的私有头文件时,可以将这些头文件的搜索路径添加到 LOCAL_C_INCLUDES 中,以便编译器能够正确地解析这些头文件。

15. JNI的JNI_OnLoad

JNI_Onload会在执行system.loadLibrary()函数时被调用;返回值为当前NDK使用的 JNI 版本,只能返回三种: JNI_VERSION_1_2 , JNI_VERSION_1_4 , JNI_VERSION_1_6 , 上述三个值返回任意一个没有区别。

用途:

  • 告知native层当前的JVM
  • 做一些数据初始化
  • 对Java中的native函数进行注册(即RegisterNatives)
static JavaVM *VM; //储存全局引用的JVM对象,会在其他地方使用到

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
	VM = vm;
	JNIEnv *env = nullptr;
	//判断是否能正常获取 JNIEnv 指针
	if(vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
	{
		//获取失败
		return -1;
	}
	//动态注册JNI方法(即RegisterNatives)
	......
	return JNI_VERSION_1_6;
}

16. JNI的JNIEnv

JNIEnv 指针是 JVM 创建的,用于 Native的 c/c++ 方法操纵 Java 执行栈中的数据,比如 Java Class, Java Method 等。

默认情况下,Java层的native函数在JNI的native层会有两个参数:

  • JNIEnv *env
  • jobect obj或者jclass class:在Java层,如果是static native函数则为jclass(代表类),如果是native函数则为jobect(代表类实例)

17. 在Native层调用Java代码

参考16,由Java调用native层函数时,native层可以通过默认传入的JNIEnv指针来反操作Java的数据;但是如果需要在native层主动调用Java层的数据,那么上述方式就行不通了,因为缺少JNIEnv指针。(比如native代码建立自己的线程做线程监听,并在合适的时候回调 Java 代码)

这种时候就需要将自己的线程 Attach到JVM上(调用AttachCurrentThread函数),这样会返回一个可用的JNIEnv指针;当使用完毕后需要解绑线程(调用DetachCurrentThread函数)。

可以通过JNI_EDETACHED来判断自己的线程是否已经绑定。

以上处理方式可以通过设计一个智能指针类来实现:

class JNIEnvCustomPtr{
public:
    JNIEnvCustomPtr(JavaVM *vm) : vm_{vm}, env_{nullptr}, need_detach_{false} {
    	if (!vm_) {
    		//如果没有传JVM则直接返回
    		return;
    	}
        if (vm_->GetEnv((void**) &env_, JNI_VERSION_1_6) == JNI_EDETACHED) {
            GetJVM()->AttachCurrentThread(&env_, nullptr); //线程绑定
            need_detach_ = true; 
        }
    }

    ~JNIEnvCustomPtr() {
        if (need_detach_) {
            GetJVM()->DetachCurrentThread(); //线程解绑
        }
    }
	
	//重载运算符->,使智能指针类能够以env->xxx的方式调用
    JNIEnv* operator->() {
        return env_;
    }

private:
    JNIEnvCustomPtr(const JNIEnvCustomPtr&) = delete;
    JNIEnvCustomPtr& operator=(const JNIEnvCustomPtr&) = delete;

private:
    JNIEnv* env_;
    bool need_detach_;
};

使用方式

JavaVM *vm;

void callback(int type)
{
	JNIEnvCustomPtr* ptr = new JNIEnvCustomPtr(vm);
	jclass class = (*ptr)->FindClass(....);
	jmethodID id = (*ptr)->GetMethodID(....);
	(*ptr)->CallVoidMethod(....);
	......
}

你可能感兴趣的:(游戏开发,游戏程序,java,android,studio)