「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
本文章已在公众号【Android开发编程】发表
前言小计
1、jni与ndk的基本知识点前面文章都讲过了,不懂的,可以在公众号首页看;
2、jni中常用的方法比如:类、方法、数组、字符串等前面也讲解过了;
3、这篇文章讲解jni中函数的注册和c++调用java的知识点;
一. JNI函数注册
1、jni函数注解知识点介绍
JNI技术是Java世界与Native世界的通信桥梁,具体到代码,Java层的代码如何同Native层的代码进行调用的呢?我们都知道,在调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.UnsatisfiedLinkError异常 。 那么,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢?
JVM查找native方法有两种方式;
按照JNI规范的命名规则,调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。
第一种方式,可用使用javah工具按照Java类中定义的native方法,按照JNI规范的命名规则的方式自动生成Jni本地C/C++头文件。
第二种方式则需要在本地库的JNI_OnLoad函数中调用RegisterNatives来动态注册。
JNI函数注册是将Java层声明的Native方法同实际的Native函数绑定起来的实现方式,也就是说,只要通过JNI函数注册机制注册了本地方,Java层就可以直接调用定义的这些本地方法了。对应上述JVM查找native方法的两种方式,JNI函数注册方式一般分为静态注册和动态注册两种方式。
2、静态注册
原理:根据函数名来建立 java 方法与 JNI 函数的一一对应关系;
实现流程:
编写 java 代码;
利用 javah 指令生成对应的 .h 文件;
对 .h 中的声明进行实现;
弊端:
编写不方便,JNI 方法名字必须遵循规则且名字很长;
编写过程步骤多,不方便;
程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时;
public class Test {
static {
System.loadLibrary("native-lib");
}
public native String textFromJni();
}
使用javah生成对应的本地方法头文件。
#include <jni.h>
#ifndef _Included_test
#define _Included_test
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_test_Test_textFromJni
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
3、动态注册
对应与上面的静态注册方法,还有一种动态注册JNI函数的方式,即动态注册。动态注册是当Java层调用System.loadLibrary方法加载so库后,本地库的JNI_OnLoad函数会被调用,在JNI_OnLoad函数中通过调用RegisterNatives函数来完成本地方法的注册。
原理:利用 RegisterNatives 方法来注册 java 方法与 JNI 函数的一一对应关系;
实现流程:
利用结构体 JNINativeMethod 数组记录 java 方法与 JNI 函数的对应关系;
实现 JNI_OnLoad 方法,在加载动态库后,执行动态注册;
调用 FindClass 方法,获取 java 对象;
调用 RegisterNatives 方法,传入 java 对象,以及 JNINativeMethod 数组,以及注册数目完成注册;
优点:
流程更加清晰可控;
效率更高;
其中JNINativeMethod结构体用来描述本地方法结构,其定义如下:
typedef struct {
const char* name; // Java方法名
const char* signature; // Java方法签名
void* fnPtr; // jni本地方法对应的函数指针
} JNINativeMethod;
结构体的第一个参数 name 是java 方法名;
第二个参数 signature 用于描述方法的参数与返回值;
第三个参数 fnPtr 是函数指针,指向 jni 函数;
其中,第二个参数 signature 使用字符串记录方法的参数与返回值,具体格式形如“()V”、“(II)V”,其中分为两部分,括号内表示的是参数,括号右侧表示的是返回值;
①、数据类型映射
基本数据类型
②. 数组引用类型
如果是一维数组则遵循下表,如果是二维数组或更高维数组则对应的 native 类型为 jobjectArray,域描述符中使用 ‘[’ 的个数表示维数
③. 对象引用类型
对于其它引用类型,即 java 中的对象,其映射规则为
④. 对象数组引用类型
如果是一维数组则遵循下表,如果是二维数组或更高维数组则对应的 native 类型为 jobjectArray,域描述符中使用 ‘[’ 的个数表示维数
在Java文件中定义本地方法,加载本地so库
package test.jnitest;
public class Test {
static {
System.loadLibrary("native-lib");
}
public native String textFromJni();
}
在JNI_OnLoad函数中注册本地方法
jstring textFromJni(JNIEnv* env, jobject thiz) {
return env->NewStringUTF("text from jni");
}
static JNINativeMethod gMethods[] = {
{"textFromJni", "()Ljava/lang/String;", (void*)textFromJni}
};
int registerMethod(JNIEnv *env) {
jclass test = env->FindClass("cc/ccbu/jnitest/Test");
return env->RegisterNatives(test, gMethods, sizeof(gMethods)/ sizeof(gMethods[0]));
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
if (registerMethod(env) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
注意:
在JNI_OnLoad函数的结尾处,我们一定要有返回值,而且必须是JNI_VERSION_1_4 或 JNI_VERSION_1_6,也就是JNI的版本号,我们一定要返回正确的版本号,否则系统也是无法加载的;
4、c++调用java详解
(1) 找到java对应的Class
(2) 找到要调用的方法的methodID
(3) 在C语言中调用相应方法
①.通过JAVA层的本地方法创建同类对象
步骤:
I.通过对象获取类
II.通过类获取类的构造方法的ID
III.基于方法ID和类,创建新对象
JNIEXPORT void JNICALL JAVA_nativeMethod
(JNIEnv *env, jobject thiz,jint i){
...
jclass clazz = (*env).GetObjectClass(thiz);
jmethodID mid = (*env).GetMethodID(clazz,"","()V");
jobject obj = (*env).NewObject(clazz,mid);
...
return;
}
②.通过C/C++创建不同类对象
步骤:
I.通过FindClass方法获取需要的类
II.通过类获取类的构造方法的ID
III.基于方法ID和类,创建新对象
JNIEXPORT void JNICALL JAVA_nativeMethod
(JNIEnv *env, jobject thiz,jint i){
...
jclass clazz = (*env).FindClass("com/x/test/Test");//参数为类路径
jmethodID mid = (*env).GetMethodID(clazz,"","()V");
jobject obj = (*env).NewObject(clazz,mid);
...
return;
}
③获取上下文环境JNIEnv
如果找不到上下文JNIEnv就要获取
bool AttachCurrentThread(JavaVM* vm, JNIEnv** p_env){ bool bAttached = false; switch(vm->GetEnv((void**)p_env, JNI_VERSION_1_4)) { case JNI_OK: break; case JNI_EDETACHED: if (vm->AttachCurrentThread(p_env, 0) < 0) { LOGD("%s :test failed!",__func__); return false; } else { bAttached = true; } break; case JNI_EVERSION: LOGE("Invalid java version"); break; } return bAttached;}
总结
以上总结了JNI中函数注册的两种方法,在实际应用中都很常见都用得到的,要理解到位才可以;
下次文章会继续讲解关于JNI的知识点和高级应用