赞
踩
参考:《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, "<init>", "()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, "<init>", "(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(....);
......
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。