JNI 两种注册过程实战

5,925 阅读6分钟
原文链接: blog.csdn.net

JNI系列

JNI两种注册过程实战

深入理解JNI

概述

Android OS加载JNI Lib的方法有两种
- JNI_OnLoad(动态注册)
- 如果JNI Lib实现中没有定义JNI_OnLoad,则dvm调用dvm ResolveNativeMethod进行动态解析(静态注册)
因此,当 Java 通过 System.loadLibrary 加载完 JNI 动态库后,紧接着会调用 JNI_OnLoad 的函数。
本文主要介绍JNI实战中的两种方式(静态注册和动态注册)并比较二者的优劣和实战中的一些坑

0x00 静态注册

静态注册基本原理

根据函数名来建立java方法和JNI函数间的一一对应关系。

静态有两个非常重要的关键字JNIEXPORTJNICALL,这两个关键字时宏定义,主要用于说明该函数是JNI函数,在虚拟机加载so库时,如果发现函数含有上面两个宏定义时,就会链接到对应java层的native方法。

如何使用静态注册

主要是几个关键点

  • 先写java代码,编译生成class文件
  • 使用 javah -jni 包名.文件名 (按照网上使用的方式不好使,在java目录下成功得到.h文件)
  • 配置文件

1 gradle.progerties文件下添加如下代码

android.useDeprecatedNdk=true

这里要说明一下,构建jni模块工程有3种方式

  • 最古老的方式 (非必要)【不使用gralde+手动生成文件+手动ndk-build】

手动写 Android.mk、Applicatoin.mk ,然后手动调用ndk-build去生成so包,早期在ADT时代开发常用

  • 最前沿的方式 使用gradle-experimental 【使用gradle+全自动生成】
    这表示使用当前版本 Gradle 插件,继续使用过时的NDK。由于google为NDK开发提供了一个实验性的工具gradle-experimental但截止目前为止最新版本为0.7.3,从版本号上来看目前仍在实验性,而且需要替换project和app的graldebuilde.gradle,需要改写app的gradle,使用model的方式,改动还是比较大的,但是带来了很大方便,可以直接创建对应的h文件,参考文献[2-4]介绍了这种最新的方式

  • 折中的方式【使用gradle+手动生成头文件+自动ndk-build】

这种方便对当前gradle无改变,唯一需要的是手动生成JNI的头文件(也可以通过配置external tool来自动生成[5])就可以。本文就是采取了这种方式来构建自己的Jni,而自动构建ndk-build就是交给了android.useDeprecatedNdk=true,表示使用ndk来build(因为谷歌出了gradle-experimental更推荐这种方式,但是这种方式改动较大、也不稳定)。

2 build.gradle文件在defaultConfig文件中

        //这里是配置ndk
         ndk{
            moduleName "xsfJni"
            ldLibs "log" //添加依赖库,因为有log等打印
            abiFilters "armeabi", "armeabi-v7a", "x86"  //输出指定三种abi体系结构下的so库。
        }

3 写.c文件(注意与.h文件相同),并且在natvie方法中加载工具类

    //加载静态库
    static {
        System.loadLibrary("Test");//此处加载的是相应的模块库,名称必须和 ndk的moduleName名一样。
    }

在这里使用了以前的,可以看下定义的Util类和使用javah -jni生成的h头文件

public class NDKUtils {
    static {
        System.loadLibrary("xsfJni");   //defaultConfig.ndk.moduleName
    }
    //测试静态方法Jni
    public native String  getVipString();
    public native String  generateKey(String name);
}

通过在 java文件夹下执行 javah -jni xsf.jnidemo.NDKUtils 得到h文件(当然你也可以自己手搓一个,只是容易写错而已),截取其中部分生成代码

#include <jni.h>

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

JNIEXPORT jstring JNICALL Java_xsf_jnidemo_NDKUtils_getVipString
  (JNIEnv *, jobject);
JNIEXPORT jstring JNICALL Java_xsf_jnidemo_NDKUtils_generateKey
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

可以看出JNI调用函数名称是按照一定的规则去生成的,规则如下:

java_完整包名_类名_方法名

静态注册弊端:

  • 后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错

  • 代码编写不方便,由于JNI层函数的名字必须遵循特定的格式,且名字特别长;

  • 会导致程序员的工作量很大,因为必须为所有声明了native函数的java类编写JNI头文件;

  • 程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。

0x01动态注册

静态注册JNI弊端多多,因此使用动态注册JNI十分有必要。

动态注册的原理

直接告诉native函数其在JNI中对应函数的指针;

动态注册的原理是这样的:JNI 允许我们提供一个函数映射表,注册给 JVM,这样 JVM 就可以用函数映射表来调用相应的函数,
而不必通过函数名来查找相关函数(这个查找效率很低,函数名超级长)。

实现过程:

  • 利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应关系;

  • 在一个JNINativeMethod数组中保存所有native函数和JNI函数的对应关系;

  • 在Java中通过System.loadLibrary加载完JNI动态库之后,调用JNI_OnLoad函数,开始动态注册;

  • JNI_OnLoad中会调用AndroidRuntime::registerNativeMethods函数进行函数注册;

  • AndroidRuntime::registerNativeMethods中最终调用jniRegisterNativeMethods完成注册。

如何使用动态注册

在NDKUtils中添加一个动态使用JNI的方法

//测试动态方法Jni
public native String  dynamicGenerateKey(String name);

函数映射表

JNINativeMethod这其实是一个结构体,在jni.h头文件中定义,通过这个结构体从而使Java与jni建立联系

typedef struct {

const char* name; //Java中函数的名字

const char* signature;//符号签名,描述了函数的参数和返回值

void* fnPtr;//函数指针,指向一个被调用的函数

} JNINativeMethod;

关于签名符号可以参考我之前的总结blog.csdn.net/xsf50717/ar…

回到正题,在我们创建的C文件中,上面的dynamicGenerateKey函数映射表如下

//函数映射表
static JNINativeMethod methods[] = {
        {"dynamicGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void *) native_dynamic_key},
        //这里可以有很多其他映射函数
};

JNI_OnLoad()函数

在开篇我们就提过当 java 通过 System.loadLibrary 加载完 JNI 动态库后,紧接着会调用 JNI_OnLoad 的函数。这个函数主要有两个作用

  • 指定Jni版本

告诉JVM该组件使用哪一个jni版本(若未提供JNI_Onload函数,JVM会默认使用最老的JNI1.1版本),如果要使用新的版本的JNI,如JNI 1.4版本,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中)来告知JVM
- 初始化

当JVM执行到System.loadLibrary() 函数时,会立即调用 JNI_OnLoad() 方法,因此在该方法中进行各种资源的初始化操作最为恰当,

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    LOGD("-------------JNI_OnLoad into.--------\n");
    JNIEnv* env = NULL;
    jint result = -1;
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)
    {
        return -1;
    }
    assert(env != NULL);
    //动态注册,自定义函数
    if (!registerNatives(env))
    {
        return -1;
    }

    return JNI_VERSION_1_4;
}

动态注册

RegisterNatives,动态注册就是在这里完成的,函数原型在jni.h中

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)

该函数有3个参数

  • clazz

java类名,通过FindClass得到

  • methods

JNINativeMethod的结构体指针

  • mMethods

方法个数

在该例子中代码如下

//注册Native
static int registerNatives(JNIEnv *env) {
    const char *className = "xsf/jnidemo/NDKUtils"; //指定注册的类
    return registerNativeMethods(env, className, methods, sizeof(methods) / sizeof(methods[0]));
}

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;
}

这样动态注册就完成了

0x01 效果图&code

学习code上传到github github.com/xsfelvis/Jn…

0x02 参考

[1] blog.csdn.net/yanbober/ar…
[2] tools.android.com/tech-docs/n…
[3] blog.csdn.net/sbsujjbcy/a…
[4] www.jianshu.com/p/7844aafe8…
[5] blog.majiajie.me/2016/03/27/…