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 全类名

如图所示:

静态和动态注册卸载广播 android jni静态注册和动态注册_jni

5 总结

当熟悉动态注册后,动态注册无疑是注册函数的更好方式,唯一要注意的是注册函数时,别把类名、函数名和签名写错了,不然 loadLibraries 时找不到 native 方法会导致应用 crash。