JNI函数动态注册进阶

3,089 阅读6分钟

函数动态注册 这篇文章的结尾提到了一个"动态注册"的工作效率问题。当我们在大型的项目中,需要在底层实现一个功能时,我们会在 Java 层声明一个 native 方法,那么在 JNI 层必须有一个本地函数相对应,我们知道"动态注册"的一个好处是可以随意定义函数的名子,函数的类型也可以通过 javah 命令获得。但是我们有没有思考过一个问题,如果只是简单的添加一个JNI函数就需要使用一次javah命令,这样的工作效率是不是太低了?那怎么样才能快速写出函数的类型呢?这就是本文要讲的东西。

例子剖析

我们通过一个例子来感受下今天要学习的内容。

假设现在有一个 JavaHello.java

public class Hello
{
    native int test(String msg);
    static native int static_test(String msg);
}

Hello.java 有两个 native 函数,最大的区别就是一个静态的,一个是非静态的,那么调用方式当然也不一样,大家应该都明白。

现在我要使用"动态注册"技术,那么首先就要面临一个问题,JNI 层的函数怎么写?我可以在不使用 javah 命令的前提下,把这个函数手写出来

jint native_test(JNIEnv * env, jobject thiz, jstring msg);
jint native_static_test(JNIEnv * env, jclass clazz, jstring msg)

可能有人觉得我在吹牛逼,那么我现在来解释下这个函数是怎么写出来的。

函数名: 从 函数动态注册 这篇文章中可知,"动态注册"的JNI层的函数名可以随意取的,我把Java层的native方法前加一个native_前缀,这样一眼就能看出来对应关系。

返回值: 返回值如何确定呢?其实是有一个类型的对应关系,在这个例子中,Javaint类型在JNI中对应jint类型。

第一个参数: 这个参数是固定的,它是指向JNIEnv的指针。

第二个参数: 代表的是Java对象或者Java类。如果调用Javanative方法的是对象,那么JNI层对应的就是jobject类型,如果调用Javanative方法是类,也就是说调用的是静态的native方法,那么JNI层对应的就是jclass类型。

JNI层函数剩下的参数就是和Java层的native层的函数一一对应的,在这个例子中,JavaString类型对应的就是JNIjstring类型。

我们是不是突然发现,原来我们需要掌握Java类型和JNI类型的对应关系。

函数动态注册 这篇文章中可知,我们还需要知道函数的签名,我也可以很快的写出来

static const JNINativeMethod nativeMethods[] = {
        {"static_test", "(Ljava/lang/String;)I", (void *)native_static_test},
        {"test", "(Ljava/lang/String;)I", (void *)native_test}
};

其中的关键就是要掌握函数的类型签名怎么写。

看完这篇文章剩下的内容你就能明白这一切。

JNI类型

基本类型

对于基本类型的对等关系呢,我们可以用一张表来说明

Java TypeNative TypeDescription
booleanjbooleanunsigned 8 bit
bytejbytesigned 8 bit
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/A

对于Java的基本类型,对应的JNI类型只需要在前面加一个j前缀即可,非常好记。

引用类型

对于引用类型的对应关系,我在官网上抠了一张图,如下。

引用类型

让我们用规律来记住这一张图。

首先,对于数组类型,分别基本类型数组和引用类型数组。基本类型数组有特殊的JNI对象,如 int[] 对应 jintArray,而对于引用类型,这幅图没有指明,其实统一对应 jobjectArray,例如 Java String[] 对应 JNI 的 jobjectArray。

然后,除了数组类型外,对于其它类型,基本上都用 jobject 表示,但是有三个例外,String 对应 jstring,Class 对应 jclass,Throwable 对应 jthrowable。

如果你使用的是C语言,那么这些引用类型之间的关系如下

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

可以看到 jstring, jclass 的类型其实都是 jobject 的别名,而 jobject 的类型居然是一个通知指针类型 void *,我们是不是似乎明白了点什么呢,毕竟C语言都是通知指针来控制数据的,用一个通用指针类型void *表示所有 Java 类型应该不过分吧~

而如果你用的是 C++ 语言,那么这些引用类型关系又是如何呢

/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;

原来JNI层的 jobject,jintArray 这些对象,其实都是指针。

类型签名

基本类型签名

JNI 使用了虚拟机的类型签名的表示方法,我们先看下基本类型签名

Type SignatureJava Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble

基本类型中,除了 booleanZ 以及 longJ 表示签名外,其他的都是用大写的首字母表示,例如 intI 表示类型签名。

除了基本类型,Java其他类型的签名如何表示呢,基本格式如下

引用类型签名

Type SignatureJava Type
L fully-qualified-class ;fully-qualified-class

什么意思呢?例如 String 类型的全路径为 java.lang.String,那么对应的签名为 Ljava/lang/String;。注意了,对于引用类型的签名,签名首字符一定为L,签名末尾字符一定为;

数组类型签名

数组也有一定的签名要求,如下表

Type SignatureJava Type
[typetype[]

怎么理解呢?举两个例子就知道了。

由于int类型的签名为 I,因此 int[] 签名就为 [I

由于java.lang.String类型的签名为Ljava/lang/String;,那么 String[] 的签名为 [Ljava/lang/String;

大家对着例子好好理解,容易看迷糊。

方法类型签名

由于Java有方法重载,然而JNI可没有对应的函数重载功能,因此每个Java方法都有唯一的方法签名才能区分出方法重载

Type SignatureJava Type
( arg-types ) ret-typemethod type

这个也不好理解,我们举个例子。假如现在有个Java方法

String f(int a);

f()方法的返回类型为String,对应签名为Ljava/lang/String;,参数为int类型,对应的签名为I,那么整个方法的签名为(I)Ljava/lang/String;

手写动态注册的代码

看完了上面讲的类型以及签名,再结合函数动态注册 这篇文章所讲的"动态注册"方法,你是否能手写出动态注册的代码呢?

作为这篇文章的结尾,我就使用文章开头用到的例子,手写出动态注册的代码

#include "hello.h"
#include <android/log.h>

#define LOG_TAG "david"

jint native_test(JNIEnv * env, jobject object,jstring msg) {
    const char * p_msg = env->GetStringUTFChars(msg, false);
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "method = %s, msg = %s", __FUNCTION__,  p_msg);
    return 0;
}

jint native_static_test(JNIEnv * env, jclass clazz, jstring msg)
{
    const char * p_msg = env->GetStringUTFChars(msg, false);
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "method = %s, msg = %s", __FUNCTION__,  p_msg);
    return 0;
}


static const JNINativeMethod nativeMethods[] = {
        {"static_test", "(Ljava/lang/String;)I", (void *)native_static_test},
        {"test", "(Ljava/lang/String;)I", (void *)native_test}
};

static int registerNativeMethods(JNIEnv *env) {
    int result = -1;
    jclass class_hello = env->FindClass("com/umx/ndkdemo/Hello");
    if (env->RegisterNatives(class_hello, nativeMethods,
                         sizeof(nativeMethods) / sizeof(nativeMethods[0])) == JNI_OK) {
        result = 0;
    }
    return result;
}


jint JNI_OnLoad(JavaVM * vm, void * reserved) {
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void **)&env, JNI_VERSION_1_1) == JNI_OK) {
        if (registerNativeMethods(env) == JNI_OK) {
            result = JNI_VERSION_1_6;
        }
    }
    return result;
}

到此,函数"动态注册"技术才算是真的讲完了。