静态注册
接着上篇博客学习
JNI函数
JNIEXPORT void JNICALL Java_com_example_jnidemo_TextDemo_setText
(JNIEnv *env, jobject this, jstring string){
__android_log_print(ANDROID_LOG_ERROR, "test", "invoke set from C\n");
char* str = (char*)(*env)->GetStringUTFChars(env,string,NULL);
__android_log_print(ANDROID_LOG_ERROR, "test", "%s\n", str);
(*env)->ReleaseStringUTFChars(env, string, str);
}
JNI函数有两个关键字JNIEXPORT和JNICALL,这两个关键字是宏定义,主要用于说明该函数时JNI函数,在虚拟机加载so库时,如果发现函数含有上面两个宏定义时,就会链接到对应java层的native方法。
"extern c"是避免编译器按照C++的方式去编译C函数,原因是C不支持函数的重载,编译之后函数名不变。C++支持函数的重载(这点与java一致),编译之后函数名会改变
函数名
看到.c中的函数名字是"Java_com_example_jnidemo_TextDemo_setText"而在.java中定义的native函数名:setText,为什么对应到.c中函数名会变成这么长呢?
这跟JNI native函数的注册方式有关
JNI native函数有两种注册方式:
1、静态注册:按照JNI接口规范的命名规则注册
2、动态注册:在.cpp的JNI_OnLoad方法里注册
JNI接口规范的命名规则:
Java_PackageName_ClassName_MethodName
当我们在Java中调用native方法时,JVM也会根据这种命名规则来查找、调用native方法对应的C方法
JNIENV
JNIENV代表了Java环境,通过(*JNIEnv)就可以对Java端的代码进行操作比如:
1、创建Java的对象
2、调用Java对象的方法
3、获取java对象的属性等
我们可以通过jni.h查看可知:
JNIEnv指向_JNIEnv,而_JNIEnv是定义的一个C++结构体,里面包含了很多通过JNI接口对象调用的方法。
jobject
jobject代表了定义native函数的Java类或java类的实例:
1、如果native函数是static,则代表类Class对象
2、如果native函数是非static,则代表类的实例对象
我们可以通过jobject访问定义该native方法的成员方法、成员变量等。
java、JNI、C/C++数据类型映射关系
基本数据类型转化关系
在java中调用jni的native方法传递的参数是java类型的,这些参数必须经过Dalvik虚拟机转换成JNI类型的才能被JNI层所识别,如图
引用类型转换关系
JNI的引用类型定义了九种数组类型,以及jobject、jclass、jstring、jthrowable四种类型,它们的继承关系如下图:
它们与java类型的对应关系如下:
动态注册
动态注册的原理
直接告诉native函数其在JNI中对应函数的指针
动态注册的原理是这样的:JNI允许我们提供一个函数映射表,注册给JVM,这样JVM就可以用函数映射表来调用相应的函数,而不必通过函数名来查找相关函数(这个查找效率很低,函数名超级长)。
实现过程:
1、利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应关系
2、在一个JNINativeMehod数组中保存所有native函数和JNI函数的对应关系
3、在Java中通过System.loadLibrary加载完JNI动态库后,调用JNI_OnLoad函数,开始动态注册
4、JNI_OnLoad中会调用(*env)->FindClass找到本地java类
5、JNI_OnLoad中调用(*env)->RegisterNatives函数进行函数注册
动态注册的步骤:
1、创建JNIDemo工程,编写java类,例如:MainActivity.java
2、build project 整个工程,在build\intermediates\javac\debug\classes\包名 文件下会生成MainActivity.class文件
3、在build\intermediates\javac\debug\classes 目录下打开命令行,输入 javah -jni 包名.MainActivity(全类名)生成包名MainActivity.h文件
4、在app目录下新建jni文件夹,在jin文件夹下创建register.c文件
5、编写register.c文件,并导入jni.h,包名_MainActivity.h头文件,重写JNI_OnLoad函数。
6、在register.c文件中创建JNINativeMethod数组,数组里面每一个元素JNI映射函数,这个我们后面会详细讲解。
7、在JNI_OnLoad函数里面需要完成以下四步:
- 通过(*vm)->GetEnv获取JVM的JNIEnv的属性信息
- 通过(*env)->FindClass方法找到对应的本地类
- 通过(*env)->RegisterNatives方法注册java本地类中的方法
- 通过return 返回值指定指定Jni版本
8、在jni文件夹下创建CMakeLists.txt文件,并在CMakeLists.txt文件中指定要生成的so库名称以及要使用的register.c源文件
9、在build.gradle文件中添加cmkeLists.text文件的路径,
10、运行so库。
JNINativeMethod
Android中使用了一种与传统不同的java JNI的方式的来定义其native的函数。
其中很重要的区别是Android使用了一种java和C函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组的类型是JNINativeMethod,JNINativeMethod其实是一个结构体,在jni.h头文件中定义,通过这个结构体从而使java与jni建立联系,定义如下:
typedef struct {
const char* name; //Java中函数的名字
const char* signature;//符号签名,描述了函数的参数和返回值
void* fnPtr;//函数指针,指向一个被调用的函数
} JNINativeMethod;
- 第一个变量name 是java中函数的名字
- 第二个变量signature,是字段描述符(签名)或者函数描述符(签名)
- 第三个变量fnotr是函数指针,指向C函数。Void*是万能指针,可以理解相当于java中的泛型
下面我们重点讲一下第二个变量signature也就是签名。
签名
什么是签名
所谓函数签名,简单点的理解可以理解成一个函数的唯一标识,一个签名对应着一个函数的签名。这个是一一对应的关系。有些人可能会问:函数名不能作为标识吗?答案是否定的,因为java支持重载
为什么要有签名呢?
我们知道,java是支持函数重载的。一个类里面可以有多个同名但是不同参数的函数,所以函数名+参数名才能构成一个函数的表示,因此我们需要针对参数做一个签名标识。这样jni层才能唯一识别到这个函数。
如何获取函数的签名
函数的签名是针对函数的参数以及返回值进行组成的。它遵循如下格式(参数类型1;参数类型2;参数类型3…)返回值类型。
目前我知道获取签名的两种方式:1、生成的.h文件,对应函数上面的注释
2利用命令:javap -s -p TextDemo.class
基本数据类型签名对照表
类型标识 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
L/java/language/String String
[I int[]
[Ljava/lang/object Object[]
V void