阅读 237

JNI函数动态注册进阶

函数动态注册 这篇文章的结尾提到了一个"动态注册"的工作效率问题。当我们在大型的项目中,需要在底层实现一个功能时,我们会在 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 Type Native Type Description
boolean jboolean unsigned 8 bit
byte jbyte signed 8 bit
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

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

引用类型

除了基本类型外,其他所有 Java 对象在 JNI 对应的类型为 jobject。然而为了方便还定义了一些其他 Java 类型对应的 JNI 类型,例如 String 对应 jstringClass类型对应jclass。我用官网的一张图来描述对应关系。

引用类型

如果你使用的是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;
复制代码

原来 _jclass_jstring 都是空继承于 _jobject,而 _jobject是一个没有任何声明的类。这个就有点费解了,虚拟机是如何完成 Java 引用类型到 _jobject 的转换的?我还木鸡~

类型签名

基本类型签名

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

Type Signature Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double

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

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

引用类型签名

Type Signature Java Type
L fully-qualified-class ; fully-qualified-class

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

数组类型签名

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

Type Signature Java Type
[type type[]

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

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

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

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

方法类型签名

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

Type Signature Java Type
( arg-types ) ret-type method 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;
}
复制代码

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