JNI学习笔记——JNI基础知识(二)

一、缓存策略

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;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值