本文已参加「新人创造礼」活动,一起敞开创造之路。

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 来分隔,如:Landroid/os/FileUtilsFileStatus;

可以根据上面的规矩手动书写办法签名,当然还有一种自动获取的办法。 如果是 ndk-build 构建的项目在\build\intermediates\classes\debug 目录下履行,如果是 CMake 构建的项目在\build\intermediates\javac\classes 目录下履行:

javap -s 全类名

如图所示:

NDK 开发之 JNI 方法静态注册与动态注册

5 总结

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