参考:《JNI_NDK开发指南》(书籍)
对JNI开发中的一些流程/细节进行总结与记录。
JNI全称为Java Native Interface,主要用于实现Java和C/C++的通信。
优势:
劣势:
有两种API可实现:
该API只需要指定动态库名字即可,不需要加lib前缀,也不需要加so、dll、jnilib后缀。
System.loadLibrary("LibraryName");
且java会到java.library.path系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError异常。
该API需要指定动态库的绝对路径名,且要加上前缀和后缀。
System.load("/Users/Desktop/LibraryName.so");
在Java类的静态代码块中加载(static关键字),防止在未加载动态库之前就调用native方法。
public class HelloWorld
{
static
{
System.loadLibrary("LibraryName");
}
}
Java在创建类实例时,类会先被ClassLoader先加载到Java VM中,紧接着调用类的static静态代码块,所以在此时加载动态库可有效避免native方法调用比加载动态库时更早。
//Windows下的定义
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stacall
//Linux下的定义(实际是空定义)
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
即根据JNI所约定的命名规则来指定函数的命名,具体规则如下:Java_类全路径_方法名
JNIEXPORT jstring JNICALL Jave_com_test_jni_HelloWord_func(JNIEnv* env, jclass class, jstring str);
//函数原型
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
typedef struct {
char *name; //java方法名称
char *signature;//java方法签名
void *fnPtr;//c/c++的函数指针
} JNINativeMethod;
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));
......
}
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 | 引用类型 |
JNI通过jstring来处理字符串数据,但是jstring是指向JVM内部的字符串,和C风格的字符串类型char * 不同,因此必须使用合适的JNI函数来访问JVM内部的字符串。
因为Java默认使用unicode编码,而C/C++默认使用UTF编码,所以要注意进行编码转换。
const char* GetStringUTFChars(jstring str, jbbolean *isCopy);
通过GetStringUTFChars获取到字符串并返回的为源字符串拷贝后,在使用完毕要记得释放内存。
void ReleaseStringUTFChars(jstring str, const char* utf);
jstring NewStringUTF(const char * bytes);
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);
}
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);
}
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);
}
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);
}
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); |
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函数的性能消耗是比较大的,其中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);
}
流程说明:
要点:
在Java中,一个对象没有被其他变量所引用的话,就随时可能会被GC回收;而在JNI中,就要特别注意从JVM中获取到的引用是否在使用的时候已经被GC回收。由于native不能直接通过引用来操作JVM内部的数据结构,因此需要调用对应的JNI接口来间接操作所引用的数据结构。
简而言之,就是我们需要在JNI关注Java的引用的生命周期,并对其做适当的回收处理。
局部引用的作用域只是在本函数中,当局部引用通过返回值返回到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);
}
总而言之,一旦发现局部引用对象后续不再使用,应当立即进行释放。
当JNI函数中需要较多的局部引用时,为了避免程序在执行过程中因局部引用不足造成崩溃,可以调用EnsureLocalCapacity来提前获取足够的局部引用。
默认情况下,JVM会支持当前函数至少16个局部引用,只有需要使用超过16个局部引用的时候才考虑使用EnsureLocalCapacity函数扩充。
jint len = 30;
if((*env)->EnsureLocalCapacity(env, len) < 0)
{
//内存不足/局部引用剩余不足
}
else
{
//成功创建len个局部引用
....
}
//函数原型
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;
}
}
弱全局引用并非一个长期持有的对象,通常在native层需要临时保存Java层对象的时候使用。
弱全局引用可以用于避免GC回收它引用的对象,因此一定程度上可以保存一些数据;但是对于类对象的引用来说,还要考虑当前这个弱全局引用对象的原引用对象是否已经GC掉了,因为原引用对象被回收,则对应的弱全局引用对象也将不可使用。
(*env)->IsSameObject(env, obj_1, obj_2);
可以通过IsSameObject来比较给定两个引用的指向是否相同:如果返回JNI_TRUE(1),说明obj_1和obj_2指向相同的对象,如果返回JNI_FALSE(0),说明两者指向不同对象。
该接口也支持引用对象和NULL进行比较,来判断当前引用对象的指向是否为NULL;但是要注意:
在 Java 中,修饰符的顺序通常是固定的,遵循特定的语法规则。对于 native static 这种特定的修饰符顺序颠倒, Java 编译器对 native 和 static 修饰符的顺序并没有强制要求,Java 编译器允许在方法声明中交换 native 和 static 的位置,不会引发语法错误或警告。
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)
用于在开发树中查找源文件,宏函数my-dir
由编译系统提供,用于返回当前路径(即Android.mk所在路径)
用于定义多个编译模块,支持CLEAR_VARS、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY等;且必须以include $(CLEAR_VARS)为起始,include $(BUILD_xxx)为终止。
由编译系统提供,指定让GNU MAKEFILE来清除LOCAL_XXX变量(比如LOCAL_MODULE等),不包括LOCAL_PATH,这样的清理是必要的,因为所有的编译控制文件都在同一个GNU MAKE执行环节中,所有的变量都是全局的。
表示编译成动态库。
表示编译成静态库。
表示编译成C的可执行程序。
表示编译生成 package/app/下的apk。
表示生成预编译可执行文件的makefile,主要用于生成手机上可执行程序。
表示编译出来jar包。
该变量必须定义。
用于确认编译的目标对象,表示Android.mk中的每一个模块,会自动生成前缀和后缀;比如命名xxx,将会生成libxxx.so的动态库文件(如果命名为libxxx,则编译系统会自动识别并忽略lib前缀,因此生成的也libxxx.so)
包含将要编译打包进模块的C/C++源代码文件(比如.c/.cpp等),也可以支持C/C++的静态库文件(比如.a);
如果需要引入多个文件,则在两个文件中以空格
分隔即可。
用于指定模块的公共 C/C++ 头文件的搜索路径。
当一个模块依赖于另一个模块时,需要通过 LOCAL_EXPORT_C_INCLUDES 将被依赖模块的头文件路径导出,以便编译器能够正确地解析依赖模块的头文件。
用于指定模块的私有 C/C++ 头文件的搜索路径。它只在当前模块的编译过程中生效,不会被导出给其他模块使用。
当一个模块需要引用自己的私有头文件时,可以将这些头文件的搜索路径添加到 LOCAL_C_INCLUDES 中,以便编译器能够正确地解析这些头文件。
JNI_Onload会在执行system.loadLibrary()函数时被调用;返回值为当前NDK使用的 JNI 版本,只能返回三种: JNI_VERSION_1_2 , JNI_VERSION_1_4 , JNI_VERSION_1_6 , 上述三个值返回任意一个没有区别。
用途:
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;
}
JNIEnv 指针是 JVM 创建的,用于 Native的 c/c++ 方法操纵 Java 执行栈中的数据,比如 Java Class, Java Method 等。
默认情况下,Java层的native函数在JNI的native层会有两个参数:
参考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(....);
......
}