JNI开发流程与引用数据类型的处理

今天我们来看下Java JNI,先看下维基百科给的定义,

JNI, Java Native Interface, Java本地接口,是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用或库,也可以被其他程序调用。本地程序一般是用其它语言(C、C++或汇编语言)编写的,并且被编译为基于本地硬件和操作系统的程序。

本文就是分析下Java调用C++程序的步骤和JNI开发访问数组和字符串的问题。

先看下Android中JNI的开发步骤。简单写了个Demo,看下效果:

Demo.png

调用方式:

button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, HelloWorld.sayHello("JNIEnjoy!"), Toast.LENGTH_LONG).show();
            }
});

点击SUM会对数组进行求和和打印出Native传递过来的二维数组

SUM.png
Log.png

调用代码:

btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                btn2.setText(String.valueOf(ArrayJni.arraySum(get())));

                int[][] arr = ArrayJni.getArray(3);
                for (int i = 0; i < 3; i++) {
                    for (int j = 0; j < 3; j++) {
                        Log.d("JNILOG", String.valueOf(arr[i][j]));
                    }
                }
            }
});

private int[] get() {
        int[] array = new int[10];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        return array;
}

接下来看下JNI开发步骤:

1.JNI开发步骤

第一步,在Java层先建立JNI Class,需要调用Native 方法的地方需要关键字native声明,其中方法sayHello就是需要底层实现的,这个Demo中会用C实现。

public class HelloWorld {

    public static native String sayHello(String name);
}

第二步, Make Project,这样会在app/build/intermediates/classes/debug下生成class文件,如下图所示,当然需要的就是Hello World.class这个文件。

Make Class.png

第三步, 在终端中切换目录到app\build\intermediates\classes\debug, 通过命令生成.h头文件javah -jni juexingzhe.com.hello.HelloWorld,juexingzhe.com.hello是包名,需要换成小伙伴自己的包名。juexingzhe.com.hello.HelloWorld.h文件。

Make h.png

看下文件内容,默认生成的函数名规则是:

Java_包名_类名_Native方法名

其中JNIEnv是线程相关的,即在每个线程中都有一个JNIEnv指针, 每个JNIEnv都是线程专有,线程A不能调用线程B的JNIEnv。

jclass就是HelloWorld这个类,因为在这个例子中方法是静态的,所以默认生成的是jclass,如果方法不是静态的,默认生成的就会传入jobject,指向调用这个native方法时的对象实例。

jstring就是定义方法时传入的参数。

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

#ifndef _Included_juexingzhe_com_hello_HelloWorld
#define _Included_juexingzhe_com_hello_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

第四步,在main目录下新建jni文件夹,将上面生成的.h文件剪切过来。

New JNI Folder.png

第五步,终于到了写c代码的时候了,注意在头文件中,C和C++写法是不一样的。

C中(*env)->NewStringUTF(env, "string)

C++中env->NewStringUTF("string")

最终的juexingzhe.com.hello.HelloWorld.c文件如下:

#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */

/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
        (JNIEnv *env, jclass jcls, jstring jstr)
{
    const char *c_str = NULL;
    char buff[128] = { 0 };
    c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (c_str == NULL)
    {
        printf("out of memory.\n");
        return NULL;
    }
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, jstr, c_str);
    return (*env)->NewStringUTF(env, buff);
}

对上面的代码有几点需要注意的, 参考后面字符串处理。

经过上面五步写代码的步骤就差不多了,还有一个问题,Java层怎么调到这个C文件呢?这就需要第六步配置ndk

第六步,配置ndk,在module包下面的build.gradle中的defaultConfig添加,其中moduleName就是最终打包出来的so库的名字

ndk {
     moduleName 'HelloWorld'
}

最终android这个task是下面这样的

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "juexingzhe.com.hello"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        ndk {
            moduleName 'HelloWorld'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

还需要在工程目录下的gradle.properties中添加下面这句话

android.useDeprecatedNdk=true

重新Make Project就可以生成.so文件,这里没有配置平台,所以会默认生成所有平台的so库,包括arm/x86/mips等

Make SO.png

第七步,需要在Java层加载这个.so文件,在第一步编写的HelloWorld.Java中添加,其中HelloWorld就是上面NDK配置生成的so库名字。

public class HelloWorld {

    static {
        System.loadLibrary("HelloWorld");
    }

    public static native String sayHello(String name);
}

2.字符串处理

再回顾一下上面.c文件的内容:

#include "juexingzhe_com_hello_HelloWorld.h"
#include <stdio.h>
/* Header for class juexingzhe_com_hello_HelloWorld */

/*
 * Class:     juexingzhe_com_hello_HelloWorld
 * Method:    sayHello
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_juexingzhe_com_hello_HelloWorld_sayHello
        (JNIEnv *env, jclass jcls, jstring jstr)
{
    const char *c_str = NULL;
    char buff[128] = { 0 };
    c_str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (c_str == NULL)
    {
        printf("out of memory.\n");
        return NULL;
    }
    sprintf(buff, "hello %s", c_str);
    (*env)->ReleaseStringUTFChars(env, jstr, c_str);
    return (*env)->NewStringUTF(env, buff);
}
  • 1.jstring类型是指向JVM内部的一个字符串,和基本类型不一样,C代码中不能直接拿来用,需要通过JNI函数来访问JVM内部的字符串数据结构。

  • 2.简单看下GetStringUTFChars(env, jstr, &isCopy), jstr是Java传递给本地代码的字符串指针,isCopy取值JNI_TRUE和JNI_FALSE,如果值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定。但我们在开发当中,并不关心这个值是多少,通常情况下这个参数填NULL即可。

  • 3.Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串。JNI支持字符串在Unicode和UTF-8两种编码之间转换,GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转换成一个UTF-8格式的C字符串。在上例中sayHello函数中我们通过GetStringUTFChars正确取得了JVM内部的字符串内容

  • 4.异常检查。调用完GetStringUTFChars需要进行安全检查,因为JVM需要为新诞生的字符串分配内存,分配失败会返回NULL,并抛出OutOfMemoryError异常。Java中如果遇到异常没有捕获程序会立即停止运行。而JNI遇到未处理的异常不会改变程序的运行流程,回继续往下走,这样后面对这个字符串的所有操作都是危险的。所以如果NULL,需要return跳过后面的代码。

  • 5.释放字符串。C和Java不一样,需要手动释放内存,通过ReleaseStringUTFChars函数通知JVM这块内存不需要了。注意GetXXX和ReleaseXXX要配套调用。

  • 6.调用NewStringUTF函数会构建一个新的java.lang.String字符串对象,这个对象会自动转换成Java支持的Unicode编码。如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL。

当然,JNI提供操作字符串的函数很多,这里就不一一解释了,主要需要注意内存的分配和跨线程的问题。

3.数组处理

数组和上面的字符串类似,没办法直接操作,需要通过JNI函数从JVM中获取到对应的指针或者拷贝到内存缓冲区再进行操作。

按照上面步骤再添加一个数组的例子,看下Java代码,两个Native函数,一个求和一个获取二维数组。

public class ArrayJni {

    static {
        System.loadLibrary("HelloWorld");
    }

    //求和
    public static native int arraySum(int[] array);

    //获取二维数组
    public static native int[][] getArray(int size);

}

接下来先看下arraySum的C代码,Java层定义的参数是int类型的数组对应到Native就是jintArray, 通过GetArrayLength获取参数数组的长度,然后通过GetIntArrayRegion将参数数组拷贝到内存缓冲区buffer,之后就可以进行求和操作了。操作完成记得释放内存。

/*
 * Class:     juexingzhe_com_hello_ArrayJni
 * Method:    arraySum
 * Signature: ([I)I
 */
JNIEXPORT jint JNICALL Java_juexingzhe_com_hello_ArrayJni_arraySum
        (JNIEnv *env, jclass jcls, jintArray jarr)
{
    jint i, sum = 0, len;
    jint *buffer;
    //1.获取数组长度
    len = (*env)->GetArrayLength(env, jarr);

    //2.分配缓冲区
    buffer = (jint*) malloc(sizeof(jint) * len);
    memset(buffer, 0, sizeof(jint) * len);

    //3.拷贝Java数组中所有元素到缓冲区
    (*env)->GetIntArrayRegion(env, jarr, 0, len, buffer);

    //4.求和
    for (int i = 0; i < len; ++i) {
        sum += buffer[i];
    }

    //5.释放内存
    free(buffer);

    return sum;
}

再看下生成二维数组的代码, 小伙伴们都知道二维数组中每一个元素其实是一维数组,所以需要先构造一维数组的引用,通过FindClass,再通过NewObjectArray构造二维数组。

通过NewIntArray构造一维数组,然后SetIntArrayRegion赋值int类型数组元素,当然也有GetIntArrayRegion函数,可以将Java数组中的所有元素拷贝到C缓冲区中。

二维数组通过SetObjectArrayElement进行赋值。

为了避免在循环内创建大量的JNI局部引用,造成JNI引用表溢出,在外层循环中每次都要调用DeleteLocalRef将新创建的jintArray引用从引用表中移除。在JNI中,只有jobject以及子类属于引用变量,会占用引用表的空间,jint,jfloat,jboolean等都是基本类型变量,不会占用引用表空间,即不需要释放。引用表最大空间为512个,如果超出这个范围,JVM就会挂掉。

/*
 * Class:     juexingzhe_com_hello_ArrayJni
 * Method:    getArray
 * Signature: (I)[[I
 */
JNIEXPORT jobjectArray JNICALL Java_juexingzhe_com_hello_ArrayJni_getArray
        (JNIEnv *env, jclass jcls, jint size)
{
    jobjectArray result;
    jclass onearray;

    //1.获取一维数组引用
    onearray = (*env)->FindClass(env, "[I");
    if (onearray == NULL){
        return NULL;
    }

    //2.构造二维数组
    result = (*env)->NewObjectArray(env, size, onearray, NULL);
    if (result == NULL){
        return NULL;
    }

    //3.构造一维数组
    for (int i = 0; i < size; ++i) {

        int j;
        jint buffer[256];
        //构造一维数组
        jintArray array = (*env)->NewIntArray(env, size);
        if (array == NULL){
            return NULL;
        }
        //准备数据
        for (int j = 0; j < size; ++j) {
            buffer[j] = i + j;
        }

        //设置一维数组数据
        (*env)->SetIntArrayRegion(env, array, 0, size, buffer);

        //赋值一维数组给二维数组
        (*env)->SetObjectArrayElement(env, result, i, array);

        //删除一维数组引用
        (*env)->DeleteLocalRef(env, array);
    }

    return result;
}

同样地, 数组操作的函数也有很多,这里不可能每个都进行说明,有需要的小伙伴可以自行搜索,差别不会太大。

4.总结

本文只是对Android开发JNI的一点点理解总结,包括JNI开发的步骤,字符串和数组的处理,在JNI Native开发过程中都没办法直接操作引用类型的数据,需要通过JNI提供的函数来获取JVM中的数据,提供的函数有的会进行原数据的拷贝有的会返回原数据的指针,根据自己需要进行不同的选择。

后面有可能会对JNI再出一些内容,比如Native调用Java层的对象方法字段等,有需要的小伙伴们欢迎关注。

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

推荐阅读更多精彩内容

  • 原链接:http://www.ibm.com/developerworks/cn/java/j-jni/ 使用 J...
    王朋6阅读 7,213评论 0 8
  • 注:原文地址 1. JNI 概念 1.1 概念 JNI 全称 Java Native Interface,Java...
    cfanr阅读 57,176评论 9 132
  • 开发者使用JNI时最常问到的是JAVA和C/C++之间如何传递数据,以及数据类型之间如何互相映射。本章我们从整数等...
    738bc070cd74阅读 827评论 0 1
  • 在一个偏僻遥远的山谷里,有一个高达数千尺的断崖。不知道什么时候,断崖边上长出了一株小小的百合。 百合刚刚诞生的时候...
    清心阁阅读 1,812评论 0 1
  • 蔡彦军一如既往的双手捧着玉米棒棒,大夏天的戴着像火车头一样的咖啡色毛线茸茸带围脖的帽子,脑袋和脖子捂的严严实实,生...
    那霞阅读 443评论 14 6