1 JNI概念
什么是JNI
JNI 全称 Java Native Interface,Java 本地化接口,可以通过 JNI 调用系统提供的 API。操作系统,无论是 Linux,Windows 还是 Mac OS,或者一些汇编语言写的底层硬件驱动都是 C/C++ 写的。Java和C/C++不同 ,它不会直接编译成平台机器码,而是编译成虚拟机可以运行的Java字节码的.class文件,通过JIT技术即时编译成本地机器码,所以有效率就比不上C/C++代码,JNI技术就解决了这一痛点,JNI 可以说是 C 语言和 Java 语言交流的适配器、中间件。
JNI调用示意图:
什么是NDK?
NDK 全名Native Develop Kit,官方说法:Android NDK 是一套允许您使用 C 和 C++ 等语言,以原生代码实现部分应用的工具集。在开发某些类型的应用时,这有助于您重复使用以这些语言编写的代码库。
JNI与NDK区别
JNI:JNI是一套编程接口,用来实现Java代码与本地的C/C++代码进行交互;
NDK: NDK是Google开发的一套开发和编译工具集,可以生成动态链接库,主要用于Android的JNI开发;
JNI与NDK中都有jni.h文件
jdk中jni.h路径
C:\Program Files\Java\jdk-1.8\include\jni.h路径
ndk中jni.h路径
C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\25.1.8937393\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include
ndk对jdk的jni.h进行了封装,native c/c++使用的是ndk中的jni.h
JNI在Android中作用
JNI可以调用本地代码库(即C/C++代码),并通过 Dalvik 虚拟机与应用层和应用框架层进行交互,Android中JNI代码主要位于应用层和应用框架层;
- 应用层: 该层是由JNI开发,主要使用标准JNI编程模型;
- 应用框架层: 使用的是Android中自定义的一套JNI编程模型,该自定义的JNI编程模型弥补了标准JNI编程模型的不足;
2 JNI数据类型映射
在 JNI 开发中,Java 的数据类型并不是直接在 JNI 里使用的
2.1 基本数据类型
Java基本数据类型和本地类型的映射关系,这些基本数据类型都是可以直接在 Native 层直接使用的
Java数据类型 | JNI本地类型 | C/C++数据类型 | 数据类型描述 |
---|---|---|---|
boolean | jboolean | unsigned char | C/C++无符号8为整数 |
byte | jbyte | signed char | C/C++有符号8位整数 |
char | jchar | unsigned short | C/C++无符号16位整数 |
short | jshort | signed short | C/C++有符号16位整数 |
int | jint | signed int | C/C++有符号32位整数 |
long | jlong | signed long | C/C++有符号64位整数 |
float | jfloat | float | C/C++32位浮点数 |
double | jdouble | double | C/C++64位浮点数 |
2.2 引用数据类型
引用数据类型和本地类型的映射关系:
Java数据类型 | JNI本地类型 | 数据类型描述 |
---|---|---|
java.lang.Object | jobject | 可以表示任何Java的对象,或者没有JNI对应类型的Java对象(实例方法的强制参数) |
java.lang.String | jstring | Java的String字符串类型的对象 |
java.lang.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的数组表示形式 |
java.lang.Throwable | jthrowable | Java的Throwable类型,表示异常的所有类型和子类 |
void | void | N/A |
需要注意的是:
1)引用类型不能直接在 Native 层使用,需要根据 JNI 函数进行类型的转化后,才能使用;
2)多维数组(含二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值;
2.3 方法和变量ID
同样不能直接在 Native 层使用。当 Native 层需要调用 Java 的某个方法时,需要通过 JNI 函数获取它的 ID,根据 ID 调用 JNI 函数获取该方法;变量的获取也是类似。ID 的结构体如下:
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID* jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */```
3 JNI描述符
3.1 域描述符
基本类型描述符
Tips:
除了 boolean 和 long 类型分别是 Z 和 J 外,其他的描述符对应的类型名的大写首字母
引用类型描述符
一般引用类型描述符的规则如下,注意不要丢掉“;”
L + 类描述符 + ;
如,String 类型的域描述符为:
Ljava/lang/String;
数组的域描述符特殊一点,如下,其中有多少级数组就有多少个“[”,数组的类型为类时,则有分号,为基本类型时没有分号
[ + 其类型的域描述符
例如:
int[] 描述符为 [I
double[] 描述符为 [D
String[] 描述符为 [Ljava/lang/String;
Object[] 描述符为 [Ljava/lang/Object;
int[][] 描述符为 [[I
double[][] 描述符为 [[D
对应在 jni.h 获取 Java 的字段的 native 函数如下,name为 Java 的字段名字,sig 为域描述符
//C
jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);
//C++
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
jobject GetObjectField(jobject obj, jfieldID fieldID)
{ return functions->GetObjectField(this, obj, fieldID); }
3.2 类描述符
类描述符是类的完整名称:包名+类名,java 中包名用 . 分割,jni 中改为用 / 分割
如,Java 中 java.lang.String 类的描述符为 java/lang/String
native 层获取 Java 的类对象,需要通过 FindClass() 函数获取, jni.h 的函数定义如下:
//C
jclass (*FindClass)(JNIEnv*, const char*);
//C++
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
字符串参数就是类的引用类型描述符,如 Java 对象 cn.cfanr.jni.JniTest,对应字符串为Lcn/cfanr/jni/JniTest; 如下:
jclass jclazz = env->FindClass("Lcn/cfanr/jni/JniTest;");
3.3 方法描述符
方法描述符需要将所有参数类型的域描述符按照声明顺序放入括号,然后再加上返回值类型的域描述符,其中没有参数时,不需要括号,如下规则:
(参数……)返回类型
例如:
Java 层方法 ——> JNI 函数签名
String getString() ——> Ljava/lang/String;
int sum(int a, int b) ——> (II)Ivoid main(String[] args) ——> ([Ljava/lang/String;)V
另外,对应在 jni.h 获取 Java 方法的 native 函数如下,其中 jclass 是获取到的类对象,name 是 Java 对应的方法名字,sig 就是上面说的方法描述符
//C
jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
//C++
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
不需要记住,可以使用下面方法查看签名
java -s -p xxx.class
4 静态注册和动态注册
当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。
注册分为 静态注册 和 动态注册两种。
4.1 静态注册
定义
通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。
规则
java_完整包名_类名_方法名
特殊规则:
- 包名或类名或方法名中含下划线 _ 要用 _1 连接;
- 重载的本地方法命名要用双下划线 __ 连接;
- 参数签名的斜杠 “/” 改为下划线 “_” 连接,分号 “;” 改为 “_2” 连接,左方括号 “[” 改为 “_3” 连接;
另外,对于 Java 的 native 方法,static 和非 static 方法的区别在于第二个参数,static 的为 jclass,非 static 的 为 jobject;JNI 函数中是没有修饰符的。
示例
包名:com.android.javac,类名:NativeAccessJava
// java method
public native void setJavaString();
// jni method
JNIEXPORT void JNICALL
Java_com_android_javac_NativeAccessJava_setJavaString(JNIEnv *env, jobject thiz)
4.2 动态注册
动态注册原理
动态注册 JNI 的原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:
1> 先编写 Java 的 native 方法;
2> 编写 JNI 函数的实现(函数名可以随便命名);
3> 利用结构体 JNINativeMethod 保存Java native方法和 JNI函数的对应关系;
4> 利用registerNatives(JNIEnv* env)注册类的所有本地方法;
5> 在 JNI_OnLoad 方法中调用注册方法;
6> 在Java中通过System.loadLibrary加载完JNI动态库之后,会自动调用JNI_OnLoad函数,完成动态注册;
不再使用 JNIEXPORT 和 JNICALL 两个宏定义声明指定的方法,也不用依照固定的命名规则命名方法(不过通常 jni 里的方法名还是保持和 native 方法的方法名一致,见名思义),而是通过了一个 RegisterNatives 方法完成了动态注册。
函数映射表
JNINativeMethod这其实是一个结构体,在jni.h头文件中定义,通过这个结构体从而使Java与jni建立联系
typedef struct {
const char* name; //Java中函数的名字
const char* signature;//符号签名,描述了函数的参数和返回值
void* fnPtr;//函数指针,指向一个被调用的函数
} JNINativeMethod;
示例
// java method
public native String stringFromJNI();
public static native int add(int a, int b);
// jni register
jint DyReg::RegisterNatives(JNIEnv *env) {
LOGE("RegisterNatives");
jclass clazz = env->FindClass("com/android/javac/DynamicReg");
if (clazz == NULL) {
LOGE("could't find class: com/android/javac/DynamicReg");
return JNI_ERR;
}
JNINativeMethod methods_MainActivity[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
{"add", "(II)I", (void *) add}
};
// int len = sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]);
return env->RegisterNatives(clazz, methods_MainActivity,
sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
}