前言小计
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,"<init>","()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,"<init>","()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的知识点和高级应用
本文转载自微信公众号「Android开发编程」,可以通过以下二维码关注。转载本文请联系Android开发编程公众号。