《深入理解Android:卷1》- JNI层(一)
这一篇主要学习理论知识
JNI是什么
JNI:Java Native Interface
WikiPedia:JNI是一个编程框架,使得虚拟机(JVM)的Java程序也可以调用C/C++写的本地应用/库。这里的本地应用/库指不同的操作系统有其编程的特殊性,例如同样是打开一个文件,Windows上的API会调用OpenFile函数,而Linux上的API会调用open函数。
JNI的作用
- Java程序中的函数可以调用Native语言写的函数,Native一般指C/C++编写的函数。
- Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。
在Android平台上,Java层、JNI层、Native层所处的位置如下图所示。
1.pngJNI的意义
JVM运行在具体的平台上,所以JVM本身无法做到与平台无关。JNI技术可以对Java层屏蔽不同操作系统平台之间的差异。
JNI实现
Java层
对Java层来说,只需要两步就能完成两项工作就可以使用JNI:
- 加载对应的JNI库
- 声明由关键字native修饰的函数
如果Java调用native函数,需要一个位于JNI层的动态库来实现,然后调用System.loadLibrary方法。System.loadLibrary函数的参数是动态库的名字,系统会自动的根据不同的平台转换成真实的动态库文件名,例如在Linux系统上会转换成pkg_Cls.so,而在Windows平台上会转换成pkg_Cls.dll。
package pkg;
class Cls {
native double f(int i, String s);
static {
System.loadLibrary(“pkg_Cls”);
}
}
Native层
Java层中,有一个函数native double f(int i, String s),这个函数是在Native层中的jdouble Java_pkg_Cls_f__ILjava_lang_String_2函数实现的。
C函数:
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
/* C语言中用char指针获取Java的String */
const char *str = (*env)->GetStringUTFChars(env, s, 0);
/* process the string */
...
/* Now we are done with str */
(*env)->ReleaseStringUTFChars(env, s, str);
return ...
}
C++函数中没有间接层和接口指针,但和C的底层实现原理一样,JNI函数会定义为内联成员函数。(why?)
extern "C" /* specify the C calling convention */
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (
JNIEnv *env, /* interface pointer */
jobject obj, /* "this" pointer */
jint i, /* argument #1 */
jstring s) /* argument #2 */
{
const char *str = env->GetStringUTFChars(s, 0);
// ...
env->ReleaseStringUTFChars(s, str);
// return ...
}
可以看出,native方法中:
- 第一个参数,类型是JNIEnv*,是一个指向JNIEnv的指针类型。
- 第二个参数,如果申明的方法是static类型的,则该参数是一个jclass类型,如果申明的方法是一个非static类型的,则该参数是一个jobject类型。
- 后面的参数是所声明方法的参数
JNI层
JNI层主要的工作是建立Java层和Native层的联系,即Java的f函数和C的Java_pkg_Cls_f__ILjava_lang_String_2函数。建立Java层和Native层联系(JNI函数注册)的方法有静态和动态两种方法。
静态方法
- 首先编写java代码,用javac编译生成.class文件
- 使用 javah 命令生成与.class 文件对应的 .h 头文件
- 实现.h文件中的函数
- 使用ndk工具生成库文件(Linux为so文件,Windows为dll文件)
以f为例,当Java层调用f函数时,会从对应的JNI库中寻找其对应的函数Java_pkg_Cls_f__ILjava_lang_String_2,如果没有,就会报错。如果有则会为Java层的f和JNI层的Java_pkg_Cls_f__ILjava_lang_String_2建立联系,其实就是保存JNI层函数的函数指针。以后再调用时,直接使用函数指针即可。
静态方法的弊端:
- 每个.class文件都要生成对应的.h文件
- javah生成的JNI层函数名特别长,书写起来很不方便
- 初次调用native函数时要根据函数名字找到其JNI层函数,影响影响运行效率
动态方法
在JNI技术中,JNINativeMethod结构体会保存,Java中的native函数和JNI层函数的这种一一对应关系。
typedef struct{
//Java中的native函数的名字
const char* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
const char* signature;
//JNI层对应函数的函数指针
void* fnPtr;
}JNINativeMethod;
然后通过AndroidRunTime类提供了一个registerNativeMethods函数来完成注册函数。
int registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
而这个函数又是调用jniRegisterNativeMethods函数完成注册工作。
int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (*env)->FindClass(env, className);
...
//实际上是调用JNIEnv的RegisterNatives函数完成注册的
if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)
{
return -1;
}
return 0;
}
总的来说,动态注册只用两个函数就能完成。
//找到对应的Java类。env指向一个JNIEnv结构体(稍后说明),className为对应的Java类名
jclass clazz = (*env)->FindClass(env, className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。nMethods为methods数组中native函数的个数
jint RegisterNatives(JNIEnv *env, jclass clazz,const JNINativeMethod *methods, jint nMethods);
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中的JNI_OnLoad函数,如果有就调用。
//JavaVM是虚拟机在JNI层的代表,每个Java进程只有一,
jint JNI_OnLoad(JavaVM *vm, void *reserved);
数据类型转换
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是对此有区分的。
基本数据类型转换
2.png引用数据类型转换
除了基本类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
3.png今日单词:
- JNI(Jave Native Interface): Java本地调用
- JVM(Java Virtual Machine):Java虚拟机
- platform-specific:特定平台
- Native Libraries:本地库
- Primitive Types:基本类型
- Reference Types:引用类型
- parameter: 形参(formal argument)
- argument :实参(actual argument)
- COM(component object model)组件对象模型
参考资料
- 《Java Native Interface Specification》
- 《深入理解Android:卷1》