写在开头:本文参考了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_cnJNI 是指 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
类型表示Java
中Class
类。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
层的属性和方法,JNI
在jni.h
头文件中定义了jfieldID
和jmethodID
这两种类型来分别代表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个关键函数:AttachCurrentThread
和DetachCurrentThread
官网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
函数中合适的位置上添加AttachCurrentThread
和DetachCurrentThread
/**
* 相当于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
是可以在多线程间共享的。
在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
指针。
所以在AttachCurrentThread
和DetachCurrentThread
之间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)
网友评论