上篇文章NDK(一):编写第一个JNI项目,讲到了怎样用Android Studio创建一个项目去编写JNI代码,接下来,就具体介绍JNI与Java之间的调用。
包括简单的参数传递回调,创建pthread线程,以及静动态注册台风“山竹”的到来,导致哪里都去不了,待在家终于把这篇文章码完!
[TOC]
JNI数据类型
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型 |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
宏定义输出日志语句
1 | // native-lib.cpp文件 |
参数介绍
extern “C”
指示编译器这部分代码按C语言进行编译
JNIEXPORT和JNICALL
JNIEXPORT 和 JNICALL,定义在jni_md.h
头文件中,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数,在Java虚拟机加载的时候会链接对应的native方法。
JNIEXPORT:
在 Windows 中,定义为
1
因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。
在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为
1
define JNIEXPORT __attribute__ ((visibility ("default")))
JNICALL:
- 在Windows中定义为:
_stdcall
,一种函数调用约定 - 在类Unix中无定义,可以省略不加
Java调用Native
不传递参数
1 | // java文件 |
传递变量
1 | // java文件 |
传递数组
1 | // java文件 |
传递对象
传递的对象,通过GetObjectClass()
可以获取到对应的class对象,然后通过该class对象调用对象的成员属性,方法。这里先不介绍,放到Native调用Java章节再介绍。
1 | // java文件 |
Native调用Java
基本数据类型签名
基本数据类型的签名采用一系列大写字母来表示, 如下表所示:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
调用Java方法
Native调用Java的流程
GetObjectClass()
获取jclass对象GetMethodID()
传入jclass对象、方法名称和方法参数数据类型签名,获取方法IdCallVoidMethod()
传入方法id和参数
1 | // Java文件 |
调用Java变量
1 | // Java文件 |
创建Java对象
上面介绍的,都是通过传递当前对象到native方法中,如果要使用其他对象,就需要自行创建了。
1 | // Bean.java文件 |
创建Native线程
1 | // Java文件 |
JavaVM与JNIEnv
JNIEnv
JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的。JNIEnv的主要作用有两点:
- 调用Java的方法。
- 操作Java(获取Java中的变量和对象等等)
通过上面的代码实例,可以看到JNIEnv的作用,但是,在多线程中,因为不能跨进程,所以,需要通过JavaVM获取当前线程的JNIEnv。
JavaVM
JavaVM,是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。
通过JavaVM的AttachCurrentThread()
函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。记得在线程退出前,要调用DetachCurrentThread()
函数来释放资源。
JNI_OnLoad
怎样获取到JavaVM的引用呢?可以通过JNI_Onload
函数。Java代码在调用System.loadLibrary()
函数时, 内部就会去查找so中的JNI_OnLoad
函数,如果存在此函数则会调用。并且会传递JavaVM的引用,把其设置为全局引用就可以了。
1 | // JNI_OnLoad会告诉 VM 此 native 组件使用的 JNI 版本 |
pthread_create参数传递
上面实例代码中,把instance传递到其他线程中,是通过声明为一个全局引用,pthread_create()
方法中,发现第4个参数为传递的变量,在这里花费很多时间,一直在探索为啥把instance传递进去,确实无效的。测试结果发现,如果是基本数据类型,是可以传递的,jobject确实没办法。具体原因暂时一直没找到。
1 | // Java文件 |
静态注册和动态注册
静态注册:在此之前我们一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 来进行与java方法的匹配,这种方式我们称之为静态注册。
动态注册:动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式。
不过在Android Studio中,写了native方法后可以自动添加静态方法,也不太需要去写特别上的方法,所以,就看使用的取舍了。
上面介绍的,通过JNI_OnLoad()
可以获取到JavaVM的引用,该方法在动态注册中也使用到。
1 | // Java文件 |
完整代码
- 上面的实例代码都上传到github地址,可以进行详细查看
参考资料
-
google官方ndk的demo,基本覆盖了各种jni的写法,强烈推荐参考的dmeo
-
对JavaVM与JNIEnv介绍得比较详细的一篇文章
Android NDK — Native 线程 pthread
详细介绍pthread的文章