JNI笔记 : 数据类型、JNI函数与签名

14 篇文章 4 订阅

1 JNI技术简单介绍

首先应该介绍什么是JNI,JNI是Java Native Interface的缩写,中文翻译Java本地接口, 也有译为Java本地调用。JNI是Java语言中的一门强大的技术,由于Android上层采用Java语言实现,所以也可以在Android中使用这门技术。

JNI技术主要是完成Java代码与native代码进行交互,简单说就是用Java代码调用native语言编写的代码或用native代码调用Java编写的代码,一般情况下native语言指的是C/C++语言。


2 了解JNI函数

在了解了什么是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 thizjbyteArray 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 <jni.h>
/* 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对象)。


3 JNI数据类型

由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的对等(或者说是映射)。下面给出jni与Java数据类型对应表(jni类型均被定义在jni.h头文件中),如下表1和表2,在jni函数中,需要使用以下jni类型来等价与Java语言对应的类型。

表1 基本类型对照表

Java类型JNI类型描述
booleanJboolean无符号8位
byteJbyte无符号8位
charJchar无符号16位
shortJshort有符号16位
intJint有符号32位
longJlong有符号64位
floatJfloat有符号32位
doubleJdouble有符号64位

在jin.h头文件中有如下定义:

# include <inttypes.h>      /* 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 objectsjobject
java.lang.Classjclass
java.lang.Stringjstring
Object[]jobjectArray
java.lang.Throwablejthrowable

如下图展示了引用类型以及关系:
引用类型继承体系

更多细节可以查看jni.h头文件


4 深入理解JNIEnv

上面列出了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()函数,我们可以做一些收尾的工作。


5 JNI函数的注册过程

在前面讲解了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<JNIEnv*>(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);
    // 获取指定类名的Class对象,并存储在局部引用中
    scoped_local_ref<jclass> 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;
}

6 签名机制

在上面动态注册小节提到一个函数签名(signature)的概念,这是用来干什么的呢?了解java语言的都知道它有一种方法重载机制,因此,为了能够调用正确的java层native方法,光凭方法名称是不够的,还需要知道它的具体参数与返回值。函数签名就是函数的参数与返回值的结合体,用来进行精准匹配。

函数签名由字符串组成,第一部分是包含在圆括号()里的,用来说明参数类型,第二部分则跟的是返回值类型。比如”([Ljava/lang/Object;)Z”就是参数为Object[],返回值是boolean的函数的签名。下表列出类型与签名标识的对应关系:

Java类型类型标识
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
StringL/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)编程指南和规范.

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值