Java 之JNI基础篇(二)

Android的JNI 专栏收录该内容
4 篇文章 1 订阅


上一篇完成了JNI流程的编写,现在来看看javah命令生成的本地方法

#include <jni.h>
#include <jni_md.h>
#include "Hello.h"

JNIEXPORT void JNICALL Java_com_test_JniUtil_sayHello
(JNIEnv *env, jobject jobj){
    printf("Hello Word!\n");
}

可以看到,在C中对应的函数名,实际上是有一定规则的,由“Java_”+完整包名类名+“_”+方法名构成。这里我们写的java的native方法是没有传参的,生成的C函数中却有两个参数,进入到jni.h头文件中查看

typedef const struct JNINativeInterface_ *JNIEnv;

由此可知,JNIEnv 是一个结构体指针,而我们生成的本地方法中的第一个参数实际上是一个二级指针,它是一个指向 JVM 函数表的指针,它就是JNI的使用环境,后面调用JNI定义的一些API时,都需要用到它。第二个参数也是一个结构体,jobject实际上对应java中的Object类型,这里则是表示的调用该native方法的java对象。

数据类型

实际上在JNI中,Java与C/C++代码的互调根本上就是数据的传递,无非是需要本地代码去处理一些数据,最后把结果返回给我们。但是Java的数据类型和C数据类型是不能通用的,这里JNI机制对Java的数据类型做了一些包装,使得Java的数据类型经过JNI转换后,可以在C中得到使用。

我们找到jdk中的jni头文件:

/* jni.h */
typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;
/* jni_md.h */
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;

我们知道,C语言的基本数据类型是和硬件平台相关的,这里在Windows平台的jdk中,jni头文件的定义与Android ndk中的jni头文件是有所区别的

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 */

在Android的JNI开发中,需要注意这些区别,可从文档中得到如下表格
这里写图片描述
除了8种基本数据类型,还有字符串等引用数据类型

/*
 * Reference types, in C.
 */
typedef void*           jobject;
typedef jobject         jclass;
typedef jobject         jstring;
typedef jobject         jarray;
typedef jarray          jobjectArray;
typedef jarray          jbooleanArray;
typedef jarray          jbyteArray;
typedef jarray          jcharArray;
typedef jarray          jshortArray;
typedef jarray          jintArray;
typedef jarray          jlongArray;
typedef jarray          jfloatArray;
typedef jarray          jdoubleArray;
typedef jobject         jthrowable;
typedef jobject         jweak;

由上可知,其他的几种类型,仍然是jobject类型,文档中有如下对应关系
这里写图片描述

从JDK源码中学习JNI

弄清楚了JNI中,java数据类型与C数据类型的映射关系,以及前一篇中提到的基本编写流程,接下来就可以从源码中学习了,我们知道JNI是java的机制,那么肯定没有人比java官方更了解JNI,也没有人写的JNI代码比JDK源码中更好,那么我们何必还浪费时间,到处去搜索资料学习JNI的具体使用呢?后续的NDK开发也是一样的道理,我们可以直接从源码中学习,并且借助JNI官方文档作为辅助,可以快速上手JNI

Java 并不是完全开源的,其中JVM虚拟机是没有开源的,JDK中的native方法也是没有开源的,我们要想学习JDK中的JNI,那就得借助JDK的开源实现,OpenJDK。在Ubuntu系统中,一般都会默认装有OpenJDK,但我们现在是在Windows平台,那么就需要手动去下载一份源码了 地址,本博客以openjdk1.7为例

打开源码后,进入根目录下的/jdk/src/share目录,share目录表示与平台无关的代码,在它的上一级目录中,我们可以很容易发现这一点,接下来我们进入share目录下的native目录,这一级目录下,放的基本就是JDK的native代码实现了。在该级目录下浏览,发现虽然代码不是特别多,但仍然是有一些的,并且很有一部分并不完全是单纯的JNI调用,有些是调用的JVM的内部实现,那么我们就先从较为简单关联较少的开始学习。

在Java中做Adler-32校验计算时,我们可以这样写

    public static void main(String[] args) {
        //Adler32校验
        Adler32 a = new Adler32();
        a.update(3);
        System.out.println(a.getValue());

        a = new Adler32();
        a.update("人生苦短,我用Python".getBytes());
        System.out.println(a.getValue());
    }

Adler32类的代码实际上非常少,也很简单,我们打开openjdk中的该类源码

package java.util.zip;
/**
 * A class that can be used to compute the Adler-32 checksum of a data
 * stream. An Adler-32 checksum is almost as reliable as a CRC-32 but
 * can be computed much faster.
 *
 * @see         Checksum
 * @author      David Connelly
 */
public class Adler32 implements Checksum {
    private int adler = 1;

    /**
     * Creates a new Adler32 object.
     */
    public Adler32() {
    }

    /**
     * Updates the checksum with the specified byte (the low eight
     * bits of the argument b).
     *
     * @param b the byte to update the checksum with
     */
    public void update(int b) {
        adler = update(adler, b);
    }

    /**
     * Updates the checksum with the specified array of bytes.
     */
    public void update(byte[] b, int off, int len) {
        if (b == null) {
            throw new NullPointerException();
        }
        if (off < 0 || len < 0 || off > b.length - len) {
            throw new ArrayIndexOutOfBoundsException();
        }
        adler = updateBytes(adler, b, off, len);
    }

    /**
     * Updates the checksum with the specified array of bytes.
     *
     * @param b the byte array to update the checksum with
     */
    public void update(byte[] b) {
        adler = updateBytes(adler, b, 0, b.length);
    }

    /**
     * Resets the checksum to initial value.
     */
    public void reset() {
        adler = 1;
    }

    /**
     * Returns the checksum value.
     */
    public long getValue() {
        return (long)adler & 0xffffffffL;
    }

    private native static int update(int adler, int b);
    private native static int updateBytes(int adler, byte[] b, int off,int len);
}

可以看到,实际上最终调用的是两个native方法,也就是说算法其实是用现成的C库做的,那么我找到这两个方法的JNI实现代码,jdk/src/share/native/java/util/zip/Adler32.c

#include "jni.h"
#include "jni_util.h"
#include "zlib.h"

#include "java_util_zip_Adler32.h"

JNIEXPORT jint JNICALL
Java_java_util_zip_Adler32_update(JNIEnv *env, jclass cls, jint adler, jint b)
{
    Bytef buf[1];

    buf[0] = (Bytef)b;
    return adler32(adler, buf, 1);
}

JNIEXPORT jint JNICALL
Java_java_util_zip_Adler32_updateBytes(JNIEnv *env, jclass cls, jint adler,
                                       jarray b, jint off, jint len)
{
    Bytef *buf = (*env)->GetPrimitiveArrayCritical(env, b, 0);
    if (buf) {
        adler = adler32(adler, buf + off, len);
        (*env)->ReleasePrimitiveArrayCritical(env, b, buf, 0);
    }
    return adler;
}

可以看到,代码也很简单,只是对现有的C库进行了一个JNI包装,给上层Java调用,由此我们即领悟到JNI的一个使用场景,那就是复用现有的C/C++库。从包含的头文件中,我们得知,使用的是C的zlib库,这里附上一个该库的 地址,源码其实很少,我们可以将源码下载过来,自行编译实现一个JNI动态库,并自行封装给上层调用。

JNI调用本地方法,传递基本数据类型参数

先来看一下Java_java_util_zip_Adler32_update函数
Java_java_util_zip_Adler32_update(JNIEnv *env, jclass cls, jint adler, jint b)
该函数对应Adler32类中的private native static int update(int adler, int b);函数,参数中则分别传入了两个int值,而JNI本地实现中,多出了JNIEnv *env和 jclass cls两个参数,JNIEnv 在上面已经谈到了,它表示JNI的环境,而第二个参数是jclass,和我们之前的jobject不同,这是因为在Java类中,native修饰的方法update()是一个静态方法,而我们第一个示例中的sayHello()方法是一个类成员方法,jclass就相当于Java中的Class类型。

由此可知,当native方法为类成员方法时,JNI本地实现函数中的第二个参数为jobject类型,当native方法为静态方法时,JNI本地实现函数中的第二个参数为jclass类型

接下来看一下函数中的具体实现,因为C语言中没有byte类型,通常有unsigned char表示byte类型,这里的Bytef 实际上就是unsigned char的一个别名,接下来找到zlib.h头文件,并打开,查看
adler32函数如下,这里没什么好说了,是关于C的zlib库的具体用法,注释已经阐释非常清楚了,甚至还写出了demo,真可谓贴心之至。最后运行完adler32()函数,将结果返回给Java层,返回值类型为jint

ZEXTERN uLong ZEXPORT adler32 OF((uLong adler, const Bytef *buf, uInt len));
/*
     Update a running Adler-32 checksum with the bytes buf[0..len-1] and
   return the updated checksum.  If buf is Z_NULL, this function returns the
   required initial value for the checksum.

     An Adler-32 checksum is almost as reliable as a CRC32 but can be computed
   much faster.

   Usage example:

     uLong adler = adler32(0L, Z_NULL, 0);

     while (read_buffer(buffer, length) != EOF) {
       adler = adler32(adler, buffer, length);
     }
     if (adler != original_adler) error();
*/

JNI 基本类型数组的传递

看完了Java_java_util_zip_Adler32_update函数,我们已经学会了Java调用C函数,传递基本数据类型,并返回结果,但我们还没有学习引用数据类型的传递,那么接下来再看第二个native函数,学习一下数组的传递和使用

JNIEXPORT jint JNICALL
Java_java_util_zip_Adler32_updateBytes(JNIEnv *env, jclass cls, jint adler,
                                       jarray b, jint off, jint len)
{
    Bytef *buf = (*env)->GetPrimitiveArrayCritical(env, b, 0);
    if (buf) {
        adler = adler32(adler, buf + off, len);
        (*env)->ReleasePrimitiveArrayCritical(env, b, buf, 0);
    }
    return adler;
}

看到Bytef *buf就知道,这是一个C语言字节数组的指针,那么我基本可以判断GetPrimitiveArrayCritical()函数可以将Java的数组转换为C的数组,它接收三个参数,其中一个就是jarray b,该方法对应的Java方法原型为private native static int updateBytes(int adler, byte[] b, int off,int len);b参数正是我们传递下来的Java字节数组。

我们通过查询JNI官方帮助手册学习该函数
在这里插入图片描述

大致是说这两个函数与已存在的Get/ ReleaseArrayElements函数功能很相似。如果有可能的话,VM虚拟机会返回一个指向基本数组的指针,反之,就返回一个拷贝的副本。但是,如何使用它还是有很大的限制。

其实意思很简单,直白的说,就是调用GetPrimitiveArrayCritical函数既有可能会返回一个JVM中原Java数组的指针,也有可能会得到Java原数组的拷贝,后面还有一大段英文,就是在描述问题的原因,其实就是在告诫我们要小心使用。我们在调用GetPrimitiveArrayCritical之后,最后还要调用ReleasePrimitiveArrayCritical函数,释放一下资源,也就是说这两个函数必须是成对出现的!这一点非常重要,JNI接口中有很多这种Get/Release开头的函数,这种函数通常必须成对使用。

先来看一下函数原型

void * (JNICALL *GetPrimitiveArrayCritical)(JNIEnv *env, jarray array, jboolean *isCopy);

void (JNICALL *ReleasePrimitiveArrayCritical)(JNIEnv *env, jarray array, void *carray, jint mode);

GetPrimitiveArrayCritical函数的第一个参数传入JNI的环境,第二个参数接收Java层传递下来的数组,第三个参数这里需要特别注意,此处接收的是一个指针!网络上有一些资料在这里直接略过,甚至有些存在错误,原因可能是官方文档给出的示例里,这个参数通常传0或者NULL,而有些人可能看到jboolean类型,就不假思索的传入了JNI中定义的jboolean常量

/*
 * jboolean constants
 */
#define JNI_FALSE 0
#define JNI_TRUE 1

正确的传值方式应该是这样的

jboolean isCopy = JNI_TRUE;

jbyte *a1 = (*env)->GetPrimitiveArrayCritical(env, arr1, &isCopy);

在JNI中,还有很多函数都有类似的一个参数,它是表示是否返回Java原数组的一份拷贝,从性能的角度考虑,通常传false不去拷贝,特别是当数组中数据量很大的时候。此处常有人在网上问,为什么直接传JNI_FALSE 没问题,而传入JNI_TRUE 程序会直接崩溃,那么请注意传值的方式。

再看ReleasePrimitiveArrayCritical函数,它需要接收4个参数,在JDK本地方法updateBytes中是如下使用的
(*env)->ReleasePrimitiveArrayCritical(env, b, buf, 0); 由此基本可以推断出,第二个参数与GetPrimitiveArrayCritical中的第二个参数一样,是Java传下来的jarray数组,而第三个参数则是该Java数组转换得到的C语言中的数组指针,正如前面说到的,这两个参数既有可能指向同一个内存区域,也可能指向两个不同内存区域,而这直接关乎第四个参数有没有意义。先卖个关子,暂不解释第四个参数,先讲一下这个函数到底有什么作用,其实这个函数就是通知虚拟机本地代码不再需要访问carray参数,也就是我们这个例子中的buf,即GetPrimitiveArrayCritical返回的指针。 如有必要,调用之后就会将所有对carray所做的更改都复制回Java原始数组。而第四个参数mode就是表示如何处理从Java原始数组拷贝的副本数据。因此,若carray不是Java原始数组的副本,那么这个参数就没有意义,不起作用。看一下JNI文档给出的取值
这里写图片描述

可以看到,有三种取值

  • 0 表示把数据复制回来,并释放carray缓冲区
  • JNI_COMMIT表示把数据复制回来,但不释放carray缓冲区
  • JNI_ABORT 表示释放缓冲区,但不把carray缓冲区的修改复制回Java原始数组

官方文档推荐我们传0作为值,其他值谨慎使用,正如我们上面分析的实例的用法。

说完了这一对函数的具体使用,最后谈一点特别注意事项,也就是开始的一段英文告诫我们的内容,这一对函数的调用之间,被视为”critical region” ,它们之间的一段代码,不能去做阻塞线程的耗时操作,也不能去调用其他JNI函数!
我们知道在 Java 中创建的对象由 JVM管理内存,当这个对象没有被其他对象引用时,在某个时候GC就会自动回收它。我们创建一个数组对象,在本地代码去访问时,假设这个对象正被 GC 线程占用,那么本地代码就会一直处于阻塞状态,直到 GC 释放这个对象的锁之后才能继续访问。为了避免这种现象的发生,JNI 提供了 Get/ReleasePrimitiveArrayCritical 这一对函数,使用它们在本地代码中访问Java数组对象时就会暂停 GC 线程,因此这是一种高效的访问Java数组的方式。

那么还有一个疑问,到底什么时候会返回Java原始数组的指针,什么情况下又返回的是副本呢?这个问题在官方文档同样做了详细说明

这里写图片描述

如果垃圾收集器支持固定,且数组的布局与本地方法所期望的相同,则不需要复制。否则,将该数组复制到一个不可移动的内存块(例如,C堆)中,并执行必要的格式转换。返回指向该副本的指针。

在这个问题上,再强调一点Android NDK开发中的差别,我们知道在Android 4.4以上系统中会默认采用ART模式,而在5.0以上版本中,ART虚拟机则已经完全取代Dalvik虚拟机。而ART虚拟机中的垃圾回收算法将会移动对象在内存中的位置,这样可以减少内存碎片,提高内存的使用率。因此,在JNI中通过获取Java原始数组指针的方式访问Java数组时,很大可能会得到一个副本的指针。

JNI数组操作总结

到这里,我们已经将JNI中数组传递的一些使用讲得清晰透彻了,下面来看一段官方给出的demo,最后我们再从官方文档中总结一下所有的关于数组操作的API函数。

jint len = (*env)->GetArrayLength(env, arr1); 
jbyte *a1 = (*env)->GetPrimitiveArrayCritical(env, arr1, 0);
jbyte *a2 = (*env)->GetPrimitiveArrayCritical(env, arr2, 0);
/* We need to check in case the VM tried to make a copy. */
if (a1 == NULL || a2 == NULL) {
 ... /* out of memory exception thrown */
}
memcpy(a1, a2, len);
(*env)->ReleasePrimitiveArrayCritical(env, arr2, a2, 0);
(*env)->ReleasePrimitiveArrayCritical(env, arr1, a1, 0);

从这个示例中,可以学习到GetArrayLength函数,它是用来获取数组的长度的。原型如下:

jsize GetArrayLength(JNIEnv *env, jarray array);

接着我们从官方文档中得到如下表
这里写图片描述

查看这一系列函数的原型

<NativeType> *Get<Type>ArrayElements(JNIEnv *env,<ArrayType> array, jboolean *isCopy); 

以及与之成对的Release函数原型:

void Release<Type>ArrayElements(JNIEnv *env,<ArrayType> array, <NativeType> *elems,jint mode);

这一系列函数用于返回Java原始数组的指针或者返回Java数组副本的指针,因此在调用Release <PrimitiveType> ArrayElements()之前,对返回的数组所做的修改不一定会反映到Java原始数组中。这一组函数的具体用法与我上面讲的Get/ReleasePrimitiveArrayCritical用法完全相同,至于区别我们上面也已经谈到过了,就是是否会暂停 GC 线程的问题,即是否是安全的。如果还有区别的话,则是Get/ReleasePrimitiveArrayCritical没有指定数组的具体类型,而这一组函数都是针对具体的基本类型使用的,再有就是添加进到JDK的版本号不同,上面的一对是JDK1.2增加的,而当前这一组则是1.1就有了。

最后给出官方的示例
这里写图片描述

如果我们需要传递的数组是固定大小的,且数据量很小,那么就可以考虑下面的这一组函数

这里写图片描述

void Get<Type>ArrayRegion(JNIEnv *env,<ArrayType> array, jsize start,jsize len, <NativeType> *buf);

这一组函数的作用就是得到一个Java原始数组在缓冲区中的副本。文档中有这样一句话

Get/SetArrayRegion is almost always the preferred function because the C buffer can be allocated on the C stack very cheaply. The overhead of copying a small number of array elements is negligible.

大意是告诉我们,当数组的数据量很小时首选这组函数,因为C缓冲区可以非常方便的分配在C堆栈上,因此复制少量数组元素的开销可以忽略不计

来看一下该函数的参数,前两个就不说了,跟之前的用法一样,后面第三个参数start表示从Java原始数组开始复制的起始位置,第四个len表示复制的长度,第五个表示复制到本地目标数组的指针。

下面是官方给出的示例
这里写图片描述

除了上面的这一组获取函数,还有与之对应的修改函数,用于修改数组
这里写图片描述

函数原型

void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize len, NativeType *buf); 

其参数与Get型的类似

最后来看一下在本地代码中创建Java数组的API函数

这里写图片描述

函数原型

<ArrayType> New<Type>Array(JNIEnv *env,jsize length);

该组函数会返回一个Java原始数组的本地引用,如果是NULL,则表示数组不能被构造。 当且仅当这个函数的调用引发了一个OutOfMemoryError异常才会返回NULL。

数组操作函数的总结
这里写图片描述

到此这篇博客基本结束,我们从JDK源码出发,学习了JNI中Java调用本地方法以及基本类型数组的传递,下一篇博客将学习一下在本地C代码中,如何访问Java对象且调用Java的方法,以及本篇博客中缺省的关于对象类型数组(包含字符串数组)的传递操作。

  • 4
    点赞
  • 1
    评论
  • 11
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值