1. 概述
在上一章节JNI—NDK开发流程(ndk-build与CMake)中讲述了NDK的开发流程,但是还遗留两个问题:
C/C++ 与 Java如何进行通信的?
如何阅读Android Native 源码?
今天来解决第二个问题C/C++与Java 如何进行通信的?
2. 数据类型与描述符
2.1. 数据类型
非常多博文讲述了JNI的数据类型与JAVA语言数据类型的映射关系,但是为什么JNI还需要定义一套本地数据类型呢。JNI定义的数据类型起到了衔接C/C++ 与JAVA的作用。 C/C++ 与JAVA属于两种不同的语言,运行在不同的平台上,也就是说C/C++ 是没有办法识别JAVA中的类型的,比如C/C++ 的整型int与Java整形int无法直接互换直接使用的。那么JNI内部再定义一套数据类型映射C/C++与JAVA之间的数据类型。
看上篇文章(文末有例子的链接)的例子:
JAVA方法:
public native String setString(String text);
JNI 方法:
JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
strcat(jnistr,": I am JNI");
return env -> NewStringUTF(jnistr);
}
JAVA Native方法的String类型在JNI变成jstring类型,而C/C++ 也无法识别jstring,需要通过env提供的函数指针转换成C/C++ 可使用数据类型char。可见JNI定义的数据类型是C/C++ 与JAVA的衔接媒介。 再来看看JNI数据类型与JAVA数据类型的映射关系表:
基本数据类型:
Java数据类型 | JNI数据类型 | 描述 |
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位浮点数 |
引用数据类型:
Java引用数据类型 | JNI应用类型 | 描述 |
Object | jobject | 可以表示任何Java的对象,或者没有JNI对应类型的Java对象 |
String | jstring | Java的String字符串类型的对象 |
Class | jclass | Java的Class类型对象(静态方法的强制参数) |
Object[] | jobjectArray | Java任何对象的数组表示形式 |
boolean[] | jbooleanArray | Java基本类型boolean的数组表示形式 |
byte[] | jbyteArray | Java基本类型byte的数组表示形式 |
char[] | jcharArray | Java基本类型char的数组表示形式 |
short[] | jshortArray | Java基本类型short的数组表示形式 |
int[] | jintArray | Java基本类型int的数组表示形式 |
long[] | jlongArray | Java基本类型long的数组表示形式 |
float[] | jfloatArray | Java基本类型float的数组表示形式 |
double[] | jdoubleArray | Java基本类型double的数组表示形式 |
void | void |
2.2. 描述符
JNI中还有一个概念,描述符,JVM虚拟机中使用描述符来存储数据类型,然后利用该描述符可以定位、调用相关class的参数和方法等。说通俗一点,描述符是JAVA数据类型在JVM中的符号表示,比如java整型 int 在JVM中的表示为 I。 描述符常常用于JNI中C/C++ 代码访问JAVA类或方法,后面第4节注册方式讲述JNI中C/C++如何获取JAVA方法和类。
基本数据类型描述符:
描述符 | JAVA类型 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
V | void |
应用数据类型描述符:
L+类全限定名称+;
例如:String:Ljava/lang/String;
方法描述符:
(<形参描述符>)+返回类型描述符
例如:
void test() : ()V
String getString(String key) : (Ljava/lang/String;)Ljava/lang/String;
3. jni.h——粘合剂
C/C++ 与JAVA的交互函数指针env以及上面讲述的JNI数据类型都是在jni.h中定义。jni.h 定义了C/C++ 与JAVA可以交互的能力的头文件。 并适配了C 与 C++的差异。
上面JNI与JAVA基本类型的映射关系不要记忆,在jni.h文件中查询可获知。一起看看这个文件:
Android P源码目录 : android\libnativehelper\include_jni\jni.h 或
在Demo中cpp文件右击进入查看jni.h文件
#ifdef __cplusplus
/*
* Reference types, in C++
*/
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
#else /* not __cplusplus */
/*
* Reference types, in C.
*/
typedef void* jobject;
typedef jobject jclass;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jobjectArray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jobject jthrowable;
typedef jobject jweak;
#endif /* not __cplusplus */
JNIEnv结构体:
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
struct JNINativeInterface {
.....
jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);
jbyteArray (*NewByteArray)(JNIEnv*, jsize);
jcharArray (*NewCharArray)(JNIEnv*, jsize);
jshortArray (*NewShortArray)(JNIEnv*, jsize);
jintArray (*NewIntArray)(JNIEnv*, jsize);
.....
}
JNIEnv结构体定义了一些系列函数指针,通过该函数指针列表C/C++ 可与JAVA进行交互,并且C 与 C++的调用方式有点不一样(一个是指针的指针,一个指针本身)。至于每个函数指针的具体实现就不讲述了,其中涉及到JVM虚拟机的实现细节了。
4. 注册方式
第2和第3节都讲述了JNI中一些遵循的规则,但是JAVA中调用Native方法是如何找到对应的JNI的方法。其实JVM映射对应的JNI方法主要由两种方式:静态注册和动态注册。
4.1. 静态注册
静态注册通过方法名建立起Java方法与JNI方法的映射关系。
静态注册的JNI方法中JNIEXPORT和JNICALL表明该函数是JNI函数,链接时通过方法名链接到对应的JAVA方法。比如Demo中JNI方法Java_com_wuzl_jnitest_HelloWorldJNI_setString对应JAVA的com.wuzl.jnitest.HelloWorldJNI.setString方法。其中Java中“.”都被替换成下划线“_”
JNI 方法:
JNIEXPORT jstring JNICALL Java_com_wuzl_jnitest_HelloWorldJNI_setString(JNIEnv *env, jobject obj, jstring str){
char* jnistr = (char *) env->GetStringUTFChars(str, NULL);
strcat(jnistr,": I am JNI");
return env -> NewStringUTF(jnistr);
}
但是发现有个弊端,那就是静态注册中JNI方法过长并且复杂,导致代码可读性降低并在编码的过程中容易出错,并且无法定义方法名。那么动态注册恰好解决了静态注册的弊端并可以自定义方法名。
4.2. 动态注册
在编写JNI方法时,利用结构体JNINativeMethod记录JNI与JAVA方法的映射关系,然后实现JNI_OnLoad方法,在该方法中调用JNI的函数指针注册到JVM虚拟机。 结构体JNINativeMethod和JNI_OnLoad方法在jni.h中被定义了, JNINativeMethod为JAVA方法和JNI函数的映射关系表。
typedef struct {
const char* name; /*java 方法名*/
const char* signature; /*java 方法描述符*/
void* fnPtr; /*Native 函数指正*/
} JNINativeMethod;
.....
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
.....
- 创建JNI函数
JAVA方法映射的JNI函数。
jstring sayHello(JNIEnv *env, jobject obj, jstring str){
char *name = (char *) env->GetStringUTFChars(str, NULL);
strcat(name,":say hello ~~~~~");
return env -> NewStringUTF(name);
}
- 编写JAVA Native方法
public native String sayHi(String text);
- C/C++源文件中实现JNI_OnLoad方法
通过System.loadLibarary()加载so库时,JAVA虚拟机会判断是否实现了该函数,若有则执行该函数,类似JAVA代码中的初始化函数。
其中JavaVM *vm可以理解为JAVA 虚拟机指针,一个进程只有一个JavaVM对象,但可有多个JNIEnv对象。然后通过JNIEnv和JAVA代码相互通信。 - 获取JNIEnv结构体指针
通过JavaVM指针获取JNIEnv结构体指针。 - 构建JNINativeMethod数组
JNINativeMethod数组为JAVA方法和JNI方法的映射关系表。 - 获取jclass对象
通过类的全路径获取jclass对象,主要使用了JNIEnv结构体的函数FindClass。 - 注册到虚拟机
通过RegisterNative函数指针注册到JVM虚拟机。
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
/*1. 通过JavaVM 虚拟机获取JNIEnv结构体指针,最终需要通过JNIEnv与Java虚拟机打交道和Java方法进行通信*/
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_EVERSION;
}
/*2. 构建JNINativeMethod数组(JAVA方法和JNI方法的映射关系表)*/
static JNINativeMethod methods_table[] = {
{"sayHi", "(Ljava/lang/String;)Ljava/lang/String;", (void *) sayHello},
};
/*3. 通过类路径查找jclass对象*/
jclass clazz;
clazz = (env)->FindClass("com/wuzl/jnitest/HelloWorldJNI");
if (clazz == NULL) {
return JNI_ERR;
}
/*4. 调用RegisterNative注册到JVM虚拟机,形成映射,参数三为注册native方法的数量*/
if ((env)->RegisterNatives(clazz, methods_table, sizeof(methods_table)/sizeof(methods_table[0])) < 0 ) {
return JNI_ERR;
}
(env)->DeleteLocalRef(clazz);
return JNI_VERSION_1_4;
}
5. 总结
C/C++通过JNIEnv与JAVA进行打交道,以及jni.h粘合剂的作用,同时JNI通过静态注册和动态注册构建JAVA与JNI方法的映射关系。在Android 源码分析过程中,会发现大量动态注册的手法,为后面如何阅读Android Native源码打下了基础。
Demo:https://github.com/PlepleLiang/JNIDemo
参考:
Android JNI编程—JNI基础Android NDK开发:JNI实战篇