Andoid NDK编程 1 - 注册native函数

打算对Android的NDK的开发做一总结,首先是JNI部分,接下来是NDK的内容。今天首先介绍一下JNI的第一部分:注册native函数。

当java代码中执行native的代码时候,首先是通过一定的方法来找到这些native方法。而注册native函数的具体方法的不同,会导致系统在运行时采用不同的方式来寻找这些native方法。

JNI有如下两种注册native方法的途径:静态和动态。其中:

静态:先由Java得到本地方法的声明,然后再通过JNI实现该声明方法。

动态:先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法。

静态注册

根据函数名找到对应的JNI函数:Java层调用函数时,会从对应的JNI中寻找该函数,如果没有就会报错,如果存在则会建立一个关联联系,以后在调用时会直接使用这个函数,这部分的操作由虚拟机完成。
静态方法就是根据函数名来遍历java和jni函数之间的关联,而且要求jni层函数的名字必须遵循
特定的格式。

具体的实现很简单,首先在java代码中声明native函数,然后通过javah来生成native函数的具体形式,接下来在JNI代码中实现这些函数即可。

看个例子就更明了了:

Java层:

static {
   System.loadLibrary("samplelib_jni");
    registerNatives();
}

private native void nativeFunc1();
private native void nativeFunc2();
private native void nativeFunc3();

接下来通过javah来产生jni代码声明:

假设你的java文件的包名是com.jni.samle 而类名是JniSample。

javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.jni.samle.JniSample    

然后就会得到一个JNI的.h文件,里面包含这几个native函数的声明,观察一下文件名以及函数名,会有一定的规律,我会在下一篇文章中对此做一详细介绍,在此不再赘述。

最后实现jni层的native方法即可。

动态注册

对Java程序员来说,可能我们总是会遵循:1.编写带有native方法的Java类;—->2.使用javah命令生成.h头文件;—->3.编写代码实现头文件中的方法,这样的标准流程,但也许有人无法忍受那“丑陋”的方法名称,所以我们可以采用动态注册方法,也就是通过RegisterNatives方法把c/c++中的方法隐射到Java中的native方法,而无需遵循特定的方法命名格式。

JNI 允许你提供一个函数映射表,注册给Jave虚拟机,这样Jvm就可以用函数映射表来调用相应的函数,
就可以不必通过函数名来查找需要调用的函数了。
Java与JNI通过JNINativeMethod的结构来建立联系,它在jni.h中被定义,其结构内容如下:

typedef struct { 
    const char* name; 
    const char* signature; 
    void* fnPtr; 
} JNINativeMethod; 

第一个变量name是Java中函数的名字。

第二个变量signature,用字符串是描述了函数的参数和返回值

第三个变量fnPtr是函数指针,指向C函数。

当java通过System.loadLibrary加载完JNI动态库后,紧接着会查找一个JNI_OnLoad的函数,如果有,就调用它, 而动态注册的工作就是在这里完成的。

一起来看一下具体的实现方法:

Java code:

比较简单,仅仅是加载so库。

static {
    System.loadLibrary("samplelib_jni");
}

JNI code:

在JNI中实现

jint JNI_OnLoad(JavaVM* vm, void* reserved)

并且在这个函数里面去动态的注册native方法,完整的参考代码如下:

#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/zhixin/jnisample/JniManager";

static void sayHello(JNIEnv *env, jobject, jlong handle) {
    LOGI("JNI", "native: say hello ###");
}

static JNINativeMethod gJni_Methods_table[] = {
    {"sayHello", "(J)V", (void*)sayHello},
};

static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    LOGI("JNI", "enter jni_onload");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

比较

下面我们来比较一下这两种不同的实现方法。

首先是静态方法:

优点

  • 实现起来比较简单,直接用javah就能将Java代码中的native函数的声明转化为native代码的函数。

缺点在于:

  • javah生成的jni层函数特别长;
  • 初次调用native函数时要根据名字搜索对应的jni层函数来建立关联联系,这样影响效率。

而动态方法的优缺点刚好和静态方法相反。

通过上面的介绍我们发现,尽管静态注册实现起来比较简单,但是会导致效率相对来说比较低。
所以在JNI层提供JNI_OnLoad是一个推荐的做法。如果不提供JNI_OnLoad,那么JNI函数的命名就需要符合规范,并且需要导出该函数。这样做很容易被别人反编译。相反,如果提供JNI_OnLoad,就可以在里面自己注册JNI函数给Dalvik VM,这样JNI函数的命名就可以按照自己的习惯了,而且也不用导出。

一个巧妙的合作

在实际的应用中,我们可以巧妙的将静态注册和动态注册结合起来:也就是在java代码中仍然声明一个native函数,但是这个函数仅仅是用来去触发在JNI层的native函数的动态注册。说的有些绕,看看代码就明白了:

Java层:

static {
   System.loadLibrary("samplelib_jni");
    registerNatives();
}

private static native void registerNatives();

JNI层:

通过javah生成java层声明的native函数的文件,并且在实现代码中去动态注册JNI函数:

JNIEXPORT void JNICALL Java_com_zhixin_jni_JniSample_registerNatives
(JNIEnv *env, jclass clazz){
     (env)->RegisterNatives(clazz, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
}

后续

关于JNI的函数注册就到这里了,在下一篇文章中会总结JNI的签名规则问题。