首先应该介绍什么是JNI,JNI是Java Native Interface的缩写,中文翻译Java本地接口, 也有译为Java本地调用。JNI是Java语言中的一门强大的技术,由于Android上层采用Java语言实现,所以也可以在Android中使用这门技术。
JNI技术主要是完成Java代码与native代码进行交互,简单说就是用Java代码调用native语言编写的代码或用native代码调用Java编写的代码,一般情况下native语言指的是C/C++语言。
在了解了什么是JNI技术后,接下来需要首先看看什么是JNI函数。JNI函数就是在native层定义的本地函数,对应于在java层使用native关键字声明的方法的。直白的说,就是在Java层声明,C/C++语言实现的。当然,这个函数并不一般,它会通过JNI某种机制与Java层的方法进行关联,使得Java层代码可以很方便的调用它。
比如在项目的framework层,用native关键字定义了一个用于读的方法(使用native声明的方法没有方法体,实现在native层),如下所示:
public native int NativeReadOkayData(byte[] buf); // 包名com.yu.ops
使用默认规则(使用动态注册JNI函数则不需遵守此规则),它对应的jni函数则是如下:
JNIEXPORT jint JNICALL Java_com_yu_ops_OkayOps_NativeReadOkayData
(JNIEnv *env, jobject thiz ,jbyteArray buf);
函数名由Java、包名(com.yu.ops,点被下划线替代)、类名(OkayOps)、方法名(NativeReadOkayData)、下划线组成,显得有点复杂,手写方法名则既复杂又容易出错,因此可以借助javah命令快速生成。
通常,使用java提供的javah命令来生成头文件,在src目录下,执行如下命令来完成头文件的生成(某些IDE可以自动生成):
javah -jni com.yu.ops. OkayOps
生成头文件内容如下所示:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_yu_ops_OkayOps */
#ifndef _Included_com_yu_ops_OkayOps
#define _Included_com_yu_ops_OkayOps
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_yu_ops_OkayOps
* Method: NativeReadOkayData
* Signature: ([B)I
*/
JNIEXPORT jint JNICALL Java_com_yu_ops_OkayOps_NativeReadOkayData
(JNIEnv *, jobject, jbyteArray);
/*
* Class: com_yu_ops_OkayOps
* Method: NativeWriteOkayData
* Signature: ([BI)I
*/
JNIEXPORT jint JNICALL Java_com_yu_ops_OkayOps_NativeWriteOkayData
(JNIEnv *, jobject, jbyteArray, jint);
#ifdef __cplusplus
}
#endif
#endif
上面的文件是执行命令自动生成的,该头文件包含了所需的jni函数的原型。
JNIEXPORT、JNICALL是定义在jni.h头文件中的宏定义,声明该JNI函数可从动态库导出和符合调用约定;
JNIEnv是JNI环境结构体指针(C语言版本),使用它能完成很多与java交互的操作;
参数jobject表示Java层native方法的调用对象(此处是OkayOps对象)。
由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的对等(或者说是映射)。下面给出jni与Java数据类型对应表(jni类型均被定义在jni.h头文件中),如下表1和表2,在jni函数中,需要使用以下jni类型来等价与Java语言对应的类型。
表1 基本类型对照表
Java类型 | JNI类型 | 描述 |
---|---|---|
boolean | Jboolean | 无符号8位 |
byte | Jbyte | 无符号8位 |
char | Jchar | 无符号16位 |
short | Jshort | 有符号16位 |
int | Jint | 有符号32位 |
long | Jlong | 有符号64位 |
float | Jfloat | 有符号32位 |
double | Jdouble | 有符号64位 |
在jin.h头文件中有如下定义:
# include /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
表2 引用类型对照表
Java引用类型 | JNI类型 |
---|---|
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
All objects | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
java.lang.Throwable | jthrowable |
更多细节可以查看jni.h头文件
上面列出了JNI自定义类型,而为了操作这些类型,尤其是引用类型,就需要JNIEnv来协助完成。那么,什么是JNIEnv呢?实际上,JNIEnv的实体是一个名为JNINativeInterface的结构体,而这个结构体又是什么呢?JNINativeInterface结构体定义在头文件jni.h中,是一个复杂的函数指针集合,每一个函数指针又会指向一个本地实现函数,来完成特定的功能。诸如常见的New StringUTF,FindClass都定义在其中,如下列出了部分内容:
/* jni.h */
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; // C++
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; // C
typedef const struct JNIInvokeInterface* JavaVM;
#endif
struct JNINativeInterface {
…
jclass (*FindClass)(JNIEnv*, const char*);
…
jstring (*NewString)(JNIEnv*, const jchar*, jsize);
…
void (*SetCharArrayRegion)(JNIEnv*, jcharArray,
jsize, jsize, const jchar*);
…
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
jint);
…
jint (*GetJavaVM)(JNIEnv*, JavaVM**);
….
/* added in JNI 1.6 */
// … 表示省略了部分内容
};
注意:上面的是C语言的情况,若是使用C++,则会相对容易点,只是对上面进行一层简单的封装即可,如下简单看一下:
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions; // 间接通过JNINativeInterface来操作
#if defined(__cplusplus)
jint GetVersion()
{ return functions->GetVersion(this); }
//....
jclass FindClass(const char* name)
{ return functions->FindClass(this, name); }
#endif /*__cplusplus*/
};
详细可以查看jni.h头文件。
有了JNIEnv*指针,就可以使用函数指针调用特定的实现函数,来完成特定需求的功能。需要注意的是,env变量是线程线程相关的,不可从一个线程传递env变量到另外一个线程。
那么又是如何使线程获得这个JNIEnv结构体指针的呢?这里涉及到一个重要的函数JNI_OnLoad(JavaVM* vm,void* reserved),当通过System. loadLibrary()方法来加载我们指定的动态库(如.so库)时,Java虚拟机会检测库中是否实现了JNI_OnLoad函数,如果实现了则这个函数就会被调用,并且一个代表JVM的对象vm被作为参数传递进来,这个对象一个进程只有一份,可以通过它的AttachCurrentThread方法来获得JNIEnv*对象,当我们的线程完成特定任务退出之前,应该调用vm的DetachCurrentThread来释放资源。
上述方法均被定义在jni.h,如下:
/* jni.h */
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
/*
* JNI invocation interface.
*/
struct JNIInvokeInterface { // C
// ....
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
/*
* C++ version.
*/
struct _JavaVM { // C++
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
jint DestroyJavaVM()
{ return functions->DestroyJavaVM(this); }
jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThread(this, p_env, thr_args); }
jint DetachCurrentThread()
{ return functions->DetachCurrentThread(this); }
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
{ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
/*
* Prototypes for functions exported by loadable shared libs. These are
* called by JNI, not provided by JNI.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);
// ....
在JNI_OnLoad()函数中,也可以通过vm->GetEnv((void**)&env来获得JNIEnv*指针。JNI_OnLoad()函数基本功能是确定并返回Java虚拟机支持的JNI版本,我们还可以用作其他用途(诸如做一些初始化工作),一个重要的用途是实现JNI函数的动态注册。
与JNI_OnLoad()函数正好相反,当共享库被卸载时,会调用JNI_OnUnload()函数,我们可以做一些收尾的工作。
在前面讲解了JNI函数,并没有深入探究Java层函数与jni函数的对应关系的建立,那么这种关联是怎样建立的,或者说当发起java native方法的调用时,是如何找到与之对应的jni函数的呢?这个过程可以分别用静态注册和动态注册的方式来完成。其实前面已经讲过了静态注册的原理。没错!就是命名规范,按照前面说的来命名jni函数,就可以实现,这里就不再赘述了。接下来,介绍JNI函数的动态注册过程。
何为动态注册呢?说的直白点就是手动的参与它的注册过程,让JNI函数在一加载完.so动态库后就完成它的注册过程(使之与对应java native函数关联起来),而不是等到调用时再来进行注册,以提高调用效率,并且我们也不用遵守前面的命名规范了,可以给jni函数取自己认为合适的名字。
要完成这个动态注册过程,就需要使用在上面提到过的JNI_OnLoad函数,它是在.so动态库加载后就会被调用的,而这又早于JNI函数的调用时机,因此在这个函数里实现注册过程是很合理的。
要完成动态注册,方法一可以选择使用AndroidRuntime类的registerNativeMethods方法来完成注册,这个方法原型如下:
/*
* Register native methods using JNI.
*/
static int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
使用这个函数需要提供包含进行注册的jni函数的类的全路径(如项目中的OkayOps 类,全路径为com/yu/ops/OkayOps),要进行注册的方法信息结构体数组(JNINativeMethod)及方法个数。JNINativeMethod是一个C结构体,用于存储Java native方法与JNI函数的一一对应关系,包含的信息有native方法名、函数签名、函数指针。它的定义如下所示:
typedef struct {
const char* name; // Java层声明的native函数的名字,不需要带路径 。
const char* signature; // Java层声明的native函数签名信息,用字符串表示
void* fnPtr; //JNI 层对应函数的函数指针,它的类型void*
} JNINativeMethod;
上面涉及一个新概念函数签名,现在只需知道它是用来标识匹配哪个java的native方法即可,为了分析注册过程的条理清晰,将在下一节详细介绍。在registerNativeMethods方法的最后又调用了jniRegisterNativeMethods方法来完成注册,这个函数是在JNIHelp.h中声明(Android提供的帮助类来方便使用jni,路径android/libnativehelper/include/nativehelper/JNIHelp.h,实现在JNIHelp.cpp),可以先来看看这个方法:
/* JNIHelp.cpp */
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast(env);
ALOGV("Registering %s's %d native methods...", className, numMethods);
// 获取指定类名的Class对象,并存储在局部引用中
scoped_local_ref c(env, findClass(env, className));
if (c.get() == NULL) { // 获取class对象为NULL
char* tmp;
const char* msg;
if (asprintf(&tmp,
"Native registration unable to find class '%s'; aborting...",
className) == -1) {
// Allocation failed, print default warning.
msg = "Native registration unable to find class; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
// 调用JNIEnv的RegisterNatives来完成注册
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
char* tmp;
const char* msg;
if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
// Allocation failed, print default warning.
msg = "RegisterNatives failed; aborting...";
} else {
msg = tmp;
}
e->FatalError(msg);
}
return 0;
}
可以发现,jniRegisterNativeMethods函数并不是具体实现,最终它会调用JNIEnv的RegisterNatives函数来完成JNI函数的注册。到此注册过程分析完成,终究是回到JNIEnv上。下面看看RegisterNatives函数原型:
jint (*RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* gMethods , jint numMethods);
可以发现,它和AndroidRuntime::registerNativeMethods函数的参数较为类似,除了第二个参数不同以外,其他均相同。而第二个参数正是要进行动态注册的类的Class运行时类,可以使用JNIEnv的FindClass函数来获取。
第二种进行动态注册的方式就是基于上面的分析,即:第一步,使用JNIEnv的FindClass函数来拿到需要进行动态注册的类的运行时Class类;第二步,直接使用JNIEnv的RegisterNatives函数来完成JNI函数的注册。
到此,我们分析了两种方案来完成JNI函数动态注册的目标。第一种,分析了使用AndroidRuntime::registerNativeMethods函数来完成动态注册的流程,使用该函数总体上来说使用方便,但流程较为复杂,第二种,使用JNIEnv的RegisterNatives函数完成动态注册,这种方法流程简单,但需要自个获取运行时Class类,稍显得烦琐点。
本文实现注册的代码如下:
// 需要注册的方法信息表
static JNINativeMethod method_table[] = {
{"NativeReadOkayData", "([B)I", (void*)Java_android_com_read_yu_data},
{"NativeWriteOkayData", "([BI)I", (void*) Java_android_com_write_yu_data},
};
// 包含本地方法的类的全路径
static const char* classPathName="com/yu/ops/OkayOps";
// 使用AndroidRuntime的registerNativeMethods方法来完成注册
static int register_com_yu_signature_ops(JNIEnv *env)
{
LOGI("register_com_yu_ops_OkayOps");
return AndroidRuntime::registerNativeMethods(env,classPathName,method_table,NELEM(method_table));
}
// 加载动态库的时候被回调
jint JNI_OnLoad(JavaVM* vm,void* reserved)
{
LOGI("JNI_OnLoad");
JNIEnv* env = NULL;
jint result = -1;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_6) != JNI_OK)
{
goto bail;
}
LOGI("register method");
if(register_com_yu_signature_ops(env) < 0) // 注册
{
goto bail;
}
init(); // 做一些初始化工作
return JNI_VERSION_1_6;
bail:
return result;
}
在上面动态注册小节提到一个函数签名(signature)的概念,这是用来干什么的呢?了解java语言的都知道它有一种方法重载机制,因此,为了能够调用正确的java层native方法,光凭方法名称是不够的,还需要知道它的具体参数与返回值。函数签名就是函数的参数与返回值的结合体,用来进行精准匹配。
函数签名由字符串组成,第一部分是包含在圆括号()里的,用来说明参数类型,第二部分则跟的是返回值类型。比如”([Ljava/lang/Object;)Z”就是参数为Object[],返回值是boolean的函数的签名。下表列出类型与签名标识的对应关系:
Java类型 | 类型标识 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
String | L/java/lang/String; |
int[] | [I |
Object[] | [L/java/lang/Object; |
int[]的标识是[I,其他基本数据类型的标识基本类似,用[+类型标识组合。需要注意的是,除了基本数据类型的数组以外,引用类型的标识后都需要跟上一个分号。一般,人为的写签名字符串难免会出错,而且类型签名标识又难以记忆,所幸的是java提供了相关命令来快速生成签名信息。到要生成签名的项目的bin目录下,使用javap命令加 –s选项来快速生成签名信息,如下:
D:\code\yu_jar\bin>javap -s com.yu.ops.OkayOps
Compiled from "OkayOps.java"
public class com.yu.ops.OkayOps {
public java.lang.String SERVICE;
descriptor: Ljava/lang/String;
static {};
descriptor: ()V
public com.yu.ops.OkayOps();
descriptor: ()V
public final int yu_read(byte[]);
descriptor: ([B)I
public final void yu_write(byte[]);
descriptor: ([B)V
public native int NativeReadOkayData(byte[]);
descriptor: ([B)I
public native int NativeWriteOkayData(byte[], int);
descriptor: ([BI)I
}
在方法下面的descriptor的内容即是所需要的签名信息。签名信息比较有用,在JNI函数的调用中,经常会需要以签名作为参数。
在jni.h头文件我们可以看到基本类型方法签名定义,如下:
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
参考:
Java本地接口(JNI)编程指南和规范.