Android JNI开发(8)--JNI出坑指南

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

谢谢以上两位作者;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值