JNI可以提高应用程序的性能,但是破坏了程序的可移植性,换平台的话,需要重新编译本地代码。
下面说一下JNI本地程序编写中的一些坑,以及避坑的方法;
局部引用超限
当我们通过FindClass,NewStringUtf等获取jclass或jobject,如果没有调用DeleteLocalRef删除局部引用,可能会出现内存泄漏或局部引用超限(local reference table overflow)的问题。
局部引用(Local Reference)是native code中对Java对象的映射,相当于持有一个Java对象的引用。局部引用属于JNI的引用类型,即是jobject或其子类。
局部引用限于其创建的堆栈帧和线程,并且在其创建的堆栈帧返回时会自动删除。也就是说一般情况下局部引用会在返回Java方法时自己删除。但调用过程中如果存在循环、递归等调用层次过多的情况,很可能会导致局部引用数量超过局部引用限制导致崩溃。
另一方面如果本地方法没有返回Java层,或本地线程没有断开与JVM的连接,局部引用无法自动释放会导致内存泄漏或局部引用超限的问题。
因此,在局部引用使用完毕后,需要尽快调用DeleteLocalRef手动删除局部引用。
未调用DetachCurrentThread导致线程无法正常退出
在natvie线程中调用了AttachCurrentThread连接到虚拟机,但线程退出前未调用DetachCurrentThread取消连接,会导致线程无法正常退出,有类似错误日志:”thread exiting, not yet detached”,甚至导致VM abort。
JNIEnv是一个指向全部JNI方法的指针。该指针只在创建它的线程有效,不能跨线程传递。
如果是从Java层通过native方法调用到C/C++方法,则会创建一个栈桢(stack frame)储存虚拟机相关信息,包括JNIEnv指针,即在native函数的入参处可获得。且此种情况不需要调用DetachCurrentThread取消连接。如:
JNIEXPORT void JNICALL nativeCallJava(JNIEnv *env, jobject thiz){
object_global = (jobject)env->NewGlobalRef(thiz);
***env = getEnv();***
jclass clazz = env->FindClass("com/xxxx/jni/MainActivity");
jmethodID method1 = env->GetStaticMethodID(clazz,"nativeCall","(Ljava/lang/String;)V");
jstring result = env->NewStringUTF("aaaaaaaaa");
env->CallStaticVoidMethod (clazz, method1,result);
jmethodID method2 = env->GetMethodID(clazz,"nativeCall_nonStatic","(Ljava/lang/String;)V");
env->CallVoidMethod (thiz, method2,result);
env->DeleteLocalRef( clazz );
env->DeleteLocalRef( result );
env->DeleteLocalRef( method2 );
}
如果是在native层通过pthread_create等方式创建的线程,则需要调用了AttachCurrentThread连接到虚拟机,才能获取JNIEnv指针。且在线程退出前需要调用DetachCurrentThread取消连接。如:
void* testcallJava_thread_func(void* p){
LOGE("testcallJava_thread_func in 1");
JNIEnv *env ;
if(savedVm == NULL){
LOGE("savedVm == NULL");
}
if (savedVm->AttachCurrentThread(&env, 0) != 0)
{
LOGE("Failed to attach current thread");
return 0;
}
// jclass clazz = env->FindClass("com/xxxx/jni/MainActivity");//在多线程中FindClass会报错
jmethodID method1 = env->GetStaticMethodID(clazz_global,"nativeCall","(Ljava/lang/String;)V");
jstring result = env->NewStringUTF("BBBBBBBBB");
env->CallStaticVoidMethod (clazz_global, method1,result);
jmethodID method2 = env->GetMethodID(clazz_global,"nativeCall_nonStatic","(Ljava/lang/String;)V");
env->CallVoidMethod (object_global, method2,result);
savedVm->DetachCurrentThread();//不调用这一句,会报错
}
多线程场景下FindClass调用失败
在自己创建的线程(类似通过pthread_create)中调用FindClass会失败得到空的返回,从而导致调用失败。
如果在Java层调用到native层,会携带栈桢(stack frame)信息,其中包含此应用类的Class Loader,因此场景下JNI能通过此应用类加载器获取类信息。 而在使用自己创建并Attach到虚拟机的线程时,因为没有栈桢(stack frame)信息,此场景下虚拟机会通过另外的系统类加载器寻找应用类信息,但此类加载器并未加载应用类,因此FindClass返回空。
解决方法有原理相同的两种方法:缓存Class,将Class的对象全局化;缓存缓存应用类的Class Loader解决此问题,通过这个类就可以获取Class的信息;
如:
// java代码
public class JniAdapter {
public static ClassLoader getClassLoader() {
return JniAdapter.class.getClassLoader();
}
}
// C/C++代码
JavaVM *MSDKJniHelper::java_vm_ = NULL;
jobject MSDKJniHelper::class_loader_obj_ = NULL;
jmethodID MSDKJniHelper::find_class_mid_ = NULL;
//主线程中,在JNI_Onload方法中执行这个方法
void MSDKJniHelper::SetJavaVM(JavaVM *vm) {
...... java_vm_ = vm;
JNIEnv *env;
if (!getenv_(&env))
{
return;
//获取ClassLoader,这里不是子线程,FindClass可以完成
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
//获取JniAdapter
jclass adapterClass = env->FindClass("com/xxxx/msdk/framework/JniAdapter");
if (adapterClass) {
jmethodID getClassLoader = env->GetStaticMethodID(adapterClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
jobject obj = env->CallStaticObjectMethod(adapterClass, getClassLoader);
//全局变量化
class_loader_obj_ = env->NewGlobalRef(obj);
find_class_mid_ = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
//删除本地引用
env->DeleteLocalRef(classLoaderClass);
env->DeleteLocalRef(adapterClass);
env->DeleteLocalRef(obj);
}
}
又如:
jclass clazz_global;//用于在多线程中回调java中的方法。
jint registerNativeMethods(JNIEnv* env, const char *class_name, JNINativeMethod *methods, int num_methods) {
int result = 0;
jclass clazz = env->FindClass(class_name);
//全局变量
clazz_global = (jclass)env->NewGlobalRef(clazz);
if(clazz == NULL){
return JNI_FALSE;
}
result = env->RegisterNatives(clazz, methods, num_methods);
if(result < 0){
return JNI_FALSE;
}
return result;
}
//clazz_global在需要用的地方直接用就可以
extern 和 static区别
1、static
在C语言中,static可以用来修饰局部变量,全局变量以及函数。在不同的情况下static的作用不尽相同。
(1)修饰局部变量
一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。
但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。
如:
#include<stdio.h>
void fun()
{
staticint a=1;
a++;
printf("%d\n",a);
}
int main(void)
{
fun();
fun();
return0;
}
执行结果为:2 , 3;这就说明第二次执行fun方法的时候,并没有再次对a进行初始化。
(2)修饰全局变量
对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。
//有file1.c
#include<stdio.h>
int a=1;
//file2.c
#include<stdio.h>
//在别的文件中声明、赋值
**extern int a;**
int main(void)
{
printf("%d",a);
return 0;
}
执行结果:1
但是如果在file1.c中把int a=1改为static int a=1;
那么在file2.c是无法访问到变量a的。原因在于用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见。
即extern修饰变量时作用域是整个工程;static修饰变量时的作用域为当前文件
(3)修饰函数
用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。
2、extern
在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
在上面的例子中可以看出,在file2中如果想调用file1中的变量a,只须用extern进行声明即可调用a,这就是extern的作用。
在这里要注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。
其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间。
在以后工作用还会遇到很多JNI方面的坑,还会继续补充。。。。。未完待续
参考:
http://www.10tiao.com/html/330/201711/2653579453/1.html
https://www.cnblogs.com/zhaodun/p/6432615.html
谢谢以上两位作者;