JNI系列
概述
Android OS加载JNI Lib的方法有两种
- JNI_OnLoad(动态注册)
- 如果JNI Lib实现中没有定义JNI_OnLoad,则dvm调用dvm ResolveNativeMethod进行动态解析(静态注册)
因此,当 java 通过 System.loadLibrary 加载完 JNI 动态库后,紧接着会调用 JNI_OnLoad 的函数。
本文主要介绍JNI实战中的两种方式(静态注册和动态注册)并比较二者的优劣和实战中的一些坑
0x00 静态注册
静态注册基本原理
根据函数名来建立java方法和JNI函数间的一一对应关系。
静态有两个非常重要的关键字JNIEXPORT
和JNICALL
,这两个关键字时宏定义,主要用于说明该函数是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;
关于签名符号可以参考我之前的总结http://blog.csdn.net/xsf50717/article/details/51598748
回到正题,在我们创建的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 https://github.com/xsfelvis/JniDemo
0x02 参考
[1] http://blog.csdn.net/yanbober/article/details/51027520
[2] http://tools.android.com/tech-docs/new-build-system/gradle-experimental
[3] http://blog.csdn.net/sbsujjbcy/article/details/48469569
[4] http://www.jianshu.com/p/7844aafe897d
[5] http://blog.majiajie.me/2016/03/27/%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%E4%BD%BF%E7%94%A8NDK/