[TOC]

概述

当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?

我们肯定是要简历一个上层Java和底层C++的一个方法调用关系的映射。那么这个映射关系就是JNI开发中的方法注册。通过方法注册将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。

JNI用法

为了使用JNI,在调用本地方法前必须把C/C++代码所在的动态库装载到进程的内存空间中。装载库文件调用的是System类的LoadLibrary()方法,原型如下:

1
public static void loadLibrary(String libName)

loadLibrary()方法的参数是动态库文件名称的一部分。Android JNI动态库的名称必须以“lib”开头,这里传入的参数是去掉前缀“lib”,以及后缀“.so”的中间部分。例如,库文件名是libhello-jni.so,装载库的语句要写成loadLibrary(“hello-jni “)。这里为什么不使用全文件名呢?JNI原本是Java的产物,Java希望代码能跨平台使用,不同平台动态库的后缀并不一样,例如,Linux下是.so, windwos下是.dll。因此,为了适应不同的平台,这里传入的参数去掉了和系统相关的部分。

1
2
3
4
5
6
7
8
9
 /**
* 加载loadLibrary
* 用于在应用程序启动时加载“ hello-jni”库。
* 用于在应用程序启动时加载"dynamic-register-jni"库。
*/
static {
System.loadLibrary("dynamic-register-jni");
System.loadLibrary("hello-jni");
}

调用loadLibrary()方法不需要指定库文件所在的路径,Android会在几个系统目录下查找动态库。

为了保证调用native方法前所需要的动态库已经加载,loadLibrary()的调用位置一般是放在类的static块中,这样进程初始化时就能执行装载语句了。

JNI开发注册分为 静态注册动态注册 两种。默认的实现方式即静态注册。

方法注册

静态注册

定义

通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。

1
2
3
4
5
6
7
8
9
/**
* 加载loadLibrary
* 用于在应用程序启动时加载“ hello-jni”库。
* 用于在应用程序启动时加载"dynamic-register-jni"库。
*/
static {
System.loadLibrary("dynamic-register-jni");
System.loadLibrary("hello-jni");
}

我们在上层Java的文件中创建一个Native的方法声明。

1
2
3
4
5
6
/**
* A native method that is implemented by the 'hello-jni' native library,
* which is packaged with this application.
* 创建本地方法,标记这个一个C或者C++方法
*/
public native String stringFromJNI();

我们可以让Android Studio来自动帮我们生成一个stringFromJNIJni里面的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ==============================Java调用C++的方法=================================================
/**
* Java虚拟机调用Native方法,会传入两个对象
* 命名规则:Java_包名_类名_方法名
* @param env
*/
extern "C" //添加这句话表示我们想让这个方法按照C编译的方法进行编译
JNIEXPORT jstring JNICALL //
Java_com_frewen_android_demo_logic_samples_jni_HelloJNIActivity_stringFromJNI(JNIEnv *env,
jobject /* this */) {
// 实例化一个Hello的字符串
// std::string hello = "Hello from C++";
// 我们这个HelloJni和Native的People关联之后,我们就可以访问C++的代码
People people;
return env->NewStringUTF(people.getStringMsg().c_str());
}

Java虚拟机调用 Native 方法,会传入两个对象。分别是JNIEnv的指针对象。还有一个是jobject对象。这两个对象我们后面再讲。

我们先说一下,Android Studio 帮我们生成这个静态注册的Native层的JNI方法。这个方法遵循一个的命名规则。

Java_包名_类名_方法名

其中使用下划线将每部分隔开,包名也使用下划线隔开,如果名称中本来就包含下划线,将使用下划线加数字替换。

优点

  • 简单明了

缺点

  • 必须遵循注册规则
  • 名字过长
  • 运行时去找效率不高

动态注册

定义

通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。

还是上面的方法。

1
2
3
4
5
6
7
8
/**
* A native method that is implemented by the 'hello-jni' native library,
* which is packaged with this application.
* 创建本地方法,标记这个一个C或者C++方法
*/
public native String stringFromJNI();

public native String stringFromJNI2(String msg);

JNI_OnLoad 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
JNIEXPORT int JNI_OnLoad(JavaVM *vm, void *revered) {
// env我们可以理解为一个函数表,通过他我们可以调用jni中为我们提供的所有的函数
JNIEnv *env = NULL;
// Jni中,如果调用成功的话,我们就认为是JNI_OK。也就是为0
// 这个VM是一个指针,我们通过这个VM指针调用他的GetEnv方法
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_FALSE;
}
// 方法映射表的注册,就是把我们的方法注册到系统中去
//
registerNativeMethods(env, JNI_CLASS_PATH, g_methods, sizeof(g_methods) / sizeof(g_methods[0]));
// LOG_D("JNI Native Methods Load Success")
// TODO 这里为什么要返回JNI使用的版本
return JNI_VERSION_1_6;
}

我们看到代码第10行。调用 registerNativeMethods 的方法进行方法映射表的注册,就是把我们的方法注册到系统中去。

registerNativeMethods方法

定义: jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

参数:

  • clazz:指定的类,即 native 方法所属的类
  • methods:方法数组,这里需要了解一下 JNINativeMethod 结构体
  • nMethods:方法数组的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义JNI的Class的Path的字符串宏.后续我们再看看这个字符串宏是怎么生成的??
#define JNI_CLASS_PATH "com/frewen/android/demo/logic/samples/jni/HelloJNIActivity"

/**
* 通过signature来机型JNI函数的动态注册。
* 什么是signature??
* Java与C或者C++相互调用的时候的表示函数的参数的描述符
* 输入参数放在括号()中,输出参数放在括号外边
* 多个参数之前顺序存放,使用分号隔开
*
* 原始类型的Signature
* boolean Z
* byte B
* char C
* short S
* int I
* long L
* float F
* double D
* void V
*
* 类的Signature
* Java对象参数 "L包路径/类名" 举例:(Ljava/lang/String;)Ljava/lang/String;
* Java数组 "[" 举例:([Student;)[Lstudent; -> Student[] Xxx(Student[])
*/
static JNINativeMethod g_methods[] = {
{
"nativeAddVariable", //Java中函数名
"(II)I", // 方法输入参数是两个int所以是II 返回值是int 所以是I
(void *) nativeAddVariable // Native的函数名称的函数指针,和Java中的函数名不需要一一对应
},
{
"getDynamicMsg", //Java中函数名
//这个方法输入参数为空。输出参数是String对象所以是:Ljava/lang/String;
"()Ljava/lang/String;",
(void *) getDynamicMsg
}
};

JNINativeMethod:

1
2
3
4
5
typedef struct {
const char* name; // native 的方法名
const char* signature; // 方法签名,例如 ()Ljava/lang/String;
void* fnPtr; // 函数指针
} JNINativeMethod;

返回值:成功则返回 JNI_OK (0),失败则返回一个负值。