1 前言
上文说到,进行 NDK 开发的时候,我们首先需要把 Java 方法声明为 native,然后编写对应的 C/C++ 代码,并编译成为动态链接库,在调用 Java 方法前加载动态链接库即可调用。那么,Java 层中的方法是如何与 native 层的函数一一对应的呢?
这里有两种方法:静态注册、动态注册。下面进行详细介绍。
2 静态注册
我们使用 Android Studio 创建的 NDK 项目,默认使用的就是静态注册方法。采用静态注册时,Java 层的 native 方法与 native 层的方法在名称上具有一一对应的关系,具体要求如下:
native 层的方法名为:Java_<包名><类名><方法名>(__<参数>)
其中,包名使用下划线代替点号进行分割。只有当 native 方法出现需要重载的时候,native 层的方法名后才需要跟上参数(即上面括号里的内容),参数的编写形式与JNI签名相关(后面会介绍)。
下面是静态注册的步骤:
1、创建一个测试类,通常我们会把所有的 Native 方法放在一个类中。
package com.example.ndk;
public class NativeTest {
public native void init();
public native void init(int age);
public native boolean init(String name);
public native void update();
}
2、然后在当前类的目录下使用命令:
javac NativeTest.java
生成 NativeTest.class文件。
3、在 \app\src\main 目录下使用命令:
javah com.example.ndk.NativeTest
生成 com_example_ndk_NativeTest.h 文件。
#include <jni.h>
/* Header for class com_example_ndk_NativeTest */
#ifndef _Included_com_example_ndk_NativeTest
#define _Included_com_example_ndk_NativeTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_ndk_NativeTest
* Method: init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_ndk_NativeTest_init__
(JNIEnv *, jobject);
/*
* Class: com_example_ndk_NativeTest
* Method: init
* Signature: (I)V
*/
JNIEXPORT void JNICALL Java_com_example_ndk_NativeTest_init__I
(JNIEnv *, jobject, jint);
/*
* Class: com_example_ndk_NativeTest
* Method: init
* Signature: (Ljava/lang/String;)Z
*/
JNIEXPORT jboolean JNICALL Java_com_example_ndk_NativeTest_init__Ljava_lang_String_2
(JNIEnv *, jobject, jstring);
/*
* Class: com_example_ndk_NativeTest
* Method: update
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_ndk_NativeTest_update
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
在例子中,对于拥有重载的 init 方法,其 native 方法名称后都带有参数,而没有重载的 update 方法则没带参数。
静态注册 JNI 方法的弊端非常明显,就是方法名会变得很长,而且当需要更改类名、包名或者方法时,需要按照之前方法重新生成头文件,灵活性不高。因此下面我们介绍另外一种动态注册的方法。
3 动态注册
使用动态注册时,我们需要准备好需要自己想要对应的 native 方法,然后构造 JNINativeMethod 数组,JNINativeMethod 是一种结构体,源码如下:
typedef struct {
// Java层native方法名称
const char* name;
// 方法签名
const char* signature;
// native层方法指针
void* fnPtr;
} JNINativeMethod;
然后重写 JNI_OnLoad 方法(该方法会在 Java 层通过 System.loadLibrary 加载完动态链接库后被调用),我们在其中进行动态注册工作:
static JNINativeMethod methods[] = {
{"init", "()V", (void *)c_init1},
{"init", "(I)V", (void *)c_init2},
{"init", "(Ljava/lang/String;)Z", (void *)c_init3},
{"update", "()V", (void *)c_update},
};
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv *env = NULL;
jint result = -1;
// 获取JNI env变量
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
// 失败返回-1
return result;
}
// 获取native方法所在类
const char* className = "com/example/ndk/NativeTest";
jclass clazz = env->FindClass(className);
if (clazz == NULL) {
return result;
}
// 动态注册native方法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return result;
}
// 返回成功
result = JNI_VERSION_1_6;
return result;
}
extern "C" JNIEXPORT void JNICALL
c_init1(JNIEnv *env, jobject thiz) {
// TODO: implement
}
extern "C" JNIEXPORT void JNICALL
c_init2(JNIEnv *env, jobject thiz, jint age) {
// TODO: implement
}
extern "C" JNIEXPORT jboolean JNICALL
c_init3(JNIEnv *env, jobject thiz, jstring name) {
// TODO: implement
}
extern "C" JNIEXPORT void JNICALL
c_update(JNIEnv *env, jobject thiz) {
// TODO: implement
}
动态注册的步骤如下:
- 通过 vm( Java 虚拟机)参数获取 JNIEnv 变量
- 通过 FindClass 方法找到对应的 Java 类
- 通过 RegisterNatives 方法,传入 JNINativeMethod 数组,注册 native 函数
对于 JNINativeMethod 结构而言,签名是其非常重要的一项元素,它用于区分 Java 中 native 方法的各种重载形式,下面将介绍方法的签名。
4 方法签名
方法签名的组成规则为:
(参数类型标识1参数类型标识2…参数类型标识n)返回值类型标识
类型标识对应关系如下:
类型标识 | Java数据类型 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L包名/类名; | 各种引用类型 |
V | void |
另外,当 Java 类型为数组时,在标识前会有“[”符号,例如:String[] 类型标识为 [Ljava/lang/String;(不要漏掉英文分号),如果有内部类则用 $ 来分隔,如:Landroid/os/FileUtils$FileStatus;
可以根据上面的规则手动书写方法签名,当然还有一种自动获取的方法。
如果是 ndk-build 构建的项目在\build\intermediates\classes\debug 目录下执行,如果是 CMake 构建的项目在\build\intermediates\javac\classes 目录下执行:
javap -s 全类名
如图所示:
5 总结
当熟悉动态注册后,动态注册无疑是注册函数的更好方式,唯一要注意的是注册函数时,别把类名、函数名和签名写错了,不然 loadLibraries 时找不到 native 方法会导致应用 crash。