IntelliJ IDEA平台下JNI编程(三)—字符串、数组

转载请注明出处:【huachao1001的简书:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】

在前面HelloWorld篇中,自动生成的头文件对本地方法声明的形参列表中的第一个参数即为JNIEnv *。那么JNIEnv到底能用来做什么?初学JNI的时候并没有太在意,只满足于Java能调用C代码就行,而并没有深究。今天这篇文章将学习JNI本地函数中如何与Java代码中的字符串、数组相互访问(或转换)。通过这篇文章的学习,相信会对JNIEnv有进一步了解。

1. 从一个简单的例子开始

先创建一个Java类:com/huachao/java/HelloJNI.java,并声明本地方法:private native String sayHello(String name);

package com.huachao.java;

/**
 * Created by HuaChao on 2017/01/13.
 */
public class HelloJNI {
    static {
        // hello.dll (Windows) or libhello.so (Unixes)
        System.loadLibrary("HelloJNI");     }

    private native String sayHello(String name);

    public static void main(String[] args) {
        // invoke the native method
      String rs=  new HelloJNI().sayHello("HuaChao");
      System.out.println("Java类收到来自JNI的返回:"+rs);
    }

}


编译一下,找到HelloJNI.class,点击右键,选择External Tools>Generate Header File,如下(这个过程有疑问的请先转移至《IntelliJ IDEA平台下JNI编程(一)—HelloWorld篇》):

生成头文件

此时在jni目录中得到com_huachao_java_HelloJNI.h如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_huachao_java_HelloJNI */

#ifndef _Included_com_huachao_java_HelloJNI
#define _Included_com_huachao_java_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_huachao_java_HelloJNI
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

然后继续在jni目录中新建HelloJNI.c文件,如下:

#include<jni.h>
#include <stdio.h>
#include "com_huachao_java_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *env, jobject thisObj, jstring name){
   char buf[128];
   /* ERROR: incorrect use of jstring as a char* pointer */
   printf("Hello %s",name);//这里会出错
   scanf("%s",buf); 
   return buf;//这里出错,不能将char*作为jstring返回
}

同样,在HelloJNI.c上点击右键选择External Tools>Generate DLL(这个过程有疑问的请先转移至《IntelliJ IDEA平台下JNI编程(一)—HelloWorld篇》)。再点击运行,会发现错处!!!!主要是因为printf函数的第二个参数应当为char*类型,而不是jstring。那怎么样将jstring转为char*呢?这就需要借助JNIEnv*了。将HelloJNI.c改为如下:

#include<jni.h>
#include <stdio.h>
#include "com_huachao_java_HelloJNI.h"

JNIEXPORT jstring JNICALL Java_com_huachao_java_HelloJNI_sayHello
  (JNIEnv *env, jobject thisObj, jstring name){
   char buf[128];
   const jbyte *str;
   str = (*env)->GetStringUTFChars(env, name, NULL);
   if (str == NULL) {
     return NULL; /* OutOfMemoryError already thrown */
   }
   printf("Hello %s", str);
   (*env)->ReleaseStringUTFChars(env, name, str);
   /* 假设输入字符不超过127个 */
   scanf("%s", buf);
   return (*env)->NewStringUTF(env, buf);
}

HelloJNI.c上点击右键选择External Tools>Generate DLL后,再运行HelloJNI.java类如下:

JNI_Return
Java类收到来自JNI的返回:JNI_Return
Hello HuaChao

可以看到,通过JNIEnv对象我们可以将jstringchar*相互转换。但需要注意的是,通过GetStringUTFChars函数将jsting转为char*时,有可能会从堆空间中分配新的空间,这就有可能因为内存不足而分配失败,因此需要判断是否为NULL。同时,当不需要时应当将这块新分配的空间释放,即调用ReleaseStringUTFChars函数。也可以在本地方法中构造一个java.lang.String实例对象,通过NewStringUTF函数来构造。

可能有人会问,为什么通过GetStringUTFChars得到的char*需要释放内存,而通过NewStringUTF得到的jstring对象不用释放内存呢?这是因为,使用GetStringUTFChars得到char*是在堆中开辟了新的空间用于存储字符串,用完肯定需要手动回收,因为JVM并不会帮你回收本地方法中开辟的空间。而使用NewStringUTF创建的jstring对象属于java.lang.String实例对象,虚拟机会自动回收,另外用于转换为jstring的char*对象(即例子中的buf)由于是函数内部的局部变量,当Java_com_huachao_java_HelloJNI_sayHello执行结束后,自然会回收其内部所有的局部变量的空间。

从上面结果可以看出,是先输入JNI中的scanf函数中的字符串,再打印Java类中传入的字符串。这个顺序好像跟代码顺序不一致,这具体原因我还不清楚,待查找到资料后再回来修改。

2. 字符串

除了上面小节中介绍的几个与字符串相关的函数以外,JNIEnv中还定义了很多其他的与字符串相关操作的函数。

通过GetStringChars函数得到的本地字符串(char*)是以Unicode编码的数据,我们知道UTF-8编码的字符串一般是以\0作为结束字符,但Unicode编码的字符串并不是这样。为了获取jstring类型引用的Unicode编码的字符串中字符数量,可以通过调用GetStringLength函数;而获取jstring引用的字符串有多少个字节则调用ANSI C语言中的strlen函数,或者是 JNIEnvGetStringUTFLength函数。

我们看看 GetStringChars函数原型:

const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

如果返回的字符串是从原始的java.lang.String实例中拷贝的数据,则第三个参数isCopy指向的内存会被设置为 JNI_TRUE,反之,如果返回的字符串是通过直接指向java.lang.String实例中的内存空间,则isCopy指向的内存会被设置为JNI_FALSE。当isCopy指向的内存存储的是JNI_FALSE,那么不能对返回的char*中的内容进行修改,因为在JavaString是不可变的对象。

大部分情况下,直接将NULL作为isCopy参数,因为大部分情况都不需要关心JVM是从java.lang.String中拷贝的字符串还是直接将指针指向原始的字符串。

一般情况下,不可预测虚拟机是否采用拷贝java.lang.String实例。因此你需要假定GetStringChars函数花费时间和空间 来创建新的本地字符串(char*)。在JVM垃圾回收过程中,为了避免内存空间碎片化,对象可能需要发生移动。如果GetStringChars函数是通过直接将指针指向java.lang.String实例中的字符串,那么垃圾回收器则不再对java.lang.String实例对象进行移动,即java.lang.String实例对象被一直固定在内存的某一位置。如果过多的对象被固定在内存中而不被移动,则会导致有很多内存碎片。因此,每次调用GetStringChars函数时,JVM需要判断,决策是采用拷贝还是采用直接修改指针。

调用GetStringChars函数后,当你不再使用该字符串时,还需要记得调用ReleaseStringChars 。物理isCopy指向的内容是 JNI_TRUE还是JNI_FALSE,都应当调用ReleaseStringChars 。 如果GetStringChars采用的是拷贝方式,则ReleaseStringChars释放拷贝字符串占用的空间;如果GetStringChars是直接修改指针的方式,则将java.lang.String实例对象取消固定(即可被在内存中移动)。

JNI 函数 描述 版本
GetStringChars ReleaseStringChars 获取/释放指向Unicode编码字符串的指针,返回的可能是java.lang.String字符串的拷贝 JDK1.1
GetStringUTFChars ReleaseStringUTFChars 获取/释放指向UTF-8编码字符串的指针,返回的可能是java.lang.String字符串的拷贝 JDK1.1
GetStringLength 返回Unicode编码的字符串中字符的个数 JDK1.1
GetStringUTFLength 返回UTF-8编码的字符串中字节的个数(不包含结尾的\0 JDK1.1
NewString 创建java.lang.String实例,并且包含给定的Unicode编码的C字符串 JDK1.1
NewStringUTF 创建java.lang.String实例,并且包含给定的UTF-8编码的C字符串 JDK1.1
GetStringCritical ReleaseStringCritical 获取一个指向Unicode编码字符串的指针,返回的可能是java.lang.String字符串的拷贝,本地代码在Get/ ReleaseStringCritical之间不能阻塞 Java 2 SDK1.2
GetStringRegion SetStringRegion 从C中已经分配好的缓存中复制/赋值Unicode编码的字符串 Java 2 SDK1.2
GetStringUTFRegion SetStringUTFRegion 从C中已经分配好的缓存中复制/赋值UTF-8编码的字符 Java 2 SDK1.2

3. 数组

3.1 基本类型数组

JNI将基本类型数组与对象数组区分对待,基本类型数组主要是元素为基本类型,对象数组元素是引用类型数组。如下:

int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;

iarrfarr是基本类型数组,而oarrarr2是对象数组。

本地方法中访问基本类型数组就像访问字符串一样需要借助JNI中的函数,如下为一个简单的例子:对int数组元素求和:

class IntArray {
    
    private native int sumArray(int[] arr);
    
    public static void main(String[] args) {
        IntArray p = new IntArray();
        int arr[] = new int[10];
        for (int i = 0; i < 10; i++) {
            arr[i] = i;
        }
        int sum = p.sumArray(arr);
        System.out.println("sum = " + sum);
    }
    
    static {
        System.loadLibrary("IntArray");
    }
}

而在本地代码中,不能像如下代码那样:

/* 以下代码有错误 */
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    int i, sum = 0;
    for (i = 0; i < 10; i++) {
        sum += arr[i];
    }
}

上面代码是有问题的,你必须使用JNI函数来访问基本来下数组中的元素,如下所示:

JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    jint buf[10];
    jint i, sum = 0;
    (*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
    for (i = 0; i < 10; i++) {
        sum += buf[i];
    }
    return sum;
}

上面例子中,使用了GetIntArrayRegion 函数来复制数组中的元素到C语言中的缓存中(buf),其中第三个参数表示起始下标,第四个参数表示复制的元素个数。 只要元素拷贝到C缓存中,本地代码就可以直接使用缓存中的数组了。 前面例子中没有异常检查,这是因为我们知道数组长度是10,所有不会越界。

JNI支持一套数组的Get和Set函数==> Get/Release<Type>ArrayElements (如: Get/ReleaseIntArrayElements),用于本地代码直接获取基本类型数组的指针。由于垃圾回收器的底层实现可能不支持数组对象在内存中固定不动,所以在垃圾回收过程中数组在内存位置发生变化,JVM返回的指针是原始的基本类型数组的拷贝的地址。 上面代码可以改为如下:

JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
    jint *carr;
    jint i, sum = 0;
    carr = (*env)->GetIntArrayElements(env, arr, NULL);
    if (carr == NULL) {
        return 0; /* exception occurred */
    }
    for (i=0; i<10; i++) {
        sum += carr[i];
    }
    (*env)->ReleaseIntArrayElements(env, arr, carr, 0);
    return sum;
}

GetArrayLength 函数返回数组中的元素个数,数组的固定长度在第一次分配内存时确定。函数 Get/ReleasePrimitiveArrayCritical允许虚拟机在访问原始数组时禁用垃圾回收器。你应该像使用 Get/ReleaseStringCritical一样小心地使用Get/ReleasePrimitiveArrayCritical。在Get/ReleasePrimitiveArrayCritical之间的代码必须不能调用任何JNI函数或执行任何阻塞操作,因为这可能会导致应用死锁。

一般使用Get/Release<type>ArrayElements都是安全的,虚拟机要么直接返回数组元素的指针,幺妹返回数组元素拷贝后的地址指针。
| JNI Function| Description | Since |
|:----|:----|:---|
|Get<Type>ArrayRegion Set<Type>ArrayRegion| 从本地分配的基本类型数组中或者复制/赋值| JDK1.1|
|Get<Type>ArrayElements Release<Type>ArrayElements|获取基本类型数组指针,可能返回的是拷贝|JDK1.1|
|GetArrayLength| 返回数组中元素个数 |JDK1.1|
|New<Type>Array| 根据指定的长度创建数组 |JDK1.1|
|GetPrimitiveArrayCritical ReleasePrimitiveArrayCritical|获取或释放基本类型数组指针,可能会禁用垃圾回收,可能返回的是数组的拷贝 |Java 2 SDK1.2|

3.2 对象数组

函数GetObjectArrayElement返回指定下标的元素,而 SetObjectArrayElement函数更新指定下包的元素。不像基本类型数组,我们无法一次性获取所有的对象数组中的元素或者是拷贝多个元素。字符串和数组都是引用类型,可以通过 Get/SetObjectArrayElement来访问数组中的字符串和数组中的数组。 如下代码示例为本地方法创建二维int数组后返回到Java代码中,并且打印该二维数组数组内容:

class ObjectArrayTest {
    private static native int[][] initInt2DArray(int size);
    
    public static void main(String[] args) {
        int[][] i2arr = initInt2DArray(3);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                System.out.print(" " + i2arr[i][j]);
            }
            System.out.println();
        }
    }
    
    static {
        System.loadLibrary("ObjectArrayTest");
    }
}

对应的本地代码实现如下:

JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size) {
    jobjectArray result;
    int i;
    jclass intArrCls = (*env)->FindClass(env, "[I");
    if (intArrCls == NULL) {
        return NULL; /* exception thrown */
    }
    result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
    if (result == NULL) {
        return NULL; /* out of memory error thrown */
    }
    for (i = 0; i < size; i++) {
        jint tmp[256]; /* make sure it is large enough! */
        int j;
        jintArray iarr = (*env)->NewIntArray(env, size);
        if (iarr == NULL) {
            return NULL; /* out of memory error thrown */
        }
        for (j = 0; j < size; j++) {
            tmp[j] = i + j;
        }
        (*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
        (*env)->SetObjectArrayElement(env, result, i, iarr);
        (*env)->DeleteLocalRef(env, iarr);
    }
    return result;
}

本地方法中,先调用了JNI函数FindClass来获取二维数组中元素类型(Class)的引用,上一章中我们介绍过类型映射,我们知道[I表示的是Java中int[]对象的类型。如果FindClass返回NULL,说明类加载失败(可能是因为类文件不存在或者是OOM)。接下来NewObjectArray 函数分配一个数组,其元素类型为intArrCls只向的引用类型。NewObjectArray 函数只能分配一维数组,我们将一维数组作为其元素类型,这样就构成了二维数组。JVM并没有指定多维数组的数据结构,二维数组只是元素类型为数组的数组。

运行结果如下:

0 1 2
1 2 3
2 3 4

上面例子中最外面的循环后面调用了DeleteLocalRef ,这是为了防止虚拟机一直持有JNI中的引用(如例子中的iarr)导致OOM。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 花了几天时间研究了下JNI,基本上知道如何使用了。照我的观点JNI还是不难的,难得只是我们一份尝试的心。 学习过程...
    皇小弟阅读 1,526评论 0 1
  • 开发者使用JNI时最常问到的是JAVA和C/C++之间如何传递数据,以及数据类型之间如何互相映射。本章我们从整数等...
    738bc070cd74阅读 827评论 0 1
  • 对于入门级Android菜鸟的我来说,从配置到开发JNI是一个煎熬的过程,但还是取得了最终的成功。这里主要是整个过...
    杰嗒嗒的阿杰阅读 3,645评论 16 23
  • 我不是王子,连勇士也差的远。 喜欢的情绪被悄悄隐藏。 欢乐气氛偷偷掩盖怯懦的落寞。 文武英俊的标签贴不到我身上。 ...
    景迪瓦阅读 278评论 0 0
  • 很久没有安安静静的坐下来写点东西了。大学时候特别喜欢手指在键盘上敲击的感觉,就像指尖在跳舞,文思也顺势泉涌。而现在...
    丢啊丢啊丢大罐阅读 290评论 0 0