一、缓存策略
1、静态局部缓存
在下面的代码中多次调用了native层的set方法。
public class HelloJNI {
//加载动态库
static {
System.load("D:\\programme\\c++\\repos\\JNIHello\\x64\\Debug\\JNIHello.dll");
}
public static int age;
public static void main(String[] args) {
set(12);
System.out.println(age);
set(14);
System.out.println(age);
set(15);
System.out.println(age);
}
//声明一个native方法
public native static void set(int age);
}
JNIEXPORT void JNICALL
Java_HelloJNI_set(JNIEnv * env, jclass jclz, jint age) {
//对应类的class、属性名称、属性的签名
jfieldID filedId = env->GetStaticFieldID(jclz, "age", "I");
//jclass clazz, jfieldID fieldID,jchar value
env->SetStaticIntField(jclz, filedId,age);
}
此时Java_HelloJNI_set方法每次被调用的时候都会获取jfieldID。这样做效率不好,可以使用局部静态变量进行缓存。
JNIEXPORT void JNICALL
Java_HelloJNI_set(JNIEnv * env, jclass jclz, jint age) {
//静态局部缓存
static jfieldID filedId = NULL;
if (filedId == NULL)
{
printf("get filedId");
filedId = env->GetStaticFieldID(jclz, "age", "I");
}
//jclass clazz, jfieldID fieldID,jchar value
env->SetStaticIntField(jclz, filedId,age);
}
备注:在c语言中对于一个变量,只要它被 static 修饰,都会被存储在全局数据区,即使它是局部变量,全局数据区的变量生命周期直到程序的结束(静态局部变量虽然存储在全局数据区,但是它的作用域或者说使用范围仅限于函数内部),不会随着函数调用结束而被销毁。并且只能被初始化一次,之后可以改变它的值,但不能再被初始化,即使有这样的语句,也无效。对于上面的
static jfieldID filedId = NULL;
除去第一次,之后都是无效的代码。
执行Java代码打印如下 可以看到虽然Java_HelloJNI_set方法被调用了多次,但是get filedId只打印了一次。
此时获取jfieldID只在函数第一次执行时获取一次,即使函数执行完成这个变量还会存在内存当中,直到程序结束。下次再执行该函数就可以直接使用,这样就不会在每次的函数调用时查询。
2、全局缓存
在native层可以将所有的id申明为全局的,在加载完动态库之后立即对所有的id进行初始化,之后在其他native函数中使用时就可以直接拿来用了。
public class HelloJNI {
//加载动态库
static {
System.load("D:\\programme\\c++\\repos\\JNIHello\\x64\\Debug\\JNIHello.dll");
initIds();
}
public static String name;
public static int age;
public static void main(String[] args) {
set("zhang1",12);
System.out.println("name="+name+" age="+age);
set("zhang2",12);
System.out.println("name="+name+" age="+age);
set("zhang3",12);
System.out.println("name="+name+" age="+age);
}
//声明一个native方法,用于初始化所有的id
public native static void initIds();
//声明一个native方法
public native static void set(String name,int age);
}
//全局缓存
jfieldID nameFiledId;
jfieldID ageFiledId;
初始化全局变量,动态库加载完成之后,立刻进行缓存。
JNIEXPORT void JNICALL
Java_HelloJNI_initIds(JNIEnv * env, jclass jclz) {
nameFiledId=env->GetStaticFieldID(jclz,"name","Ljava/lang/String;");
ageFiledId = env->GetStaticFieldID(jclz, "age", "I");
}
JNIEXPORT void JNICALL
Java_HelloJNI_set(JNIEnv * env, jclass jclz, jstring name, jint age) {
//直接使用,不需要再去获取
env->SetStaticObjectField(jclz, nameFiledId, name);
env->SetStaticIntField(jclz, ageFiledId,age);
}
让Java在第一次加载这个类的时候首先调用nitive方法初始化所有的jfieldID,jmethodID这样的话,就可以省去多次的确定id是否存在的语句,当然,这些jfieldID,jmethodID是定义在C/C++的全局变量。当Java类卸载或是重新加载的时候,也会重新呼叫该本地代码来重新计算id。
为了提高程序的性能通常会使用缓存的方式来缓存所有的id包括jfieldID、jmethodID。但是需要注意不能在函数中将局部引用(通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass等))通过static变量的方式缓存起来,供下次调用时使用。这种方式是错误的,因为函数返回后局部引用所引用的对象马上就会被释放掉,static 静态变量中存储的就是一个被释放后的内存地址,成为了一个野指针,再次调用时就会造成非法地址的访问,使程序崩溃。
不管是全局缓存还是局部缓存,其实就是让变量存储在全局数据区,对于全局变量可以不加static,它本身就是存储在全局数据区的,对于局部变量通过static的方式使变量存储在全局数据区,提高它的生命周期,这其实都是C语言的基础。
二、引用
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
1、局部引用
首先要明白局部引用时如何产生的?
- 大多数JNI函数会创建局部引用。NewObject、FindClass、GetObjectClass、NewStringUTF、NewCharArray 等等创建的都是局部引用。
- 通过NewLocalRef方法创建的引用。
- 函数调用时传入的jobject
局部引用只在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放,或调用DeleteLocalRef手动释放。
//java中申明的native方法
native void createUser();
//createUser方法在native中的实现
Java_com_example_hellojni_MainActivity_createUser(JNIEnv *env, jobject thiz) {
//userClass是局部引用
jclass userClass=env->FindClass("com/example/hellojni/User");
jmethodID initMethodID=env->GetMethodID(userClass,"<init>","()V");
jmethodID helloMethodID=env->GetMethodID(userClass,"hello","()V");
//user是局部引用
jobject user=env->NewObject(userClass,initMethodID);
env->CallVoidMethod(user,helloMethodID);
}
当本地方法返回后,这些局部引用都会被自动释放,虽然如此但是一个好的习惯是我们调用DeleteLocalRef进行释放。
每一个局部引用都消耗一些JVM资源。 我们需要确保本地方法不会过度分配局部引用。 尽管在本地方法返回后会自动释放局部引用,但局部引用的过多分配可能会导致VM在执行本地方法期间耗尽内存。比如下面这样:
for(int i=0;i<n;i++){
//user是局部引用
jobject user=env->NewObject(userClass,initMethodID);
env->CallVoidMethod(user,helloMethodID);
env->DeleteLocalRef(user);
}
在循环中创建User对象,使用完成之后应该手动调用DeleteLocalRef将局部引用删除,然后user就会被回收。如果不删除的话,只有等到本地方法返回,所有的局部引用才会被释放。而局部引用会阻止它所引用的对象被GC回收,在本地方法返回前,可能由于一直循环而导致内存耗尽。
2、全局引用
调用NewGlobalRef可以基于局部引用创建全局引用。全局引用可以跨方法、跨线程使用,如果不主动释放,就永远不会被垃圾回收,所以一定要注意。不使用了可以使用DeleteGlobalRef来释放全局引用。
//全局引用
jobject user;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_createUser(JNIEnv *env, jobject thiz) {
jclass userClass=env->FindClass("com/example/hellojni/User");
jmethodID initMethodID=env->GetMethodID(userClass,"<init>","()V");
//user是局部引用
jobject localUser=env->NewObject(userClass,initMethodID);
//基于局部引用创建全局引用
user=env->NewGlobalRef(localUser);
//可以释放局部引用,此时有一个全局引用使用User对象
env->DeleteLocalRef(localUser);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_invokeHello(JNIEnv *env, jobject thiz) {
jclass userClass=env->GetObjectClass(user);
jmethodID helloMethodID=env->GetMethodID(userClass,"hello","()V");
//跨方法使用全局引用
env->CallVoidMethod(user,helloMethodID);
env->DeleteLocalRef(userClass);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_releaseGlobalRef(JNIEnv *env, jobject thiz) {
//在合适的时机手动释放全局引用
env->DeleteGlobalRef(user);
}
注意:全局引用只能使用NewGlobalRef方法创建,下面是一个错误的创建方式。
//错误示例
jobject user;
jobject pThis;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_createUser(JNIEnv *env, jobject thiz) {
jclass userClass=env->FindClass("com/example/hellojni/User");
jmethodID initMethodID=env->GetMethodID(userClass,"<init>","()V");
//下面的使用方式都是错误的
pThis=thiz;
user=env->NewObject(userClass,initMethodID);
}
在Java编程中为了让变量的作用域更大,我们可能会这么干,但是在JNI编程中,如果想创建全局的引用只能通过NewGlobalRef。通过上面的错误方式并不会增加对象的引用计数,在其他JNI函数使用这些jobject可能会产生严重的后果,因为他们可能已经被垃圾收集器回收了,此时jobject就是一个野指针。
3、弱全局引用
通过调用NewWeakGlobalRef基于局部引用或全局引用创建弱全局引用。与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 ,当垃圾收集器运行时,如果某个对象仅由弱引用引用,则它将被释放。如果弱全局引用所引用的对象被释放,此时弱全局引用等效于NULL。我们可以通过使用IsSameObject将弱引用与NULL比较来判断其引用的对象是否被释放。
//弱全局引用
jclass userClz;
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_test(JNIEnv *env, jobject thiz) {
if (userClz==NULL||env->IsSameObject(userClz,NULL)){
jclass local=env->FindClass("com/example/hellojni/User");
//基于局部引用创建弱全局引用
userClz= static_cast<jclass>(env->NewWeakGlobalRef(local));
//删除局部引用
env->DeleteLocalRef(local);
}
jmethodID methodId=env->GetStaticMethodID(userClz,"hello","()V");
env->CallStaticVoidMethod(userClz,methodId);
}
在内存紧张时,如果某个对象只被弱全局引用引用着,则它可能会被回收,当然我们也可以手动释放它。
env->DeleteWeakGlobalRef(userClz);
userClz=NULL;
三、静态注册与动态注册
JNI函数注册的目的是为了将Java层的native函数申明和JNI层对应的函数实现关联起来。JAVA层调用JIN函数时,会从对应的JNI文件中查找该函数(根据关联的规则)。
1、静态注册
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
//....
//native函数申明
native void test();
}
//native方法在C/C++中的具体实现
extern "C"
JNIEXPORT void JNICALL
Java_com_example_hellojni_MainActivity_test(JNIEnv *env, jobject thiz) {
//...
}
静态方法就是根据函数名来建立Java函数和JNI函数之间的关联关系的。
在Native中,符号".“有着特殊的意义,所以在JNI层需要把”.“换成”_"。参数和返回值对应的JAVA类型换成了对应的JNI类型。
静态注册具体的查找过程:
当调用Java层native关键字申明的函数时,它会从对应的so库找对应的JNI方法(Java_包名_类名_方法名),如果没有,就会报错。如果找到,虚拟机会为这个Java层native方法和JNI层对应的方法建立一个关联关系(保存JNI层函数的函数指针)。以后再调用native函数时,直接使用这个函数指针就可以了。
静态注册的缺点
- native函数名称特别长,不利于书写
- 当需要更改包名、类名或者方法时, 需要重新生成头文件, 灵活性不高
- 初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,会影响运行效率。
2、动态注册
上面知道在调用Java 层native函数时会去so查找对应的JNI函数,如果找到,虚拟机会为这个Java native方法和JNI层对应的方法建立一个关联关系(保存这个JNI函数的函数指针)。那么能否直接让Java native函数知道JNI层对应函数的函数指针,答案是肯定的,这就是动态注册。
动态注册的原理是在JNI层通过重载JNI_OnLoad()函数来实现,我们在调用 System.loadLibrary加载动态库时,内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。在这个函数中可以做一些初始化的工作,包括提供一个函数映射表动态注册函数。
实现过程:
- 在一个JNINativeMethod数组中保存所有native函数和JNI函数的对应关系
- 在JNI_OnLoad函数中调用JNI提供的RegisterNatives()注册方法进行函数注册
Java类(包含了native函数的申明)
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//...
nativeTest();
nativeHello();
}
public native void nativeTest();
public native void nativeHello();
}
native层实现
#include <jni.h>
#include <string>
#include <android/log.h>
//打印日志用
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"JNI",__VA_ARGS__)
void test(JNIEnv *env, jobject thiz) {
LOGD("test 在native中被调用");
}
void hello(JNIEnv *env, jobject thiz) {
LOGD("hello 在native中被调用");
}
//需要动态注册的方法数组
static const JNINativeMethod mMethods[] = {
{"nativeTest","()V", (void *)test},
{"nativeHello", "()V", (void *)hello}
};
//加载动态库时 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。
jint JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//获得 JniEnv
int r = vm->GetEnv((void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
需要动态注册native方法的类
jclass mainActivityClz = env->FindClass("com/example/hellojni/MainActivity");
//将mainActivityClz类中的native方法(mMethods)进行动态注册。第三个参数是方法的个数
r = env->RegisterNatives(mainActivityClz,
mMethods,
sizeof(mMethods)/ sizeof(JNINativeMethod));
//如果注册失败
if(r != JNI_OK )
{
return -1;
}
//告诉 VM 此 native 组件使用的 JNI 版本。
return JNI_VERSION_1_4;
}
很简单,通过动态注册这种方式就建立了Java中的native函数申明和C/C++中具体实现的关联。
其中JNINativeMethod是一个结构体,定义如下:
typedef struct {
const char* name;//java中申明的native方法名称
const char* signature;;//java中申明的native方法的签名
void* fnPtr;//java中申明的native方法在C/C++中的对应实现,是一个函数指针
} JNINativeMethod;
四、native线程调用Java
//native-lib.cpp
//线程的入口函数
void* runInBackground(void* args){
}
void test(JNIEnv *env, jobject thiz) {
pthread_t pId;
pthread_create(&pId,0,runInBackground,0);
}
这里当test这个本地方法被调用,会创建一个线程,并且在这个线程中执行runInBackground这个函数,在c/c++中创建线程很简单,这里的关键是如何在runInBackground这个native函数中和Java互调,比如创建一个Java对象、回调Java方法。我们知道通过JNIEnv可以让c/c++代码和Java互调,所以现在的问题是,runInBackground这个方法如何获取一个JNIEnv。熟悉C/C++的都知道pthread_create方法可以创建线程,其中通过第四个参数可以给线程入口函数传递参数,所以我们可以将test函数中的env传递给runInBackground函数使用吗?答案是不可以的。理由如下:
JNIEnv 是一个线程相关的结构体,不能跨线程使用
JNIEnv不能跨线程使用,我们可以通过JavaVM 来获取一个和本线程相关的JNIEnv。
JavaVM 是 Java虚拟机在 JNI 层的代表, 是进程唯一的
在JNI_OnLoad函数入参就传递了JavaVM,我们可以将其保存为全局的,以便之后在线程函数中使用。
//线程的入口函数
void* runInBackground(void* args){
JNIEnv * env;
//将当前本地线程附加到jvm,并获得JNIEnv
javaVm->AttachCurrentThread(&env,0);
//拿到JNIEnv后就可以愉快的和Java交互了
//分离
javaVm->DetachCurrentThread();
return 0;
}
例子:在c/c++的runInBackground线程方法中调用MainActivity的showToast弹一个土司
//MainActivity.Java
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
nativeTest();
}
public native void nativeTest();
//在C/C++中回调这个方法
public void showToast(final String msg){
if (Looper.myLooper()==Looper.getMainLooper()){
Toast.makeText(MainActivity.this,msg,Toast.LENGTH_LONG).show();
}else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,msg,Toast.LENGTH_LONG).show();
}
});
}
}
}
C/C++层实现(native-lib.cpp)
#include <jni.h>
#include <string>
#include <pthread.h>
#include <android/log.h>
//打印日志用
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"JNI",__VA_ARGS__)
jobject mainActivity;
JavaVM* javaVm;
//线程的入口函数
void* runInBackground(void* args){
JNIEnv * env;
//调用JavaVM的AttachCurrentThread函数,得到这个线程的JNIEnv结构体
javaVm->AttachCurrentThread(&env,0);
jclass mainActivityCls=env->GetObjectClass(mainActivity);
jmethodID showToastMethodId=env->GetMethodID(mainActivityCls,"showToast","(Ljava/lang/String;)V");
jstring msg=env->NewStringUTF("hello");
//调用MainActivity的showToast方法
env->CallVoidMethod(mainActivity,showToastMethodId,msg);
//释放资源
env->DeleteLocalRef(mainActivityCls);
env->DeleteLocalRef(msg);
env->DeleteGlobalRef(mainActivity);
//释放对应的资源
javaVm->DetachCurrentThread();
return 0;
}
void test(JNIEnv *env, jobject thiz) {
mainActivity=env->NewGlobalRef(thiz);
pthread_t pId;
pthread_create(&pId,0,runInBackground,0);
}
static const JNINativeMethod mMethods[] = {
{"nativeTest","()V", (void *)test},
};
jint JNI_OnLoad(JavaVM* vm, void* reserved){
//将JavaVM保存为全局的
javaVm=vm;
JNIEnv* env = NULL;
int r = vm->GetEnv((void**) &env, JNI_VERSION_1_4);
if( r != JNI_OK){
return -1;
}
jclass mainActivityClz = env->FindClass("com/example/hellojni/MainActivity");
r = env->RegisterNatives(mainActivityClz,
mMethods,
sizeof(mMethods)/ sizeof(JNINativeMethod));
if(r != JNI_OK )
{
return -1;
}
return JNI_VERSION_1_4;
}