Android总结-JNI原理及静态、动态注册

大家知道在JNI开发过程中,JAVA代码中调用SO动态库中的函数时,需要找到JAVA接口映射的Native函数。因此JNI就需要对JAVA代码中可能要访问的函数进行事先注册。目前有两种方法注册:静态注册和动态注册。


一、JNI概述

JNI(Java Native Interface,Java本地接口),用于打通Java层与Native(C/C++)层。这不是Android系统所独有的,而是Java所有。众所周知,Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。

二、JNI原理

Jni对于应用本身来说,可以看做一个代理模式。对于开发者来说,需要使用c/c++来实现一个代理程序(jni程序)来实际操作目标原生函数,java程序中则是jvm通过加载并调用此jni程序来间接地调用目标原生函数。

三、静态注册

静态注册的原理是先由JAVA代码编写需要调用的接口什么,然后通过JNI实现这些声明方法。
比较通用的做法是先创建一个java文件,声明需要使用JNI实现的接口,然后使用javah命令生成对应的C/C++头文件。再一一实现头文件中的函数即可。JAVA层调用JIN函数时,会从对应的JNI文件中查找该函数,因此需要把JAVA层接口和Native函数建立一层关联,静态注册的实现方法就是在Native函数命名上遵守特定的格式,否侧就会找不到对应函数而报错。
举个例子:

1. JAVA层接口声明

定义一个JniTest.java类,其中有两个方法。

package com.luoxudong;

public class JniTest {
    static {
        System.loadLibrary("TesJnitLib");//需要加载的动态库名称
    }

    public native int test1(int val);//测试接口1
    public native String test2(String val);测试接口2
}
    

2.生成.h文件

声明JAVA层接口以后,使用javah命令生成C/C++的都文件。

#include <jni.h>

#ifndef _Included_com_luoxudong_JniTest
#define _Included_com_luoxudong_JniTest
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL Java_com_luoxudong_JniTest_test1
  (JNIEnv *, jobject, jint);
JNIEXPORT jstring JNICALL Java_com_luoxudong_JniTest_test2
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

大家应该注意到了,生成的Native函数名称是有一定规则的:包名_类名_方法名称,其中JAVA包名中的“.”替换成“_”了。参数对应的JAVA参数类型换成了对应JNI的参数类型。

3.定义.c/.cpp文件

剩下要做的就是在Native层实现.h定义的函数业务逻辑了。
JNI静态注册有一些缺点,如:

  • native函数名称特别长,不利于书写
  • 每次新增或删除接口时需要重新生成.h文件,比较繁琐
  • 第一次调用时需要根据函数名建立索引,影响效率
  • JNI层的函数名是由java接口名生成,很容易通过hook调用动态库中的函数。

四、动态注册

动态注册的原理是在JNI层通过重载JNI_OnLoad()函数来实现。
针对静态注册的缺点,动态注册方法就可以避免。动态注册的原理是通过RegisterNatives方法把C/C++函数映射到JAVA定义的方法,不需要通过JAVA方法名查找匹配Native函数名,也就不需要遵循静态注册的命名规则。
使用方法如下:

1.定义函数映射表

static JNINativeMethod method_table[] = {
        {"test1", "(I)I", (void *)n_test1},
        {"test2", "(Ljava/lang/String;)Ljava/lang/String;", (void *)n_test2}
};

JNINativeMethod的结构体定义为

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

属性说明:

  • name:JAVA中的方法名称
  • signature:描述JAVA方法的参数和返回值
  • fnPtr:JAVA接口映射的Native函数指针。

其中signature字符串描述分为两部“()”中的表示方法参数类型,“()”后面表述返回值类型。数据类型分为两种:基本数据类型和对象类型:

基本数据类型:

对象类型:

示例:
()V”表示void func()
(I)V”表示void func(int param)
“(Ljava/lang/String;I)Ljava/lang/String;”表示String func(String param1, int param2)

2.重写注册入口函数

so加载时会先调用JNI_OnLoad函数,重写该函数,在里面实现动态注册JNI方法。

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

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

    if (!registerNatives(env)) //注册
    {
        return -1;
    }

    result = JNI_VERSION_1_4;

    return result;
}

static int registerNatives(JNIEnv* env)
{
    if (!registerNativeMethods(env, JNIREG_CLASS, method_table, sizeof(method_table) / sizeof(method_table[0])))
    {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

static int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL)
    {
        return JNI_FALSE;
    }

    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0)
    {
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

这样函数动态注册就完成了,JAVA层调用和静态注册中使用方法一样。

五、总结

以上哪里写的不对或者有待改进,欢迎大家提意见,谢谢!
转载请注明出处:http://www.luoxudong.com/?p=360

发表回复

您的电子邮箱地址不会被公开。

评论(1)

  • 匿名 2020-08-01 11:11

    与v