Android Jni开发细节总结

上篇文章介绍了Android studio下Cmake配置编译开发jni总结,这篇介绍JNi开发的细节。Android Jni开发中比java开发不论是在编译阶段还是运行阶段都有更多的细节点需要注意,稍有不慎就会不断地进入填坑模式,扫码加入日志,不断慢慢调试,下面是个人最近开发中所遇到细节整理如下:

1、认识JNI中的JavaVM和JNIEnv对象

在标准的java平台下,每一个Process里面可以产生很多JavaVM对象,每一个java VM对象都有一个与之对应的JavaVM对象。但是,在Android平台上,每一个Process只能产生一个DalvikVM对象,也就是说,在一个Android的进程中,是通过有且只有一个虚拟器对象来服务所有的Java和C/C++代码的。
Android中JNIEnv对象和Dalvik的Java VM对象的关系如下:
(1)一个JNIEnv*内部包含一个Pointer,指针指向Dalvik的Java VM对象的FunctionTable,JNIEnv*关于程序执行环境的众多函数正是来源于Dalvik虚拟机;
(2)Android中每当一个java线程第一次要调用本地C/C++代码时,Dalvik虚拟机实例会为该java线程产生一个JNIEnv*指针以及当前调用者对象jobject;
(3)java每个线程在和C/C++互相调用时,JNIEnv*是相互独立的,互不干扰,这种做法提升了并发执行时的安全性;
(4)当本地的C/C++代码想获得当前线程所要使用的JNIEnv时,可以使用Dalvik VM对象的JavaVM*jvm->GetEnv()方法,该方法会返回当前线程所在的JNIEnv*。

2、可变参数使用细节

在头文件stdarg.h中声明了一种类型(va_list)和三个宏(va_start/va_arg/va_end)用来实现可变参数,与java中的可变参数使用和原理不一样。java中在编译器看来就是个数组,而在c/c++中是个结构体实现,故更强大,可以实现不同类型的参数传入,在函数中va_arg根据不同类来获取下一个参数。

相关说明

 void va_start(va_list ap, lastfix);
  type va_arg(va_list ap, type);
  void va_end(va_list ap);

va_list: 该变量保存了可变参数列表的一些信息,va_arg和va_end都要用到它。
va_start:这个宏会使得ap"指"向可变参数列表的第一个参数的首地址(参数列表中第一个是确定参数,之后的才是可变参数),它必须在va_arg和va_end前先被使用,其中va_start有两个参数,ap含义如上,lastfix是传递给函数的最后一个明确(非可变)的参数值(函数传参是自右向左,最后一个其实就是正着数的第一个)其中JNIEnv这个参数不能作为va_start
va_arg:这个宏的作用其实就是返回ap所"指"向的type类型(!的参数值
   重点1: type不能为char,unsigned char,float
   重点2:首次使用va_arg时,返回的是可变参数列表的第一个参数,每次调用成功都会依次返回参数列表中的下一个参数。它的实现依赖于ap的指向,根据type类型参数的长度来向后移动ap从而指向下个返回对象
va_end:该宏通常起一个返回(结束)作用,它会“释放”ap使其指向NULL从而使该变量失效除非再次调用va_start初始化(可能又是windows下?)。va_end应该放于参数列表全部被va_arg读取完之后使用,调用失败可能会导致一些未定义的错误。

参考:http://blog.csdn.net/u011560601/article/details/45510811

如下:

int foo(char* fmt, ...){
    va_list args;
    va_start(args, fmt);
    int i = va_arg(args, int);
    double f = va_arg(args, double);
    va_end(args);
}

在开发中我们很可能是传入同一种类型参数,希望循环处理,这样简单。但是这里针对实例类型的实参引用会有个必现隐藏问题,出现崩溃代码如下

  jstring apend(JNIEnv *env, jstring arg1, ...){
   va_list arg_strs;
   va_start(arg_strs, arg1);
  jstring tmpJstr;
    while ((tmpJstr = va_arg(arg_strs, jstring))) {
        env->CallObjectMethod(stringBuilder, append_method, tmpJstr);
    }
}

崩溃如下

 A/art: art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: use of deleted local reference 0x10002d

其所指的崩溃通过日志定位就是指tmpJstr,当arg_strs移动到最后时,赋值给tmpJstr就是个无效引用,不能再使用进行判断,即不能对最后一个非基本类型的引用进行引用非空判断。故这里改造的话,通过传入个数来判断到达最后一个参数:

  jstring apend(JNIEnv *env, int count, ...){
   va_list arg_strs;
   va_start(arg_strs, count);

  for (int index = 0; index < count; index++) {
        env->CallObjectMethod(stringBuilder, append_method, va_arg(arg_strs, jstring));
    }
     va_end(arg_strs);
}

3、关于引用删除DeleteLocalRef使用

在前面的文章Android Studio下Ndk开发踩过的坑以及解办法决中提到过引用过多会导致崩溃,不能超过512个。故平时要保持对不需要的引用类型,即继承jobect的,除去基本类型以及jmethodId,jfieldid等不需要之外,其余就剩jobject,jstring,jclass等。但这里要说的是,在笔者开发中因这个习惯让自己在调试错误中总出现了使用了已经删除的引用错误,如下:

JNI DETECTED ERROR IN APPLICATION: use of deleted local reference 0xd9

这种错误查找起来很头疼,没有具体方便告诉你是个引用使用错导致智能一个个打log来定位位置。

这里总结一个回收删除准则:
函数内新定义的局部引用各自负责回收,对函数入参引用类型以及函数返回值引用不回收,均交给外包函数调用者本事回收

一个特殊情况看起来不符合

jstring apend(JNIEnv *env,jstring arg1,...){
 va_list arg_strs;
 va_start(arg_strs, arg1);
 jstring operatorType = va_arg(arg_strs, jstring);
 env->CallObjectMethod(hashmap, put_method, env->NewStringUTF("operatorType"), operatorType);

    }

如果在append 把可变参数operatorType 回收删除引用,而外部调用者传入该参的引用也删除,就会出现重复而导致上述错误,其实在apend里面 jstring operatorType就是入参,不是新生成的引用跟外面传入的参数引用是一个,不需要调用DeleteLocalRef方法。

对于Char*的回收

 const char* key =env->GetStringUTFChars(keyJ, NULL);
 //回收char* 
  env->ReleaseStringUTFChars(keyJ, key); 

总结:对于Java开发人员来说无需关系垃圾回收,完全由虚拟机GC来负责垃圾回收,而对于JNI开发人员,对于内存释放需要谨慎处理,需要的时候申请,使用完记得释放内容,以免发生内存泄露。在JNI提供了三种Reference类型,Local Reference(本地引用), Global Reference(全局引用), Weak Global Reference(全局弱引用)。其中Global Reference如果不主动释放,则一直不会释放;对于其他两个类型的引用都是释放的可能性,那是不是意味着不需要手动释放呢?答案是否定的,不管是这三种类型的那种引用,都尽可能在某个内存不再需要时,立即释放,这对系统更为安全可靠,以减少不可预知的性能与稳定性问题。

注意:ART虚拟机在GC算法有所优化,为了减少内存碎片化问题,在GC之后有可能会移动对象内存的位置,对于Java层程序并没有影响,但是对于JNI程序可要小心了,对于通过指针来直接访问内存对象是,Dalvik能正确运行的程序,ART下未必能正常运行。
一个例子关于可能内存地址错误:
获取sharepreference数据,通过java程序获取的结果和通过jni调用java程序竟然返回的值不一样,还仅发现为long型返回值时,一直没有找到原因。这个还不是普通就出现了,而是在这样的调用下java->jni->java->jni->java时出现的,直接java-jni-java下没有问题,这个只是猜测。

4、如何抛异常

1) jni中有专门抛出异常的方法如下

       jclass clz = env->FindClass("java/lang/NullPointerException");
        jmethodID methodId = env->GetMethodID(clz, "<init>", "()V");
        jthrowable throwable = (jthrowable) env->NewObject(clz, methodId);
        env->Throw(throwable);

或者:
 env->ThrowNew(env->FindClass("java/lang/Exception"), "this is Exception error form C++");

这样抛出之后 java层直接try catch。本地方法接口通过在函数末尾增加throws***同时需要把异常抛出。

注意的是:这种抛出不会阻止jni方法内函数执行结束,会继续往下执行,并发生异常而终止,如下所示:

  jstring getStringNative(JNIEnv *env, jobject context){  
     ......
    if (result) {
        env->Throw((jthrowable) newLoginAuthResult(env, result, msg));
    }
    LOGI(" getModel 4");
    getFieldID = env->GetFieldID(pre_class, "accessCode", "Ljava/lang/String;");
.....
}

异常如下:

 art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: JNI GetFieldID called with pending exception java.lang.Exception: ****

通过日志和异常发现均走到了下面的env->GetFieldID方法这,但是只要执行jni操作都会崩溃,故不能在抛出之后执行相关操作,必须结束该方法返回。

jni中对应的捕获如下:

    jthrowable throwEx = evn->ExceptionOccurred();
    if (throwEx) {
       //必须ExceptionClear处理,否则会异常未处理
        evn->ExceptionClear();
        //可以继续往上层抛出
        evn->Throw(throwEx);
    }

注意:Java层出现异常,虚拟机会直接抛出异常,这是需要try..catch或者继续往外throw。但是对于JNI出现异常时,即执行到JNIEnv中某个函数异常时,若捕获到异常,必须用 evn->ExceptionClear();来处理异常,然后可以继续执行jni下面的程序,相当于java中的try…catch动作捕获到异常,但是Throw动作之后就不能。
另外,Dalvik虚拟机有些情况下JNI函数出错可能返回NULL,但ART虚拟机在出错时更多的是抛出异常。这样导致的问题就可能是在Dalvik版本能正常运行的程序,在ART虚拟机上由于没有正确处理异常而崩溃。

2)通过C/C++语言中throw
发现虽然能终止方法执行下去,但是无法抛出异常给java处理,直接崩溃,结束程序。暂不清楚为啥

5、jni线程使用回调java方法

大致步骤(转载):JNI全局回调java方法
1、jni里面调用java方法的大致步骤是:根据jobject获取jclass(静态方法就不用这一步了)–> 获取jmethodid –> 调用方法。
2、jni里面调用java方法的环境分为2种。
第一种:在env所在线程调用java方法,这种情况不需要做特殊处理,直接按照步骤执行即可。
第二种:在pthread子线程调用java方法,这种情况下就需要做处理了。在jni中,子线程中是不能直接调用JNIEnv对象的,也不能直接调用env线程中的jobject对象,因为:jni中,JNIEnv是和线程相关的,每一个native方法所在线程就有一个当前线程相关的JNIEnv对象,而pthread线程中是不能调用native方法所在线程的JENnv对象的,解决办法是:利用JavaVM虚拟机,JavaVM是和进程相关的,一个进程里面的JavaVM都是同一个,所以在pthread线程中就可以通过JavaVM来获取(AttachCurrentThread)当前线程的JNIEnv指针,然后就可以使用JNIEnv指针操作数据了;还有在pthread线程中调用jobject对象时,首先需要把native线程里面的jobject创建全局引用(env->NewGlobalRef(jobj)),其返还的jobject对象就可以在程序中使用了。
3、在JNI_OnLoad中获取我们需要的JavaVM指针。

相关jni中线程知识参考如下
Jni线程-创建线程
Jni线程— 线程锁之生产者消费者

6、关于静态变量的使用

(1)Jni开发中静态变量使用遵守C++的相关规范,除了在头文件中申明之外,必须要单独在cpp文件中进行初始化,这不同于普通类对象,会动态分配内存空间,故静态变量必须手动分配,即单独初始化,也不能在类的声明头文件中初始化。否则会报Error:(12) undefined reference toXXXX’
(2)使用静态变量缓存时候,不能缓存jni方法中的局部变量,因为在方法执行结束之后会自动释放引用,若在其他方法中使用这个静态变量,会崩溃报使用了
use the delete local reference`

***.h
class Native_Object {
public:
    Native_Object(JNIEnv *jniEnv, jobject obj);

    //缓存的必须是全局变量或者外部java相同线程环境中的变量
    static jobject sLoader;

    static void setSloader(jobject obj);

    static jobject getSloader();

private:
    JNIEnv *m_jniEnv;
};


****.cpp

//在此初始化
jobject  Native_Object::sLoader = NULL;

void Native_Object::setSloader(jobject obj) {
    Native_Object::sLoader = obj;
}

jobject Native_Object::getSloader() {
    return Native_Object::sLoader;
}

即使include了

 externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -frtti -fexceptions  -lz"
            }
        }

二、在cmakeLists.txt中target_link_libraries配置中添加该库配置 z

target_link_libraries( # Specifies the target library.
                       ymm_log

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
                       z
                       )

更多文章:
Android JNI原理分析
C语言基础

猜你喜欢

转载自blog.csdn.net/u010019468/article/details/78717034