JNI簡單總結(jié)

寫在開頭:本文參考了Android-JNI開發(fā)系列這個系列的大綱去做總結(jié)。
本文將從3個方面去簡單總結(jié)下JNI。

1. 基礎(chǔ)知識

  • 1.1 JNI簡介
  • 1.2 Java基礎(chǔ)數(shù)據(jù)類型和引用類型分別和JNI的對應(yīng)關(guān)系
  • 1.3 方法簽名
  • 1.4 頭文件的生成/書寫和規(guī)則
  • 1.5 JNIEnv和JavaVM

2. Java和JNI交互

  • 2.1 函數(shù)的注冊
  • 2.1.1 靜態(tài)注冊
  • 2.1.2 動態(tài)注冊
  • 2.1.3 優(yōu)缺點對比
  • 2.2 Java調(diào)用JNI
    • 2.2.1 傳遞基本的數(shù)據(jù)類型到JNI層
    • 2.2.2 傳遞復(fù)雜的數(shù)據(jù)類型到JNI層
    • 2.2.3 如何在JNI層獲取傳遞過來的數(shù)據(jù)
  • 2.3 JNI調(diào)用Java
    • 2.3.1 如何創(chuàng)建Java層的任意對象
    • 2.3.2 如何調(diào)用Java類的成員方法/屬性,靜態(tài)方法/屬性
    • 2.3.3 回調(diào)
  • 2.4 JNI多線程
    • 2.4.1 如何在JNI子線程中回調(diào)到Java層
    • 2.4.2 線程的創(chuàng)建銷毀等待
    • 2.4.3 JNI中如何保證線程安全
  • 2.5 熟悉JNI常見方法

3.引用

  • 3.1 局部引用
  • 3.2 全局引用
  • 3.3 弱全局引用
  • 3.4 三種引用的區(qū)別和使用場景
  • 3.5 緩存
  • 3.6 內(nèi)存回收機制

1. 基礎(chǔ)知識

1.1 JNI簡介

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

JNI 是指 Java 原生接口,JNI是JAVA語言自己的特性,也就是說JNI和Android沒有關(guān)系。

為什么需要JNI

有些事情Java無法處理時,JNI允許程序員用其他編程語言來解決,例如,Java標(biāo)準(zhǔn)庫不支持的平臺相關(guān)功能或者程序庫。也用于改造已存在的用其它語言寫的程序,供Java程序調(diào)用。許多基于JNI的標(biāo)準(zhǔn)庫提供了很多功能給程序員使用,例如文件I/O、音頻相關(guān)的功能。當(dāng)然,也有各種高性能的程序,以及平臺相關(guān)的API實現(xiàn),允許所有Java應(yīng)用程序安全并且平臺獨立地使用這些功能。

這里順帶提一下NDK,Android中使用NDK這個工具進(jìn)行JNI開發(fā)。


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

1.2 Java基礎(chǔ)數(shù)據(jù)類型和引用類型分別和JNI的對應(yīng)關(guān)系

基本數(shù)據(jù)類型Java與Native映射關(guān)系如下表所示:

Java JNI中的別名 C/C++中的類型 字節(jié)數(shù)
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

引用數(shù)據(jù)類型
外面的為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的引用類型代表了繼承關(guān)系,jbooleanArray繼承jarray,jarray繼承jobject,最終都繼承jobject。


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

下面列出Java和其對應(yīng)的JNI函數(shù)。

//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函數(shù)名
    const char* signature; //Java函數(shù)簽名,記錄參數(shù)類型和個數(shù),以及返回值類型
    void*       fnPtr; //Native層對應(yīng)的函數(shù)指針
} JNINativeMethod;

JNINativeMethod結(jié)構(gòu)體中有一個signature(簽名),這個就是方法簽名。Method結(jié)構(gòu)體中的signature這個char字符。

我們平時定義的int,float,String等類型在JVM虛擬機中,存儲數(shù)據(jù)類型的名稱時是使用描述符來存儲。
基本數(shù)據(jù)類型對應(yīng)的描述符:

類型描述符 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

數(shù)組數(shù)據(jù)類型是在前面添加[

類型描述符 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

復(fù)雜數(shù)據(jù)類型:L+classname +;
classname規(guī)則是:類全名(包名+類名)將原來的.分隔符換成/ 分隔符

類型描述符 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方法簽名格式:(輸入?yún)?shù)...)返回值參數(shù)

Java函數(shù) 對應(yīng)的簽名
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輸出內(nèi)部類型簽名。A.class為class的全路徑。

為什么JNI中突然多出了一個概念叫"簽名"?

出處:http://www.itdecent.cn/p/b71aeb4ed13d
為什么JNI中突然多出了一個概念叫"簽名"?
因為Java是支持函數(shù)重載的,也就是說,可以定義相同方法名,但是不同參數(shù)的方法,然后Java根據(jù)其不同的參數(shù),找到其對應(yīng)的實現(xiàn)的方法。這樣是很好,所以說JNI肯定要支持的,那JNI要怎么支持那,如果僅僅是根據(jù)函數(shù)名,沒有辦法找到重載的函數(shù)的,所以為了解決這個問題,JNI就衍生了一個概念——"簽名",即將參數(shù)類型和返回值類型的組合。如果擁有一個該函數(shù)的簽名信息和這個函數(shù)的函數(shù)名,我們就可以順序的找到對應(yīng)的Java層中的函數(shù)了。

1.4 頭文件的生成/書寫和規(guī)則

頭文件一般用Android Studio自動生成。
手動的話會經(jīng)過幾個步驟:.java->.class->.h

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

JNIEXPORT和JNICALL都是JNI的關(guān)鍵字,表示此函數(shù)是要被JNI調(diào)用的。
函數(shù)的名稱是Java_Java程序的package路徑_函數(shù)名組成的。
生成的頭文件名字格式一般為[包名]_[類名].h,
函數(shù)的名稱默認(rèn)一般為Java_[包名]_[類名]_函數(shù)名組成,但并不一定。

例如/android/os路徑下的MessageQueue.java對應(yīng)
/framework/base/core/jni/目錄下的android_os_MessageQueue.h,這種是Java_[包名]_[類名]_函數(shù)名。

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

} 

但是android_util_Binder.h中的函數(shù)卻不是這種命名規(guī)則。/android/os路徑下的Binder.java所對應(yīng)的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:是指進(jìn)程虛擬機環(huán)境,每個進(jìn)程有且只有一個JavaVM實例
  • JNIEnv:是指線程上下文環(huán)境,每個線程有且只有一個JNIEnv實例
    先大致有個印象,后面會用到更詳細(xì)地分析。

2. Java和JNI交互

2.1 函數(shù)的注冊

在Linux平臺下so庫分為動態(tài)庫和靜態(tài)庫。表現(xiàn)形式以.so為后綴動態(tài)庫和.a為后綴的靜態(tài)庫。
在動態(tài)庫里函數(shù)注冊分為2種:靜態(tài)注冊和動態(tài)注冊。

2.1.1 靜態(tài)注冊

在Java層中添加System.loadLibrary()native函數(shù)。
Java和JNI對應(yīng)函數(shù)關(guān)系為:
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層中直接通過調(diào)用JNIMethodDynamic.stringFromJNI即可走到JNI層中。

2.1.2 動態(tài)注冊

動態(tài)注冊,也就是通過RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而無需遵循特定的方法命名格式。

// 對類clazz注意nMethods個方法,方法說明在methods中。成功返回0,出錯時返回負(fù)數(shù)。
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);

typedef struct {
  char *name;       // native方法名
  char *signature;  // 函數(shù)簽名
  void *fnPtr;      // C/C++中的函數(shù)指針
};

// fnPtr有如下定義
// ReturnType (*fnPtr)(JNIEnv *env, jobject objectOrClass, ...);


// 清理對類clazz進(jìn)行的注冊的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[]={    // 函數(shù)映射表
        {"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;
    }
    // 注冊函數(shù),參數(shù):Java類,方法數(shù)組,注冊方法數(shù)
    jint result=env->RegisterNatives(clazz,methods,sizeof(methods)/sizeof(methods[0]));
    if (result < 0){    // 注冊失敗會返回一個負(fù)值
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}

Java層中的System.loadLibrary()的作用就是調(diào)用相應(yīng)庫中的JNI_OnLoad()方法。由于native-lib.cpp中定義了JNI_OnLoad,并且其中調(diào)用了RegisterNatives,這個函數(shù)的功能就是注冊JNI函數(shù)。

 //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結(jié)構(gòu)體指針,也就是將調(diào)用下面方法:

//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 優(yōu)缺點對比

靜態(tài)注冊:

優(yōu)點
實現(xiàn)簡單,易于理解
缺點
必須遵循某些規(guī)則
JNI方法名過長
運行時根據(jù)函數(shù)名查找對應(yīng)的JNI函數(shù),程序效率不高

動態(tài)注冊

優(yōu)點
通過函數(shù)映射表來查找對應(yīng)的JNI方法,運行效率高
不需要遵循命名規(guī)則,靈活性更好
缺點
實現(xiàn)起來相對復(fù)雜
容易搞錯方法簽名導(dǎo)致注冊失敗

2.2 Java調(diào)用JNI

2.2.1 傳遞基本的數(shù)據(jù)類型到JNI層

當(dāng)JNI注冊完成后,調(diào)用Java層中的使用native聲明的函數(shù)后,會調(diào)用JNI層中對應(yīng)的函數(shù),例如

//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中的基本數(shù)據(jù)類型,在JNI中,存在對應(yīng)的定義,直接傳遞即可。

2.2.2 傳遞復(fù)雜的數(shù)據(jù)類型到JNI層

現(xiàn)存在一個自定義類Student,需要傳遞到JNI層。要怎么做?

package com.feixun.jni;
 
public class Student
{
    private int age ;
    private String name ;
    //構(gòu)造函數(shù),什么都不做
    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層中添加對應(yīng)函數(shù)

//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類引用
}

復(fù)雜對象是通過jobject傳遞的。

2.2.3 如何在JNI層獲取傳遞過來的數(shù)據(jù)

當(dāng) 通過調(diào)用JNIEXPORT void JNICALL Java_com_feixun_jni_HelloJni_printStuInfoAtNative (JNIEnv * env, jobject obj, jobject obj_stu),使用jobject傳遞了Student以后,可以通過調(diào)用env->GetObjectClass(obj_stu);,獲得一個Student對象。

出處:http://www.itdecent.cn/p/b71aeb4ed13d
為了能夠在C/C++中調(diào)用Java中的類,jni.h的頭文件專門定義了jclass類型表示JavaClass類。JNIEnv中有3個函數(shù)可以獲取jclass。

  • jclass FindClass(const char* clsName):
    通過類的名稱(類的全名,這時候包名不是用'"."點號而是用"/"來區(qū)分的)來獲取jclass。比如:jclass jcl_string=env->FindClass("java/lang/String");
  • jclass GetObjectClass(jobject obj):
    通過對象實例來獲取jclass,相當(dāng)于Java中的getClass()函數(shù)
  • jclass getSuperClass(jclass obj):
    通過jclass可以獲取其父類的jclass對象
    如果需要在JNI層保存,那就在JNI層定義一個struct。
    可參考這篇:Android JNI 傳遞對象

2.3 JNI調(diào)用Java

2.3.1 如何創(chuàng)建Java層的任意對象

常用的JNI中創(chuàng)建對象的方法如下:

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

比如有我們知道Java類中可能有多個構(gòu)造函數(shù),當(dāng)我們要指定調(diào)用某個構(gòu)造函數(shù)的時候,會調(diào)用下面這個方法

jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);
2.3.2 如何調(diào)用Java類的成員方法/屬性,靜態(tài)方法/屬性

出處:http://www.itdecent.cn/p/b71aeb4ed13d
在Native本地代碼中訪問Java層的代碼,一個常用的常見的場景就是獲取Java類的屬性和方法。所以為了在C/C++獲取Java層的屬性和方法,JNIjni.h頭文件中定義了jfieldIDjmethodID這兩種類型來分別代表Java端的屬性和方法。在訪問或者設(shè)置Java某個屬性的時候,首先就要現(xiàn)在本地代碼中取得代表該Java類的屬性的jfieldID,然后才能在本地代碼中進(jìn)行Java屬性的操作,同樣,在需要調(diào)用Java類的某個方法時,也是需要取得代表該方法的jmethodID才能進(jìn)行Java方法操作。

GetFieldID/GetMethodID:獲取某個屬性/某個方法
GetStaticFieldID/GetStaticMethodID:獲取某個靜態(tài)屬性/靜態(tài)方法

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");

    // 創(chuàng)建java對象,就是調(diào)用構(gòu)造方法,構(gòu)造方法的方法簽名固定為<init>
    jmethodID mid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;)V");
    jobject obj = env->NewObject(cls, mid, env->NewStringUTF("com.gxz.com"));

    // 給定方法名字和簽名,調(diào)用方法
    jmethodID setVersionCode_mid = env->GetMethodID(cls, "setVersionCode", "(I)V");
    env->CallVoidMethod(obj, setVersionCode_mid, 1);

    // 給定屬性名字和簽名,設(shè)置屬性的值
    jfieldID size_field_id = env->GetFieldID(cls, "size", "J");
    env->SetLongField(obj, size_field_id, (jlong) 1000);
2.3.3 回調(diào)

當(dāng)我們處理一個密集型計算數(shù)據(jù)(比如音視頻的軟編解碼處理,bitmap的特效處理等),這時候就需要用c/c++實現(xiàn)。當(dāng)在c/c++處理完后需要異步回調(diào)/通知到j(luò)ava中。
有2種情況的回調(diào):JNI非子線程中回調(diào)到Java 和 JNI子線程回調(diào)到Java 層。子線程回調(diào)到Java 層的情況放到后面說。
首先,定義一個Java回調(diào)接口。

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

JNI中定義對應(yīng)函數(shù),調(diào)用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);
    // 獲取回調(diào)方法的id
    jmethodID mid = env->GetMethodID(cls, "onCall", "()V");
    // 調(diào)用java中的方法
    env->CallVoidMethod(call_back, mid);
}

2.4 JNI多線程

使用JNI多線程,有2個關(guān)鍵函數(shù):AttachCurrentThreadDetachCurrentThread
官網(wǎng)doc地址
Attaching to the VM
JNI接口指針(JNIEnv)僅在當(dāng)前線程中有效。
如果另一個線程需要訪問jvm,它必須首先調(diào)用AttachCurrentThread()將自己附加到 JVM并獲取JNI接口指針。
一旦連接到JVM上,本地線程(jni線程)的工作方式與在本地方法中運行的普通Java線程一樣。
本機線程在調(diào)用DetachCurrentThread()來分離它自己之前一直連接到VM。

附加的線程應(yīng)該有足夠的堆??臻g來執(zhí)行合理數(shù)量的任務(wù)。
每個線程的堆??臻g分配是取決于操作系統(tǒng)。
例如,使用pthreads,可以在pthread_attr_t參數(shù)中為pthread_create指定堆棧大小。

而在調(diào)用JavaVM中的AttachCurrentThread和DetachCurrentThread我們需要拿到JavaVM *vm指針。怎么拿到這個呢?一種是調(diào)用JNI_CreateJavaVM加載并初始化Java虛擬機,并返回指向JNI接口指針的指針。我們可以用另外一種jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)全局變量保存一下vm即可。

2.4.1 如何在JNI子線程中回調(diào)到Java層

簡單使用回調(diào)實現(xiàn)數(shù)據(jù)回傳到Java:在jni中創(chuàng)建一個線程實現(xiàn)一個寫入隨機字符串到文件(用來模擬線程任務(wù)的耗時),然后寫入完成后給java層一個回調(diào)告訴java層寫入成功。
定義Java回調(diào)接口和JNI函數(shù)。

// 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) {
    // 創(chuàng)建一個jni中的全局引用
    gCallBackObj = env->NewGlobalRef(call_back);
    jclass cls = env->GetObjectClass(call_back);
    gCallBackMid = env->GetMethodID(cls, "onSuccess", "(Ljava/lang/String;)V");
    // 創(chuàng)建一個線程
    pthread_t pthread;
    jint ret = pthread_create(&pthread, nullptr, writeFile, nullptr);
    LOG_D("pthread_create ret=%d", ret);
}

這里簡單說一下線程的幾個參數(shù)

 pthread_create
 參數(shù)1 pthread_t* pthread 線程句柄
 參數(shù)2  pthread_attr_t const* 線程的一些屬性
 參數(shù)3 void* (*__start_routine)(void*) 線程具體執(zhí)行的函數(shù)
 參數(shù)4 void* 傳給線程的參數(shù)
 返回值 int  0 創(chuàng)建成功

然后在writeFile函數(shù)中合適的位置上添加AttachCurrentThreadDetachCurrentThread

/**
 * 相當(dāng)于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;
    // 將當(dāng)前線程添加到Java虛擬機上,返回一個屬于當(dāng)前線程的JNIEnv指針env
    if (gvm->AttachCurrentThread(&env, nullptr) == 0) {
        jstring jstr = env->NewStringUTF("write success");
        // 回調(diào)到j(luò)ava層
        env->CallVoidMethod(gCallBackObj, gCallBackMid, jstr);
        // 刪除jni中全局引用
        env->DeleteGlobalRef(gCallBackObj);
        // 從Java虛擬機上分離當(dāng)前線程
        gvm->DetachCurrentThread();
    }
    return nullptr;
}

注意:這里把傳入的call_back變成全局引用,具體原因后面分析引用的時候會說明。

2.4.2 線程的創(chuàng)建銷毀等待

主要是使用pthread去操作。

// 創(chuàng)建線程
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() 函數(shù)不能返回一個指向局部數(shù)據(jù)的指針,否則很可能使程序運行結(jié)果出錯甚至崩潰。
pthread_exit()
 
2.4.3 JNI中如何保證線程安全

可參考這篇:【JNI編程】JNI中進(jìn)行線程同步

if ((*env)->MonitorEnter(env, obj) != JNI_OK) {

     ... /* error handling */

 }

 ...  /* synchronized block */

 if ((*env)->MonitorExit(env, obj) != JNI_OK) {

     ... /* error handling */

 };

JAVA來進(jìn)行同步要比在JNI Native上方便的多,所以,盡量用JAVA來做同步,把與同步相關(guān)的代碼都挪到JAVA中去。

2.5 熟悉JNI常見方法

可通讀這篇,有需要的時候查找。
Android JNI學(xué)習(xí)(四)——JNI的常用方法的中文API

3.引用

JNI中如果需要返回字符串的話,不能直接返回String,而需要創(chuàng)建一個jstring對象:

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

那問題就來了,這個jstr是我們用env去new出來的。那我們需要手動去delete嗎,不delete會不會造成內(nèi)存泄露?
如果需要的話,當(dāng)我們需要將這個jstr返回給java層使用的時候又要怎么辦呢?不delete就內(nèi)存泄露,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為了解決這個問題,設(shè)計了三種引用類型:

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

3.1 局部引用

出處:http://www.itdecent.cn/p/787053d11dfd
這里通過NewStringUTF創(chuàng)建的jstring就是局部引用,那它有什么特點呢?
我們在c層大多數(shù)調(diào)用jni方法創(chuàng)建的引用都是局部引用,它會別存放在一張局部引用表里。它的內(nèi)存有四種釋放方式:
1.程序員可以手動調(diào)用DeleteLocalRef去釋放
2.c層方法執(zhí)行完成返回java層的時候,jvm會遍歷局部引用表去釋放
3.使用PushLocalFrame/PopLocalFrame創(chuàng)建/銷毀局部引用棧幀的時候,在PopLocalFrame里會釋放幀內(nèi)創(chuàng)建的引用
4.如果使用AttachCurrentThread附加原生線程,在調(diào)用DetachCurrentThread的時候會釋放該線程創(chuàng)建的局部引用
所以上面的問題我們就能回答了, jstr可以不用手動delete,可以等方法結(jié)束的時候jvm自己去釋放(當(dāng)然如果返回之后在java層將這個引用保存了起來,那也是不會立馬釋放內(nèi)存的)
所以上面的問題我們就能回答了, jstr可以不用手動delete,可以等方法結(jié)束的時候jvm自己去釋放(當(dāng)然如果返回之后在java層將這個引用保存了起來,那也是不會立馬釋放內(nèi)存的)
但是這樣是否就意味著我們可以任性的去new對象,不用考慮任何東西呢?其實也不是,局部引用表是有大小限制的,如果new的內(nèi)存太多的話可能造成局部引用表的內(nèi)存溢出,例如我們在for循環(huán)里面不斷創(chuàng)建對象:

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

所以在使用完之后一定記得調(diào)用DeleteLocalRef去釋放它。

局部引用棧幀
如上面所說我們可能在某個函數(shù)中創(chuàng)建了局部引用,然后這個函數(shù)在循環(huán)中被調(diào)用,就容易出現(xiàn)溢出。
但是如果方法里面創(chuàng)建了多個局部引用,在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,結(jié)尾PopLocalFrame,這樣整個方法就在一個局部引用幀里面,而在PopLocalFrame就會將該幀里面創(chuàng)建的局部引用全部釋放。
如果需要將某個局部引用當(dāng)初返回值返回怎么辦?用局部引用幀會不會造成野指針?
其實jni也考慮到了這中情況,所以PopLocalFrame有一個參數(shù):
jobject PopLocalFrame(jobject result)
這個result參數(shù)可以傳入你的返回值引用,這樣的話這個局部引用就會在去到父幀里面,這樣就能直接返回了:

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這個數(shù)據(jù)結(jié)構(gòu)去調(diào)用JNI的方法創(chuàng)建局部引用,但是JNIEnv將用于線程本地存儲,所以我們不能在線程之間共享它。
如果是Java層創(chuàng)建的線程,那調(diào)到c層會自然傳入一個JNIEnv指針。
假設(shè)現(xiàn)在在c層中新建了一個線程A,線程A默認(rèn)是沒有JNIEnv的,因此我們需要使用JavaVM,拿到這個線程A的JNIEnv。
理論上每個進(jìn)程可以有多個JavaVM,但Android只允許有一個,所以JavaVM是可以在多線程間共享的。

https://www.cnblogs.com/mazhimazhi/p/15528565.html

在Java層使用System.loadLibrary方法加載so的時候,c層的JNI_OnLoad方法會被調(diào)用,我們可以在拿到JavaVM指針并將它保存起來:

JavaVM* g_Vm;

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    g_Vm = vm;
    return JNI_VERSION_1_4;
}

之后可以在線程中使用它的AttachCurrentThread方法附加原生線程,然后在線程結(jié)束的時候使用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);

調(diào)用AttachCurrentThread函數(shù)后就會返回一個屬于當(dāng)前線程的JNIEnv指針。

所以在AttachCurrentThreadDetachCurrentThread之間JNIEnv都是有效的,我們可以使用它去創(chuàng)建局部引用,而在DetachCurrentThread之后JNIEnv就失效了,同時我們用它創(chuàng)建的局部引用也會被回收。

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; // 錯誤的做法!!!
}

原因是這里傳進(jìn)來的jobject其實也是局部引用,而局部引用是不能跨線程使用的。我們應(yīng)該將它轉(zhuǎn)換成全局引用去保存,這里通過調(diào)用NewGlobalRef把局部引用轉(zhuǎn)換成全局引用。

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);
}

然后這樣又出現(xiàn)了個問題,按道理這個g_listener和listener應(yīng)該指向的是同一個java對象,但是如果我們這樣去判斷的話是錯誤的:

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

它們的值是不會相等的,如果要判斷兩個jobject是否指向同一個java對象要需要用IsSameObject去判斷:

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

然后在適當(dāng)?shù)膶嶋H調(diào)用DeleteGlobalRef

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

3.3 弱全局引用

弱全局引用和全局引用類似,可以在跨線程使用,它使用NewGlobalWeakRef創(chuàng)建,使用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);
}

弱全局引用在內(nèi)存不足的時候會被JVM回收,可以通過調(diào)用env->IsSameObject(g_listener, NULL)判斷是否為null。JNI中的NULL引用指向JVM中的null對象。

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

3.4 三種引用的區(qū)別和使用場景

局部引用 指向的JVM內(nèi)部空間會在本地方法返回的之后被銷毀,因此不能跨方法和線程。

全局引用 可以跨方法和線程進(jìn)行訪問,必須手動釋放。通過NewGlobalRef創(chuàng)建,DeleteGlobalRef釋放。

弱全局引用 和全局引用類似,可以在跨方法和線程使用,它使用NewGlobalWeakRef創(chuàng)建,使用DeleteGlobalWeakRef釋放。但是弱全局引用是會被gc回收,所以在使用的時候我們需要先判斷它是否已經(jīng)被回收。

3.5 緩存

出處:http://www.itdecent.cn/p/cffcb01fd457
緩存策略:
當(dāng)我們在本地代碼方法中通過FindClass查找Class、GetMethodID查找方法、GetFieldID獲取類的字段ID和GetFieldValue獲取字段的時候是需要jvm來做很多工作的,可能這個字段ID或者方法是在超類中繼承而來的,那jvm可能還需要層次遍歷。而這些負(fù)責(zé)和jni交互java中的類的全路徑,字段,方法一般是不會修改了,是固定的。這也是為什么我們在做android混淆打包的時候需要keep這些類,因為這些一般不會變,不能變,變了后jni中會找不到了具體的類,字段,方法了。既然打包后不會變我們是可以進(jìn)行緩存策略來處理。
另外至于效率提高多少,沒有驗證,不過不重要,如果是頻繁這種查找一般會采用緩存,只查找一次或者在程序初始化的時候提前查找。
對于這類情況的緩存分為基本數(shù)據(jù)類型緩存和引用緩存。
基本數(shù)據(jù)類型緩存
基本數(shù)據(jù)類型的緩存在c,c++中可以借助關(guān)鍵字static處理。
引用類型的緩存
可以借助上面的全局引用或者弱全局引用,弱全局引用記得在使用前判斷下是否被回收了IsSameObject,最后記得釋放 DeleteGlobalRef ,DeleteWeakGlobalRef。
局部引用可以加static嗎?不用全局引用/全局弱應(yīng)用? 可以加static,但是不能起到緩存的作用。因為上文說了局部引用在函數(shù)結(jié)束后會被jvm回收了,不然再次使用回到非法內(nèi)存訪問導(dǎo)致應(yīng)用crash,所以正確的做法如上用全局引用/全局弱應(yīng)用。

3.6 內(nèi)存回收機制

出處:https://blog.csdn.net/tabactivity/article/details/106902540
局部引用
JNI 函數(shù)內(nèi)部創(chuàng)建的 jobject 對象及其子類( jclass 、 jstring 、 jarray 等) 對象都是局部引用,它們在 JNI 函數(shù)返回后無效;
一般情況下,我們應(yīng)該依賴 JVM 去自動釋放 JNI 局部引用;但下面兩種情況必須手動調(diào)用 DeleteLocalRef() 去釋放:
1.(在循環(huán)體或回調(diào)函數(shù)中)創(chuàng)建大量 JNI 局部引用,即使它們并不會被同時使用,因為 JVM 需要足夠的空間去跟蹤所有的 JNI 引用,所以可能會造成內(nèi)存溢出或者棧溢出;
2.如果對一個大的 Java 對象創(chuàng)建了 JNI 局部引用,也必須在使用完后手動釋放該引用,否則 GC 遲遲無法回收該 Java 對象也會引發(fā)內(nèi)存泄漏.
全局引用
全局引用允許你持有一個 JNI 對象更長的時間,直到你手動銷毀;但需要顯式調(diào)用 NewGlobalRef()DeleteGlobalRef()
弱全局引用
弱全局引用類似 Java 中的弱引用,它允許對應(yīng)的 Java 對象被 GC 回收;
類似地,創(chuàng)建和釋放也是通過NewWeakGlobalRef()DeleteWeakGlobalRef()
調(diào)用 IsSameObject(env, jobj, NULL)可以判斷該弱全局引用指向的 Java對象是否已被 GC回收。

參考鏈接:
Android-JNI開發(fā)系列
Android NDK開發(fā)——靜態(tài)注冊和動態(tài)注冊
JNI 動態(tài)注冊
JNI開發(fā)之方法簽名與Java通信(二)
Android JNI原理分析
第39篇-Java通過JNI調(diào)用C/C++函數(shù)
第40篇-JNIEnv和JavaVM
JNI內(nèi)存管理
Jni多線程與類加載
Android-JNI開發(fā)系列《五》局部引用&全局引用&全局弱引用
JNI 引用, DeleteLocalRef使用場景詳解
Android JNI學(xué)習(xí)(三)——Java與Native相互調(diào)用
JNI學(xué)習(xí)積累之三 ---- 操作JNI函數(shù)以及復(fù)雜對象傳遞
Android-JNI開發(fā)系列《二》在jni層的線程中回調(diào)到j(luò)ava層
JNI(五) pthread子線程操作
【多線程編程學(xué)習(xí)筆記4】終止線程執(zhí)行的3種方法(pthread_exit()、pthread_cancel()、return)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容