JNI简单总结

2022-08-31  本文已影响0人  梧叶已秋声

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

1. 基础知识

2. Java和JNI交互

3.引用

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中的。

上面的层次中的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

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

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)

上一篇下一篇

猜你喜欢

热点阅读