寫在開頭:本文參考了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簡介

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ā)。

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。

下面列出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

- 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類型表示Java中Class類。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層的屬性和方法,JNI在jni.h頭文件中定義了jfieldID和jmethodID這兩種類型來分別代表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ù):AttachCurrentThread和DetachCurrentThread
官網(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ù)中合適的位置上添加AttachCurrentThread和DetachCurrentThread
/**
* 相當(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是可以在多線程間共享的。

在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指針。
所以在AttachCurrentThread和DetachCurrentThread之間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)