深入理解JNI

5 篇文章 0 订阅

概述

JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作

java层使用

1. 加载jni库
static {
	try {
		System.loadLibrary(“demo_jni”);
	} catch (SecurityException ex) {
		Log.e(TAG,System.loadLibrary SecurityException+ ex);
	} catch (NullPointerException ex) {
		Log.e(TAG,System.loadLibrary NullPointerException+ ex);
	} catch (UnsatisfiedLinkError ex) {
		Log.e(TAG,System.loadLibrary UnsatisfiedLinkError+ ex);
	}
}

demo_jni是jni库的名字,jni二进制库为libdemo_jni.so, jni库名字并没有特定模板,android中一般写为libxxx_jni.so
系统会根据不同平台拓展成真实的动态库文件名,在linux平台系统上会拓展成libdemo_jni.so,在windows平台上回拓展成demo_jni.dll。

2. 声明native函数
private native void native_test();

jni层使用

1. jni函数注册
static void com_example_Demo_test(JNIEnv *env, jobject thiz) {
	ALOGD("to call native method");
}

java层声明的native函数与jni层函数如何对应呢,有两种方式
(1)静态方法
这种方法是根据函数名来找对应的函数
native_test位于com.example.Demo中,native_test的全路径为com.example.Demo.native_test, 而jni函数名为com_example_Demo_test, 当java层调用native_test时,会在对应的demo_jni库中寻找com_example_Demo_test,找到将native_test与其建立关联,即保存jni函数的函数指针, 以后再调用就可以直接使用该函数指针
(2)动态注册
Android中用一个结构来表示java native函数与jni函数的对应关系,并在其中描述了函数的参数和返回值。就是JNINativeMethod

typedef struct {
	const char* name; // java函数名
	const char* signature; // 用字符串是描述了函数的参数和返回值
	void* fnPtr; // jni函数对应的函数指针
} JNINativeMethod;
static void com_example_test(JNIEnv *env, jobject thiz) {
	printf("to call native method");
}

static JNINativeMethod sMethods[] = {
{“native_test”,()V”, (void )com_example_test}
};

// 注册native函数
static int register_jni(JNIEnv *env) {
	return jniRegisterNativeMethods(env, JAVA_CLASSNAME_HEALTHMONITOR, 
	sMethods, NELEM(sMethods));
}

// java层调用System.loadLibrary就会加载JNI_OnLoad函数,动态注册就在该函数中完成,如果不需要动态注册可以不重写该函数,会使用默认的实现
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
	JNIEnv env = NULL;
	jint result = -1;

	if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
	    goto bail;
	}
	assert(env != NULL);

	if (register_jni(env) < 0) {
	    goto bail;
	}

	/* success -- return valid version number */
	result = JNI_VERSION_1_4;
	
	bail:
	return result;
}
2. java和jni数据类型转换
2.1 基本数据类型转换

在这里插入图片描述

2.2 引用数据类型转换

在这里插入图片描述

3. JNIEnv

JNIEnv是与线程相关的代表jni环境的结构体
在这里插入图片描述
JNIEnv实际上提供了一些jni系统函数,来实现调用java函数, 获取jObject等功能
JNIEnv是线程相关的,也就是不同线程之间的JNIEnv不可以互相调用,JNIEnv一般由java层调用传入,但是当jni层反调用java层函数时,如何获取正确的JNIEnv呢?
这就需要用到JavaVM,也就是之前JNI_OnLoad函数的第一个参数, 它是虚拟机在JNI层的代表,也就是说不管多少个线程, JavaVM是独一份的,可以通过JavaVM获取JNIEnv

static JNIEnv *get_env() {
	if (mJvm == NULL) return NULL;
	JNIEnv *env = NULL;
	
	int status = mJvm->GetEnv((void**) &env, JNI_VERSION_1_4);
	if (status == JNI_EDETACHED || env == NULL) {
	    status = mJvm->AttachCurrentThread(&env, NULL);
	    if (status < 0) {
	        env = NULL;
	    }
	}
	return env;
}

// 在后台线程退出时,调用DetachCurrentThread释放对应的资源
(*mJvm)->DetachCurrentThread(*mJvm);
4.通过JNIEnv操作java层
4.1 获取java层成员函数

jni层用 jmethodID表示java层成员函数
java层成员函数

public void TestCallBack(int a, int b) {
	// to do something ...
}
public static void TestCallBack1(int a, int b) {
	// to do something ...
}

jni层获取java层成员函数

  • 调用静态函数:
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
	JNIEnv env = NULL;
	jint result = -1;

	if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
	    goto bail;
	}
	assert(env != NULL);
	// 通过FindClass获取的jclass 并不是一个实例,只能回调静态方法
	// 回调非静态方法需要通过jclass clazz = env->GetObjectClass(obj)获取jclass,
	jclass tmpClazz = env->FindClass("com.example.test.Test");
	// 直接使用tmpClazz 会导致crash-->jni error (app bug): accessed stale local reference
	jclass clazz = (jclass)env->NewGlobalRef(tmpClazz);
	// 调用GetMethodID 获取 jmethodID 
	jmethodID callbackMethod = env->GetStaticMethodID(clazz , “TestCallBack1”,(II)V”);
	// 使用jmethodID 
	env->CallStaticVoidMethod(clazz, callbackMethod, 1, 1);
	....
}
  • 获取非静态函数:
static void com_example_test(JNIEnv *env, jobject thiz)  { 
     jclass clazz = env->GetObjectClass(thiz); 

     jmethodID callbackMethod = env->GetMethodID(clazz , “TestCallBack”,(II)V”);

	env->CallVoidMethod(thiz, callbackMethod, 1, 1);
 } 
4.2 获取java层成员变量

java层成员变量:

private int s; 
private int s1;

jni层获取java层成员函数:

  • 获取静态字段:
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) {
	JNIEnv env = NULL;
	jint result = -1;

	if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
	    goto bail;
	}
	assert(env != NULL);
	// 通过FindClass获取的jclass 并不是一个实例,只能回调静态方法
	// 回调非静态方法需要通过jclass clazz = env->GetObjectClass(obj)获取jclass,
	jclass tmpClazz = env->FindClass("com.example.test.Test");
	// 直接使用tmpClazz 会导致crash-->jni error (app bug): accessed stale local reference
	jclass clazz = (jclass)env->NewGlobalRef(tmpClazz);
	// 调用GetFieldID获取 jfieldID 
	jfieldID field = env->GetStaticFieldID(clazz , “s1”, “I”);
	// 使用jfieldID 
	jint si = env->GetStaticIntField(clazz , field); 
	....
}
  • 获取实例字段:
static void com_example_test(JNIEnv *env, jobject thiz)  { 
     jclass clazz = env->GetObjectClass(thiz); 
     // 调用GetFieldID获取 jfieldID 
	jfieldID field = env->GetFieldID(clazz , “s”, “I”);
	// 使用jfieldID 
	jint si = env->GetIntField(thiz, field); 
 } 
5. jstring

Java中的String 也是引用类型,不过由于它的使用频率有点高,所以再JNI 规范中,单独创建了一个 jstring 类型表示java中的String类型,虽然jstring 是一种独立的数据类型,但是它并没有提供成员函数以便操作,而C++中的string 类有自己的成员哈数,操作jstring的过程,是保存在那个JNIEnv中。

  • c++ string 转 jstring
string str = "test";
jstring jsr =  env->NewStringUTF(str.c_str());
  • jstring转 string
    方法一:
const char* chardata = env->GetStringUTFChars(jstr, 0);
string s(chardata);

方法二:

static std::string JavaStringToString(JNIEnv* env, jstring str) {
	if (env == nullptr || str == nullptr) {
		return NULL;
	}
	const jchar* chars = env->GetStringChars(str, NULL);
	if (chars == nullptr) {
		return NULL;
	}
	std::string u8_string = UTF16StringToUTF8String(reinterpret_cast<const char16_t*>(chars), env->GetStringLength(str));
	env->ReleaseStringChars(str, chars);
	return u8_string;
}
6. jni类型签名

之前动态注册时:

{“native_test”,()V”, (void )com_example_test}
}

“()V”就是jni函数签名,格式为(参数1类型签名参数二类型签名…)返回值类型签名
各种数据类型签名详见:
https://blog.csdn.net/amin_hui/article/details/115558987

7. 垃圾回收

java中创建的对象最后是由垃圾回收器来回收和释放内存的,JNI技术提供了三种类型的引用,对应不同的垃圾回收方法
1)Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层创建的jobject。Local Reference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。

virtual bool scanFile (const char* path,long long lastModified,long long fileSize)
{
    jstring pathStr;
    ...
    for(int i=0;i<100;i++){
          jstring pathStr = mEnv->NewStringUTF(path);
          .....
          // mEnv->DeleteLocalRef(pathStr);//不立即释放Local Ref
    }
    //  上面如果不立即释放Local Reference ,则会创建100个jstring,那么内存消耗就很可观了!!!
   ...
}

2)Global Reference:全局引用。这种对象如不主动释放,它永远不会被垃圾回收。

// android_media_MediaScanner.cpp::MyMediaScannerCilent的构造函数
MyMediaScannerClient(JNIEnv *env,jobject client):mEnv(env),
   // 调用 NewGlobalRef创建一个Global Reference ,这样mClient就不用担心被回收了
   mClient(env->NewGlobalRef(client)),mScannerFileMethodID(0),
    mHandleStringTagMethodID(0),
    mSetMimeTypeMethodID(0)
{
  ........
}

// 析构函数
 Virtual -MyMediaScannerClient()
{
   mEnv->DeleteGolbalRef(mClient);// 释放全局引用

}

3)Weak Global Reference:弱全局引用。一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了

jclass class_test_weak_global;

extern "C"
JNIEXPORT void JNICALL
com_examplejniWeakGlobalReferenceTest(JNIEnv *env, jobject instance) {
//如果 class_test_weak_global对象被回收 , 返回 true ; 没有被回收返回 false ;
    jboolean isClassReleased = env->IsSameObject(class_test_weak_global, NULL);
    if( class_test_weak_global== NULL || isClassReleased ) {
        //生成局部引用 , 该局部引用使用完毕后可释放
        jclass tmp_class = env->FindClass("com.example.test.Test");
        //将上述生成的局部引用变成弱全局引用
        //      弱全局引用释放时 , env->DeleteWeakGlobalRef(class_test_weak_global) 即可释放下面转换的弱全局引用
        class_test_weak_global= static_cast<jclass>(env->NewWeakGlobalRef(tmp_class));
        //将局部引用释放掉
        env->DeleteLocalRef(tmp_class);
    }
8. jni异常处理

JNI没有像Java一样有try…catch…final这样的异常处理机制,面且在本地代码中调用某个JNI接口时如果发生了异常,后续的本地代码不会立即停止执行,而会继续往下执行后面的代码

JNI层函数可以在代码中截获和修改这些异常,
1)ExceptionOccurred函数/ExceptionCheck函数,用来判断是否发生异常;

ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE
ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL

ExceptionCheck:

//异常捕获 ,检查JNI调用是否有异常
if(env->ExceptionCheck()){
    env->ExceptionDescribe();
    env->ExceptionClear();//清除引发的异常,在Java层不会打印异常堆栈信息,如果不清除,后面的调用ThrowNew抛出的异常堆栈信息会
//覆盖前面的异常信息
    jclass cls_exception = env->FindClass("java/lang/Exception");
    env->ThrowNew(cls_exception,"call java static method ndk error");
    return;
}

ExceptionOccurred:

if(env->ExceptionOccurred()){
    env->ExceptionDescribe();
    env->ExceptionClear();
    jclass cls_exception = env->FindClass("java/lang/Exception");
    env->ThrowNew(cls_exception,"call java static method ndk error");
    return;
}

2)ExceptionClear函数,用来清理当前JNI层中发生的异常;

3)ThrowNew函数,用来向java层抛出异常,并自定义输出异常信息

void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
 {
     // 查找异常类
     jclass cls = (*env)->FindClass(env, name);
     /* 如果这个异常类没有找到,VM会抛出一个NowClassDefFoundError异常 */
     if (cls != NULL) {
         (*env)->ThrowNew(env, cls, msg); // 抛出指定名字的异常
     }
     /* 释放局部引用 */
     (*env)->DeleteLocalRef(env, cls);
 }
  1. 异常发生后释放资源
    在异常发生后,释放资源是一件很重要的事情。下面的例子中,调用GetStringChars 函数后,如果后面的代码发生异常,要记得调用 ReleaseStringChars 释放资源
JNIEXPORT void JNICALL Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
 {
     const jchar *cstr = (*env)->GetStringChars(env, jstr);
     if (c_str == NULL) {
         return; 
     }
     ...
     if ((*env)->ExceptionCheck(env)) { /* 异常检查 */
         (*env)->ReleaseStringChars(env, jstr, cstr); // 发生异常后释放前面所分配的内存
         return; 
     }
     ...
     /* 正常返回 */
     (*env)->ReleaseStringChars(env, jstr, cstr);
}
  1. ExceptionDescribe:打印异常的堆栈信息

异常处理详见:
https://zhuanlan.zhihu.com/p/158727336

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值