美文网首页
JNI简单总结

JNI简单总结

作者: 梧叶已秋声 | 来源:发表于2022-08-31 14:12 被阅读0次

    写在开头:本文参考了Android-JNI开发系列这个系列的大纲去做总结。
    本文将从3个方面去简单总结下JNI。

    1. 基础知识

    • 1.1 JNI简介
    • 1.2 Java基础数据类型和引用类型分别和JNI的对应关系
    • 1.3 方法签名
    • 1.4 头文件的生成/书写和规则
    • 1.5 JNIEnv和JavaVM

    2. Java和JNI交互

    • 2.1 函数的注册
    • 2.1.1 静态注册
    • 2.1.2 动态注册
    • 2.1.3 优缺点对比
    • 2.2 Java调用JNI
      • 2.2.1 传递基本的数据类型到JNI层
      • 2.2.2 传递复杂的数据类型到JNI层
      • 2.2.3 如何在JNI层获取传递过来的数据
    • 2.3 JNI调用Java
      • 2.3.1 如何创建Java层的任意对象
      • 2.3.2 如何调用Java类的成员方法/属性,静态方法/属性
      • 2.3.3 回调
    • 2.4 JNI多线程
      • 2.4.1 如何在JNI子线程中回调到Java层
      • 2.4.2 线程的创建销毁等待
      • 2.4.3 JNI中如何保证线程安全
    • 2.5 熟悉JNI常见方法

    3.引用

    • 3.1 局部引用
    • 3.2 全局引用
    • 3.3 弱全局引用
    • 3.4 三种引用的区别和使用场景
    • 3.5 缓存
    • 3.6 内存回收机制

    1. 基础知识

    1.1 JNI简介

    https://developer.android.google.cn/training/articles/perf-jni?hl=zh_cn

    JNI 是指 Java 原生接口,JNI是JAVA语言自己的特性,也就是说JNI和Android没有关系。

    为什么需要JNI

    有些事情Java无法处理时,JNI允许程序员用其他编程语言来解决,例如,Java标准库不支持的平台相关功能或者程序库。也用于改造已存在的用其它语言写的程序,供Java程序调用。许多基于JNI的标准库提供了很多功能给程序员使用,例如文件I/O、音频相关的功能。当然,也有各种高性能的程序,以及平台相关的API实现,允许所有Java应用程序安全并且平台独立地使用这些功能。

    这里顺带提一下NDK,Android中使用NDK这个工具进行JNI开发。


    https://developer.android.google.cn/ndk/guides?hl=zh_cn

    1.2 Java基础数据类型和引用类型分别和JNI的对应关系

    基本数据类型Java与Native映射关系如下表所示:

    Java JNI中的别名 C/C++中的类型 字节数
    boolean jboolean unsigned char 1
    byte jbyte signed char 1
    char jchar unsigned short 2
    short jshort short 2
    int jint/jsize long 4
    long jlong __int64 8
    float jfloat float 4
    double jdouble double 8

    引用数据类型
    外面的为JNI中的,括号中的Java中的。

    • jobject
      • jclass (java.lang.Class objects)
      • jstring (java.lang.String objects)
      • jarray (arrays)
        • jobjectArray (object arrays)
        • jbooleanArray (boolean arrays)
        • jbyteArray (byte arrays)
        • jcharArray (char arrays)
        • jshortArray (short arrays)
        • jintArray (int arrays)
        • jlongArray (long arrays)
        • jfloatArray (float arrays)
        • jdoubleArray (double arrays)
      • jthrowable (java.lang.Throwable objects)

    上面的层次中的jni的引用类型代表了继承关系,jbooleanArray继承jarray,jarray继承jobject,最终都继承jobject。


    https://zhuanlan.zhihu.com/p/93114273

    下面列出Java和其对应的JNI函数。

    //Java 层
    public native void data(byte b, char c, boolean bool, short s, int i, float f, double d, long l, float[] floats);
    
    //JNI层
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_data_JNIData_data(JNIEnv *env, jobject thiz, jbyte b, jchar c,
                                             jboolean j_bool,
                                             jshort s, jint i, jfloat f, jdouble d, jlong l,
                                             jfloatArray floats) {
        LOG_D("byte=%d", b);
        LOG_D("jchar=%c", c);
        LOG_D("jboolean=%d", j_bool);
        LOG_D("jshort=%d", s);
        LOG_D("jint=%d", i);
        LOG_D("jfloat=%f", f);
        LOG_D("jdouble=%lf", d);
        LOG_D("jlong=%lld", l);
    
        jfloat *float_p = env->GetFloatArrayElements(floats, nullptr);
        jsize size = env->GetArrayLength(floats);
        for (int index = 0; index < size; index++) {
            LOG_D("floats[%d]=%lf", index, *(float_p++));
        }
        env->ReleaseFloatArrayElements(floats, float_p, 0);
    }
    
    

    1.3 方法签名

    什么是方法签名?

    // jni.h
    typedef struct {
        const char* name;  //Java层native函数名
        const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型
        void*       fnPtr; //Native层对应的函数指针
    } JNINativeMethod;
    

    JNINativeMethod结构体中有一个signature(签名),这个就是方法签名。Method结构体中的signature这个char字符。

    我们平时定义的int,float,String等类型在JVM虚拟机中,存储数据类型的名称时是使用描述符来存储。
    基本数据类型对应的描述符:

    类型描述符 Java Native
    B byte jbyte
    C char jchar
    D double jdouble
    F float jfloat
    I int jint
    S short jshort
    J long jlong
    Z boolean jboolean
    V void void

    数组数据类型是在前面添加[

    类型描述符 Java Native
    [B byte[] jbyteArray
    [C char[] jcharArray
    [D double[] jdoubleArray
    [F float[] jfloatArray
    [I int[] jintArray
    [S short[] jshortArray
    [J long[] jlongArray
    [Z boolean[] jbooleanArray

    复杂数据类型:L+classname +;
    classname规则是:类全名(包名+类名)将原来的.分隔符换成/ 分隔符

    类型描述符 Java Native
    Ljava/lang/String; String jstring
    L+classname +; 所有对象 jobject
    [L+classname +; Object[] jobjectArray
    Ljava.lang.Class; Class jclass
    Ljava.lang.Throwable; Throwable jthrowable

    Java方法签名格式:(输入参数...)返回值参数

    Java函数 对应的签名
    void foo() ()V
    float foo(int i) (I)F
    long foo(int[] i) ([I)J
    double foo(Class c) (Ljava/lang/Class;)D
    boolean foo(int[] i,String s) ([ILjava/lang/String;)Z
    String foo(int i) (I)Ljava/lang/String;

    如何查看描述符/签名

    可以使用jdk提供的javap -s A.class 命令,-s输出内部类型签名。A.class为class的全路径。

    为什么JNI中突然多出了一个概念叫"签名"?

    出处:https://www.jianshu.com/p/b71aeb4ed13d
    为什么JNI中突然多出了一个概念叫"签名"?
    因为Java是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持那,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——"签名",即将参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。

    1.4 头文件的生成/书写和规则

    头文件一般用Android Studio自动生成。
    手动的话会经过几个步骤:.java->.class->.h

    javac  xxx.java  //生成xxx.class文件
    javah -jni //xxx生成xxx.h
    

    JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。
    函数的名称是Java_Java程序的package路径_函数名组成的。
    生成的头文件名字格式一般为[包名]_[类名].h,
    函数的名称默认一般为Java_[包名]_[类名]_函数名组成,但并不一定。

    例如/android/os路径下的MessageQueue.java对应
    /framework/base/core/jni/目录下的android_os_MessageQueue.h,这种是Java_[包名]_[类名]_函数名

    /* Gets the native object associated with a MessageQueue. */
    extern sp<MessageQueue> android_os_MessageQueue_getMessageQueue(
            JNIEnv* env, jobject messageQueueObj);
    
    } 
    

    但是android_util_Binder.h中的函数却不是这种命名规则。/android/os路径下的Binder.java所对应的native文件:android_util_Binder.h

    namespace android {
    
    // Converstion to/from Java IBinder Object and C++ IBinder instance.
    extern jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val);
    extern sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj);
    
    extern jobject newParcelFileDescriptor(JNIEnv* env, jobject fileDesc);
    
    extern void set_dalvik_blockguard_policy(JNIEnv* env, jint strict_policy);
    
    extern void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
            bool canThrowRemoteException = false, int parcelSize = 0);
    
    // does not take ownership of the exception, aborts if this is an error
    void binder_report_exception(JNIEnv* env, jthrowable excep, const char* msg);
    }
    
    #endif
    

    1.5 JNIEnv和JavaVM

    https://developer.android.google.cn/training/articles/perf-jni?hl=zh_cn#javavm-and-jnienv
    • JavaVM:是指进程虚拟机环境,每个进程有且只有一个JavaVM实例
    • JNIEnv:是指线程上下文环境,每个线程有且只有一个JNIEnv实例
      先大致有个印象,后面会用到更详细地分析。

    2. Java和JNI交互

    2.1 函数的注册

    在Linux平台下so库分为动态库和静态库。表现形式以.so为后缀动态库和.a为后缀的静态库。
    在动态库里函数注册分为2种:静态注册和动态注册。

    2.1.1 静态注册

    在Java层中添加System.loadLibrary()native函数。
    Java和JNI对应函数关系为:
    JNI方法名是Java_[包名]_[类名]_方法名Java类中的.全用_替换。

    // JNIMethodDynamic.java
    package com.bj.gxz.jniapp;
    public class JNIMethodDynamic {
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("native-lib");
        }
        /**
         * A native method that is implemented by the 'native-lib' native library,
         * which is packaged with this application.
         */
        public native String stringFromJNI();
    
        public native int sum(int x, int y);
    }
    
    // jni_method_dynamic.cpp
    #if 0
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_bj_gxz_jniapp_JNIMethodDynamic_stringFromJNI(
            JNIEnv *env,
            jobject thiz) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_bj_gxz_jniapp_JNIMethodDynamic_sum(JNIEnv *env, jobject thiz, jint x, jint y) {
        return x + y;
    }
    #else
    #endif
    

    然后Java层中直接通过调用JNIMethodDynamic.stringFromJNI即可走到JNI层中。

    2.1.2 动态注册

    动态注册,也就是通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而无需遵循特定的方法命名格式。

    // 对类clazz注意nMethods个方法,方法说明在methods中。成功返回0,出错时返回负数。
    jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
    
    typedef struct {
      char *name;       // native方法名
      char *signature;  // 函数签名
      void *fnPtr;      // C/C++中的函数指针
    };
    
    // fnPtr有如下定义
    // ReturnType (*fnPtr)(JNIEnv *env, jobject objectOrClass, ...);
    
    
    // 清理对类clazz进行的注册的Native方法
    jint UnregisterNatives(JNIEnv *env, jclass clazz);
    
    //MainActivity.java
        static {
            System.loadLibrary("native-lib");
        }
        public native String stringFromJNI();
    
        public native int func(int x);
    
    // native-lib.cpp
    #include <jni.h>
    #include <string>
    
    jstring stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    jint func(JNIEnv* env, jobject thiz, jint x){
        return x*x+2*x-3;
    }
    
    JNINativeMethod methods[]={    // 函数映射表
            {"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
            {"func","(I)I",(void*)func}
    };
    
    jint JNI_OnLoad(JavaVM* vm, void* reserved){
        JNIEnv* env=NULL;
        if (vm->GetEnv((void**)&env,JNI_VERSION_1_6) != JNI_OK){
            return JNI_ERR;
        }
    
        // 获取Java的类对象
        jclass clazz=env->FindClass("com/example/dynamicjni/MainActivity");
        if (clazz == NULL){
            return JNI_ERR;
        }
        // 注册函数,参数:Java类,方法数组,注册方法数
        jint result=env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));
        if (result < 0){    // 注册失败会返回一个负值
            return JNI_ERR;
        }
        return JNI_VERSION_1_6;
    }
    

    Java层中的System.loadLibrary()的作用就是调用相应库中的JNI_OnLoad()方法。由于native-lib.cpp中定义了JNI_OnLoad,并且其中调用了RegisterNatives,这个函数的功能就是注册JNI函数。

     //https://android-opengrok.bangnimang.net/android-12.0.0_r3/xref/libnativehelper/include_jni/jni.h?r=ce48736b#974
    struct _JNIEnv {
        const struct JNINativeInterface* functions;
         jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
              jint nMethods)
         { return functions->RegisterNatives(this, clazz, methods, nMethods); }
    }
    

    functions是指向JNINativeInterface结构体指针,也就是将调用下面方法:

    //https://android-opengrok.bangnimang.net/android-12.0.0_r3/xref/libnativehelper/include_jni/jni.h?r=ce48736b#149
      typedef _JNIEnv JNIEnv;
      typedef _JavaVM JavaVM;
    
      struct JNINativeInterface {
    
      ....
         jint        (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
                              jint);
      ...
    }
    
      struct _JNIEnv {
          /* do not rename this; it does not seem to be entirely opaque */
          const struct JNINativeInterface* functions;
      
      #if defined(__cplusplus)
      
          jint GetVersion()
          { return functions->GetVersion(this); }
      
          jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
              jsize bufLen)
          { return functions->DefineClass(this, name, loader, buf, bufLen); }
      
          jclass FindClass(const char* name)
          { return functions->FindClass(this, name); }
      ...
    }
      /*
       * C++ version.
       */
      struct _JavaVM {
          const struct JNIInvokeInterface* functions;
      
      #if defined(__cplusplus)
          jint DestroyJavaVM()
          { return functions->DestroyJavaVM(this); }
          jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
          { return functions->AttachCurrentThread(this, p_env, thr_args); }
          jint DetachCurrentThread()
          { return functions->DetachCurrentThread(this); }
          jint GetEnv(void** env, jint version)
          { return functions->GetEnv(this, env, version); }
          jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
          { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
      #endif /*__cplusplus*/
      };
    

    注册就到这里了。

    2.1.3 优缺点对比

    静态注册:

    优点
    实现简单,易于理解
    缺点
    必须遵循某些规则
    JNI方法名过长
    运行时根据函数名查找对应的JNI函数,程序效率不高

    动态注册

    优点
    通过函数映射表来查找对应的JNI方法,运行效率高
    不需要遵循命名规则,灵活性更好
    缺点
    实现起来相对复杂
    容易搞错方法签名导致注册失败

    2.2 Java调用JNI

    2.2.1 传递基本的数据类型到JNI层

    当JNI注册完成后,调用Java层中的使用native声明的函数后,会调用JNI层中对应的函数,例如

    //Java 层
    public native void data(byte b, char c, boolean bool, short s, int i, float f, double d, long l, float[] floats);
    
    //JNI层
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_data_JNIData_data(JNIEnv *env, jobject thiz, jbyte b, jchar c,
                                             jboolean j_bool,
                                             jshort s, jint i, jfloat f, jdouble d, jlong l,
                                             jfloatArray floats) {
        LOG_D("byte=%d", b);
        LOG_D("jchar=%c", c);
        LOG_D("jboolean=%d", j_bool);
        LOG_D("jshort=%d", s);
        LOG_D("jint=%d", i);
        LOG_D("jfloat=%f", f);
        LOG_D("jdouble=%lf", d);
        LOG_D("jlong=%lld", l);
    
        jfloat *float_p = env->GetFloatArrayElements(floats, nullptr);
        jsize size = env->GetArrayLength(floats);
        for (int index = 0; index < size; index++) {
            LOG_D("floats[%d]=%lf", index, *(float_p++));
        }
        env->ReleaseFloatArrayElements(floats, float_p, 0);
    }
    
    

    Java中的基本数据类型,在JNI中,存在对应的定义,直接传递即可。

    2.2.2 传递复杂的数据类型到JNI层

    现存在一个自定义类Student,需要传递到JNI层。要怎么做?

    package com.feixun.jni;
     
    public class Student
    {
        private int age ;
        private String name ;
        //构造函数,什么都不做
        public Student(){ }
        
        public Student(int age ,String name){
            this.age = age ;
            this.name = name ;
        }
        
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public void setName(String name){
            this.name = name;
        }
        
        public String toString(){
            return "name --- >" + name + "  age --->" + age ;
        }
    }
    

    Java层中声明native,JNI层中添加对应函数

    //xxx.java  
    public class HelloJni {
        ...
        //在Native层打印Student的信息
        public native void  printStuInfoAtNative(Student stu);
        ... 
    }
    
     
    /*
     * Class:     com_feixun_jni_HelloJni
     * Method:    printStuInfoAtNative
     * Signature: (Lcom/feixun/jni/Student;)V
     */
    //在Native层输出Student的信息
    JNIEXPORT void JNICALL Java_com_feixun_jni_HelloJni_printStuInfoAtNative
      (JNIEnv * env, jobject obj,  jobject obj_stu) //第二个类实例引用代表Student类,即我们传递下来的对象
    {
        jclass stu_cls = env->GetObjectClass(obj_stu); //获得Student类引用
    }
    
    

    复杂对象是通过jobject传递的。

    2.2.3 如何在JNI层获取传递过来的数据

    当 通过调用JNIEXPORT void JNICALL Java_com_feixun_jni_HelloJni_printStuInfoAtNative (JNIEnv * env, jobject obj, jobject obj_stu),使用jobject传递了Student以后,可以通过调用env->GetObjectClass(obj_stu);,获得一个Student对象。

    出处:https://www.jianshu.com/p/b71aeb4ed13d
    为了能够在C/C++中调用Java中的类,jni.h的头文件专门定义了jclass类型表示JavaClass类。JNIEnv中有3个函数可以获取jclass

    • jclass FindClass(const char* clsName):
      通过类的名称(类的全名,这时候包名不是用'"."点号而是用"/"来区分的)来获取jclass。比如:jclass jcl_string=env->FindClass("java/lang/String");
    • jclass GetObjectClass(jobject obj):
      通过对象实例来获取jclass,相当于Java中的getClass()函数
    • jclass getSuperClass(jclass obj):
      通过jclass可以获取其父类的jclass对象
      如果需要在JNI层保存,那就在JNI层定义一个struct。
      可参考这篇:Android JNI 传递对象

    2.3 JNI调用Java

    2.3.1 如何创建Java层的任意对象

    常用的JNI中创建对象的方法如下:

    jobject NewObject(jclass clazz, jmethodID methodID, ...)
    

    比如有我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法

    jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
    obj = (*env)->NewObject(env, cls, mid);
    
    2.3.2 如何调用Java类的成员方法/属性,静态方法/属性

    出处:https://www.jianshu.com/p/b71aeb4ed13d
    在Native本地代码中访问Java层的代码,一个常用的常见的场景就是获取Java类的属性和方法。所以为了在C/C++获取Java层的属性和方法,JNIjni.h头文件中定义了jfieldIDjmethodID这两种类型来分别代表Java端的属性和方法。在访问或者设置Java某个属性的时候,首先就要现在本地代码中取得代表该Java类的属性的jfieldID,然后才能在本地代码中进行Java属性的操作,同样,在需要调用Java类的某个方法时,也是需要取得代表该方法的jmethodID才能进行Java方法操作。

    GetFieldID/GetMethodID:获取某个属性/某个方法
    GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法
    
    jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
    

    jclass上面也说了代表Java层中的"类",name则代表方法名或者属性名。那最后一个char *sig代表什么?它其实代表了JNI中的一个特殊字段——签名。

    获取后的简单使用,如下所示。

        // 获取java的class
        jclass cls = env->FindClass("com/bj/gxz/jniapp/methodfield/AppInfo");
    
        // 创建java对象,就是调用构造方法,构造方法的方法签名固定为<init>
        jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
        jobject obj = env->NewObject(cls, mid, env->NewStringUTF("com.gxz.com"));
    
        // 给定方法名字和签名,调用方法
        jmethodID setVersionCode_mid = env->GetMethodID(cls, "setVersionCode", "(I)V");
        env->CallVoidMethod(obj, setVersionCode_mid, 1);
    
        // 给定属性名字和签名,设置属性的值
        jfieldID size_field_id = env->GetFieldID(cls, "size", "J");
        env->SetLongField(obj, size_field_id, (jlong) 1000);
    
    2.3.3 回调

    当我们处理一个密集型计算数据(比如音视频的软编解码处理,bitmap的特效处理等),这时候就需要用c/c++实现。当在c/c++处理完后需要异步回调/通知到java中。
    有2种情况的回调:JNI非子线程中回调到Java 和 JNI子线程回调到Java 层。子线程回调到Java 层的情况放到后面说。
    首先,定义一个Java回调接口。

    //INativeListener.java
    public interface INativeListener {
        void onCall();
    }
    public native void nativeCallBack(INativeListener callBack);
    

    JNI中定义对应函数,调用onCall

    // jni_thread_callback.cpp
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeCallBack(JNIEnv *env, jobject thiz,
                                                               jobject call_back) {
        // 获取java中的对象
        jclass cls = env->GetObjectClass(call_back);
        // 获取回调方法的id
        jmethodID mid = env->GetMethodID(cls, "onCall", "()V");
        // 调用java中的方法
        env->CallVoidMethod(call_back, mid);
    }
    

    2.4 JNI多线程

    使用JNI多线程,有2个关键函数:AttachCurrentThreadDetachCurrentThread
    官网doc地址
    Attaching to the VM
    JNI接口指针(JNIEnv)仅在当前线程中有效。
    如果另一个线程需要访问jvm,它必须首先调用AttachCurrentThread()将自己附加到 JVM并获取JNI接口指针。
    一旦连接到JVM上,本地线程(jni线程)的工作方式与在本地方法中运行的普通Java线程一样。
    本机线程在调用DetachCurrentThread()来分离它自己之前一直连接到VM。

    附加的线程应该有足够的堆栈空间来执行合理数量的任务。
    每个线程的堆栈空间分配是取决于操作系统。
    例如,使用pthreads,可以在pthread_attr_t参数中为pthread_create指定堆栈大小。

    而在调用JavaVM中的AttachCurrentThread和DetachCurrentThread我们需要拿到JavaVM *vm指针。怎么拿到这个呢?一种是调用JNI_CreateJavaVM加载并初始化Java虚拟机,并返回指向JNI接口指针的指针。我们可以用另外一种jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)全局变量保存一下vm即可。

    2.4.1 如何在JNI子线程中回调到Java层

    简单使用回调实现数据回传到Java:在jni中创建一个线程实现一个写入随机字符串到文件(用来模拟线程任务的耗时),然后写入完成后给java层一个回调告诉java层写入成功。
    定义Java回调接口和JNI函数。

    // INativeThreadListener.java
    public interface INativeThreadListener {
        void onSuccess(String msg);
    }
    public native void nativeInThreadCallBack(INativeThreadListener listener);
    
    //xxx.cpp
    JavaVM *gvm;
    jobject gCallBackObj;
    jmethodID gCallBackMid;
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_bj_gxz_jniapp_cb_JNIThreadCallBack_nativeInThreadCallBack(JNIEnv *env, jobject thiz,
                                                                       jobject call_back) {
        // 创建一个jni中的全局引用
        gCallBackObj = env->NewGlobalRef(call_back);
        jclass cls = env->GetObjectClass(call_back);
        gCallBackMid = env->GetMethodID(cls, "onSuccess", "(Ljava/lang/String;)V");
        // 创建一个线程
        pthread_t pthread;
        jint ret = pthread_create(&pthread, nullptr, writeFile, nullptr);
        LOG_D("pthread_create ret=%d", ret);
    }
    

    这里简单说一下线程的几个参数

     pthread_create
     参数1 pthread_t* pthread 线程句柄
     参数2  pthread_attr_t const* 线程的一些属性
     参数3 void* (*__start_routine)(void*) 线程具体执行的函数
     参数4 void* 传给线程的参数
     返回值 int  0 创建成功
    

    然后在writeFile函数中合适的位置上添加AttachCurrentThreadDetachCurrentThread

    /**
     * 相当于java中线程的run方法
     * @return
     */
    void *writeFile(void *args) {
        // 随机字符串写入
        FILE *file;
        if ((file = fopen("/sdcard/thread_cb", "a+")) == nullptr) {
            LOG_E("fopen filed");
            return nullptr;
        }
        for (int i = 0; i < 10; ++i) {
            fprintf(file, "test %d\n", i);
        }
        fflush(file);
        fclose(file);
        LOG_D("file write done");
    
        // https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/invocation.html
        JNIEnv *env = nullptr;
        // 将当前线程添加到Java虚拟机上,返回一个属于当前线程的JNIEnv指针env
        if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
            jstring jstr = env->NewStringUTF("write success");
            // 回调到java层
            env->CallVoidMethod(gCallBackObj, gCallBackMid, jstr);
            // 删除jni中全局引用
            env->DeleteGlobalRef(gCallBackObj);
            // 从Java虚拟机上分离当前线程
            gvm->DetachCurrentThread();
        }
        return nullptr;
    }
    

    注意:这里把传入的call_back变成全局引用,具体原因后面分析引用的时候会说明。

    2.4.2 线程的创建销毁等待

    主要是使用pthread去操作。

    // 创建线程
    pthread_t pthread;
    pthread_create(&pthread, NULL, threadFunc, (void *) "");
    //等待线程
    int retvalue;
    pthread_join(pthread,(void**)&retvalue);
    if(retvalue!=0){
        LOGD("thread error occurred");
    }
    //退出线程  pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。
    pthread_exit()
     
    
    2.4.3 JNI中如何保证线程安全

    可参考这篇:【JNI编程】JNI中进行线程同步

    if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
    
         ... /* error handling */
    
     }
    
     ...  /* synchronized block */
    
     if ((*env)->MonitorExit(env, obj) != JNI_OK) {
    
         ... /* error handling */
    
     };
    
    

    JAVA来进行同步要比在JNI Native上方便的多,所以,尽量用JAVA来做同步,把与同步相关的代码都挪到JAVA中去。

    2.5 熟悉JNI常见方法

    可通读这篇,有需要的时候查找。
    Android JNI学习(四)——JNI的常用方法的中文API

    3.引用

    JNI中如果需要返回字符串的话,不能直接返回String,而需要创建一个jstring对象:

    std::string hello = "hello world";
    jstring jstr = env->NewStringUTF(hello.c_str());
    

    那问题就来了,这个jstr是我们用env去new出来的。那我们需要手动去delete吗,不delete会不会造成内存泄露?
    如果需要的话,当我们需要将这个jstr返回给java层使用的时候又要怎么办呢?不delete就内存泄露,delete就野指针:

    extern "C" JNIEXPORT jstring JNICALL
    Java_me_linjw_ndkdemo_MainActivity_stringFromJNI(
            JNIEnv *env,
            jobject thiz/* this */) {
        std::string hello = "hello world";
        jstring jstr = env->NewStringUTF(hello.c_str());
        return jstr;
    }
    

    JNI为了解决这个问题,设计了三种引用类型:

    • 局部引用
    • 全局引用
    • 弱全局引用

    3.1 局部引用

    出处:https://www.jianshu.com/p/787053d11dfd
    这里通过NewStringUTF创建的jstring就是局部引用,那它有什么特点呢?
    我们在c层大多数调用jni方法创建的引用都是局部引用,它会别存放在一张局部引用表里。它的内存有四种释放方式:
    1.程序员可以手动调用DeleteLocalRef去释放
    2.c层方法执行完成返回java层的时候,jvm会遍历局部引用表去释放
    3.使用PushLocalFrame/PopLocalFrame创建/销毁局部引用栈帧的时候,在PopLocalFrame里会释放帧内创建的引用
    4.如果使用AttachCurrentThread附加原生线程,在调用DetachCurrentThread的时候会释放该线程创建的局部引用
    所以上面的问题我们就能回答了, jstr可以不用手动delete,可以等方法结束的时候jvm自己去释放(当然如果返回之后在java层将这个引用保存了起来,那也是不会立马释放内存的)
    所以上面的问题我们就能回答了, jstr可以不用手动delete,可以等方法结束的时候jvm自己去释放(当然如果返回之后在java层将这个引用保存了起来,那也是不会立马释放内存的)
    但是这样是否就意味着我们可以任性的去new对象,不用考虑任何东西呢?其实也不是,局部引用表是有大小限制的,如果new的内存太多的话可能造成局部引用表的内存溢出,例如我们在for循环里面不断创建对象:

    std::string hello = "hello world";
    for(int i = 0 ; i < 9999999 ; i ++) {
        env->NewStringUTF(hello.c_str());
    }
    

    所以在使用完之后一定记得调用DeleteLocalRef去释放它。

    局部引用栈帧
    如上面所说我们可能在某个函数中创建了局部引用,然后这个函数在循环中被调用,就容易出现溢出。
    但是如果方法里面创建了多个局部引用,在return之前一个个去释放会显得十分繁琐:

    void func(JNIEnv *env) {
        ...
        jstring jstr1 = env->NewStringUTF(str1.c_str());
        jstring jstr2 = env->NewStringUTF(str2.c_str());
        jstring jstr3 = env->NewStringUTF(str3.c_str());
        jstring jstr4 = env->NewStringUTF(str4.c_str());
        ...
        env->DeleteLocalRef(jstr1);
        env->DeleteLocalRef(jstr2);
        env->DeleteLocalRef(jstr3);
        env->DeleteLocalRef(jstr4);
    }
    

    这个时候可以考虑使用局部引用栈帧:

    void func(JNIEnv *env) {
        env->PushLocalFrame(4);
        ...
        jstring jstr1 = env->NewStringUTF(str1.c_str());
        jstring jstr2 = env->NewStringUTF(str2.c_str());
        jstring jstr3 = env->NewStringUTF(str3.c_str());
        jstring jstr4 = env->NewStringUTF(str4.c_str());
        ...
        env->PopLocalFrame(NULL);
    }
    

    我们在方法开头PushLocalFrame,结尾PopLocalFrame,这样整个方法就在一个局部引用帧里面,而在PopLocalFrame就会将该帧里面创建的局部引用全部释放。
    如果需要将某个局部引用当初返回值返回怎么办?用局部引用帧会不会造成野指针?
    其实jni也考虑到了这中情况,所以PopLocalFrame有一个参数:
    jobject PopLocalFrame(jobject result)
    这个result参数可以传入你的返回值引用,这样的话这个局部引用就会在去到父帧里面,这样就能直接返回了:

    jstring func(JNIEnv *env) {
        env->PushLocalFrame(4);
        ...
        jstring jstr1 = env->NewStringUTF(str1.c_str());
        jstring jstr2 = env->NewStringUTF(str2.c_str());
        jstring jstr3 = env->NewStringUTF(str3.c_str());
        jstring jstr4 = env->NewStringUTF(str4.c_str());
        ...
        return (jstring)env->PopLocalFrame(jstr4);
    }
    

    多线程下的局部引用
    使用JNIEnv这个数据结构去调用JNI的方法创建局部引用,但是JNIEnv将用于线程本地存储,所以我们不能在线程之间共享它。
    如果是Java层创建的线程,那调到c层会自然传入一个JNIEnv指针。
    假设现在在c层中新建了一个线程A,线程A默认是没有JNIEnv的,因此我们需要使用JavaVM,拿到这个线程A的JNIEnv
    理论上每个进程可以有多个JavaVM,但Android只允许有一个,所以JavaVM是可以在多线程间共享的。

    https://www.cnblogs.com/mazhimazhi/p/15528565.html
    在Java层使用System.loadLibrary方法加载so的时候,c层的JNI_OnLoad方法会被调用,我们可以在拿到JavaVM指针并将它保存起来:
    JavaVM* g_Vm;
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        g_Vm = vm;
        return JNI_VERSION_1_4;
    }
    

    之后可以在线程中使用它的AttachCurrentThread方法附加原生线程,然后在线程结束的时候使用DetachCurrentThread去解除附加:

    pthread_t g_pthread;
    JavaVM* g_vm;
    
    void* ThreadRun(void *data) {
        JNIEnv* env;
        g_vm->AttachCurrentThread(&env, nullptr);
        ...
        g_vm->DetachCurrentThread();
    }
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        g_vm = vm;
        return JNI_VERSION_1_4;
    }
    
    ...
    
    pthread_create(&g_pthread, NULL, ThreadRun, (void *) 1);
    

    调用AttachCurrentThread函数后就会返回一个属于当前线程的JNIEnv指针。

    所以在AttachCurrentThreadDetachCurrentThread之间JNIEnv都是有效的,我们可以使用它去创建局部引用,而在DetachCurrentThread之后JNIEnv就失效了,同时我们用它创建的局部引用也会被回收。

    3.2 全局引用

    下面来看一种 错误 的使用全局引用的写法。这里直接将传入的jobject保存到全局变量。

    jobject g_listener;
    
    extern "C" JNIEXPORT void JNICALL
    Java_me_linjw_ndkdemo_MainActivity_registerListener(
            JNIEnv *env,
            jobject thiz,
            jobject listener) {
        g_listener = listener; // 错误的做法!!!
    }
    

    原因是这里传进来的jobject其实也是局部引用,而局部引用是不能跨线程使用的。我们应该将它转换成全局引用去保存,这里通过调用NewGlobalRef把局部引用转换成全局引用。

    jobject g_listener;
    
    extern "C" JNIEXPORT void JNICALL
    Java_me_linjw_ndkdemo_MainActivity_registerListener(
            JNIEnv *env,
            jobject thiz,
            jobject listener) {
        g_listener = env->NewGlobalRef(listener);
    }
    

    然后这样又出现了个问题,按道理这个g_listener和listener应该指向的是同一个java对象,但是如果我们这样去判断的话是错误的:

    if(g_listener == listener) {
        ...
    }
    

    它们的值是不会相等的,如果要判断两个jobject是否指向同一个java对象要需要用IsSameObject去判断:

    if(env->IsSameObject(g_listener, listener)) {
        ...   
    }
    

    然后在适当的实际调用DeleteGlobalRef

     // 释放g_listener全局引用
    env->DeleteGlobalRef(g_listener);
    

    3.3 弱全局引用

    弱全局引用和全局引用类似,可以在跨线程使用,它使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。

    jobject g_listener;
    
    extern "C" JNIEXPORT void JNICALL
    Java_me_linjw_ndkdemo_MainActivity_registerListener(
            JNIEnv *env,
            jobject thiz,
            jobject listener) {
        g_listener = env->NewGlobalWeakRef(listener);
    }
    

    弱全局引用在内存不足的时候会被JVM回收,可以通过调用env->IsSameObject(g_listener, NULL)判断是否为null。JNI中的NULL引用指向JVM中的null对象。

    if(!env->IsSameObject(g_listener, NULL)) {
              env->DeleteWeakGlobalRef(g_listener);
    }
    

    3.4 三种引用的区别和使用场景

    局部引用 指向的JVM内部空间会在本地方法返回的之后被销毁,因此不能跨方法和线程

    全局引用 可以跨方法和线程进行访问,必须手动释放。通过NewGlobalRef创建,DeleteGlobalRef释放。

    弱全局引用 和全局引用类似,可以在跨方法和线程使用,它使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。但是弱全局引用是会被gc回收,所以在使用的时候我们需要先判断它是否已经被回收。

    3.5 缓存

    出处:https://www.jianshu.com/p/cffcb01fd457
    缓存策略:
    当我们在本地代码方法中通过FindClass查找Class、GetMethodID查找方法、GetFieldID获取类的字段ID和GetFieldValue获取字段的时候是需要jvm来做很多工作的,可能这个字段ID或者方法是在超类中继承而来的,那jvm可能还需要层次遍历。而这些负责和jni交互java中的类的全路径,字段,方法一般是不会修改了,是固定的。这也是为什么我们在做android混淆打包的时候需要keep这些类,因为这些一般不会变,不能变,变了后jni中会找不到了具体的类,字段,方法了。既然打包后不会变我们是可以进行缓存策略来处理。
    另外至于效率提高多少,没有验证,不过不重要,如果是频繁这种查找一般会采用缓存,只查找一次或者在程序初始化的时候提前查找。
    对于这类情况的缓存分为基本数据类型缓存和引用缓存。
    基本数据类型缓存
    基本数据类型的缓存在c,c++中可以借助关键字static处理。
    引用类型的缓存
    可以借助上面的全局引用或者弱全局引用,弱全局引用记得在使用前判断下是否被回收了IsSameObject,最后记得释放 DeleteGlobalRef ,DeleteWeakGlobalRef。
    局部引用可以加static吗?不用全局引用/全局弱应用? 可以加static,但是不能起到缓存的作用。因为上文说了局部引用在函数结束后会被jvm回收了,不然再次使用回到非法内存访问导致应用crash,所以正确的做法如上用全局引用/全局弱应用。

    3.6 内存回收机制

    出处:https://blog.csdn.net/tabactivity/article/details/106902540
    局部引用
    JNI 函数内部创建的 jobject 对象及其子类( jclass 、 jstring 、 jarray 等) 对象都是局部引用,它们在 JNI 函数返回后无效;
    一般情况下,我们应该依赖 JVM 去自动释放 JNI 局部引用;但下面两种情况必须手动调用 DeleteLocalRef() 去释放:
    1.(在循环体或回调函数中)创建大量 JNI 局部引用,即使它们并不会被同时使用,因为 JVM 需要足够的空间去跟踪所有的 JNI 引用,所以可能会造成内存溢出或者栈溢出;
    2.如果对一个大的 Java 对象创建了 JNI 局部引用,也必须在使用完后手动释放该引用,否则 GC 迟迟无法回收该 Java 对象也会引发内存泄漏.
    全局引用
    全局引用允许你持有一个 JNI 对象更长的时间,直到你手动销毁;但需要显式调用 NewGlobalRef()DeleteGlobalRef()
    弱全局引用
    弱全局引用类似 Java 中的弱引用,它允许对应的 Java 对象被 GC 回收;
    类似地,创建和释放也是通过NewWeakGlobalRef()DeleteWeakGlobalRef()
    调用 IsSameObject(env, jobj, NULL)可以判断该弱全局引用指向的 Java对象是否已被 GC回收。

    参考链接:
    Android-JNI开发系列
    Android NDK开发——静态注册和动态注册
    JNI 动态注册
    JNI开发之方法签名与Java通信(二)
    Android JNI原理分析
    第39篇-Java通过JNI调用C/C++函数
    第40篇-JNIEnv和JavaVM
    JNI内存管理
    Jni多线程与类加载
    Android-JNI开发系列《五》局部引用&全局引用&全局弱引用
    JNI 引用, DeleteLocalRef使用场景详解
    Android JNI学习(三)——Java与Native相互调用
    JNI学习积累之三 ---- 操作JNI函数以及复杂对象传递
    Android-JNI开发系列《二》在jni层的线程中回调到java层
    JNI(五) pthread子线程操作
    【多线程编程学习笔记4】终止线程执行的3种方法(pthread_exit()、pthread_cancel()、return)

    相关文章

      网友评论

          本文标题:JNI简单总结

          本文链接:https://www.haomeiwen.com/subject/smwknrtx.html