Java JNI接口的详细描述

当前随着移动设备、大数据以及人工智能的蓬勃发展,我们设计出的App也好或者其他程序也罢对于CPU性能的要求也是越来越高,因此对于Java开发者而言,现在也难免需要用到更偏向硬件底层的C语言。Java语言发展到现在已经经历了20多年,其语言框架本身已经非常成熟,而且整个生态都保持得非常好,因而再与底层的C、甚至汇编进行辅助的话,那就能释放出更强大的威力来。而Java要与本地底层代码进行交互,则需要通过 JNIJava Native Interface)接口。

Oracle官方的JNI说明文档在此(基于Java SE 10):https://docs.oracle.com/javase/10/docs/specs/jni/index.html


环境配置

笔者此前已经写过一篇博文对于JNI的一个初步使用方式,原文为:《 Java JNI的使用基础》。而本篇博文将基于Android开发环境,对JNI接口做更深入详细地介绍。如果各位想了解其他平台如何编译构建动态库的话可以参考《C语言编程魔法书》

本博文所基于的开发环境为Android Studio 3.1.4,采用Java 8语言。而底层的C语言部分则使用的是android-ndk-r16b,基于Clang 5.0编译工具链。

我们要在自己所创建的Android项目工程中使用JNI访问底层C语言代码,则需要做一些准备工作。比如,我们需要在项目工程中的app文件夹中创建一个名为jni的文件夹,然后在里面需要至少创建三个文件——一个是Android.mk,一个是Application.mk,还有一个则是自己所定制的C源文件。当然,如果需要的话还可以增加其他C源文件或者是汇编源文件等。笔者为了能跟各位清晰地展示代码demo,这里就创建了一个名为test.c的C源文件。

Android.mk文件类似于一个makefile文件,它对我们当前JNI项目包做一些编译配置,包括导入哪些其他库文件,输出的动态库文件名叫啥,哪些源文件参与编译等。该文件内容可参考以下代码:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni_test

LOCAL_SRC_FILES := test.c

LOCAL_STATIC_LIBRARIES := cpufeatures

LOCAL_LDLIBS := -llog

include $(BUILD_SHARED_LIBRARY)

$(call import-module, android/cpufeatures)

各位可以看到,这里整个JNI工程将会输出jni_test这一动态库模块,而动态库文件名则为:libjni_test.so。此外,这里还引入了cpufeatures这个库,这个各位可以暂时不用管,反正编译进去也问题不大,这个库的内容很小。
而Application.mk则是对当前JNI所生成目标的整体配置,包括C语言的整体编译选项、输出目标处理器架构等。下面是笔者所写的Application.mk文件内容,各位可以参考:

# Build all available machine code.
APP_ABI := all
APP_CFLAGS += -std=gnu11 -Os

上述代码表示构建所有当前NDK所支持的处理器架构目标动态库。而在C语言编译选项上则使用最新的GNU11标准,并且使用Os(最小尺寸,速度最快)的优化选项。
接着,我们就可以实现tes.t源文件的内容了。

随后,我们在Android项目工程中,需要给build.gradle(Module: app)添加上sourceSets配置,否则使用ndk-build完所生成的库加载不到当前的项目工程中。添加完的内容如下所示:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.test.zenny_chen.test"
        minSdkVersion 17
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
    compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-rc02'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.android.support:design:28.0.0-rc02'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

各位只需关注sourceSets部分即可。


Java与本地代码的桥接

Java端是如何调用JNI本地代码的呢?Java是一门完全基于类的编程语言,当某个类中包含调用本地代码的方法,那么在访问该类时就需要加载相应的动态库(在Windows系统中是dll文件;在macOS中是dylib文件;在其他类Unix系统中则是so文件)。然后,对于实现在JNI侧完成的方法,需要显式地使用native关键字进行声明,指明在调用该方法时需要在刚才所加载的动态库中去找。因此,我们这个demo中,Java侧的类如以下代码所示:

package com.test.zenny_chen.test;

/**
 * 我们定制的JNI测试类
 */
public class MyJNIClass {

    static {
        // 在访问MyJNIClass时,加载jni_test动态库
        System.loadLibrary("jni_test");
    }

    /**
     * 声明一个实例方法,它在JNI中实现
     * @param i 指定一个整数参数
     * @return 某一整数
     */
    public native int nativeInstanceMethod(int i);

    /**
     * 声明一个类方法,它在JNI中实现
     * @param s 指定一个字符串
     */
    public native static void nativeStaticMethod(String s);

    /**
     * 当前类的实例方法,将指定参数的值加1然后返回
     * @param i 指定的一个整数
     * @return 参数加1后的值
     */
    public int increasedInt(int i) {
        return i + 1;
    }

    /**
     * 当前类的类方法,用于实现打印输出指定的字符串
     * @param s 指定的字符串内容
     */
    public static void print(String s) {
        System.out.println(s);
    }

    /** 当前类的一个实例属性 */
    public int mField;

    /** 当前类的一个类属性 */
    public static int cField;
}

上述代码完整展示了一个com.test.zenny_chen.test.MyJNIClass类。它在被访问时就会自动加载jni_test这一动态库。然后,nativeInstanceMethod 是一个MyJNIClass类的实例方法,其实现在JNI侧完成。nativeStaticMethod 则是一个类方法,其实现也是在JNI侧完成。

有了Java端的方法声明,那么当这些JNI方法被调用时,JVM是如何去找这些方法的实现的呢?这就需要本地代码的符号命名与Java端有一套约定成俗的规则。在JNI侧我们需要一套命名法则使得当前函数在动态库中能被JVM找到。这套规则其实也不复杂,基本遵循以下几条:

  1. Java_ 打头,表示这是一个能被JVM识别的在JNI端实现的全局函数。
  2. 必须具体指明当前函数所实现的是具体哪个包里的哪个类中的哪个方法。对于包名之间以及包名与类名之间的分隔符由 . 改为了 _ 单下划线,因为 . 点符号不是一个有效的标识符字符。而对于包名或类名中已经含有一条下划线的,则在该下划线后面加一个数字,即 _1 进行区分。比如,com.test.zenny_chen.test.MyJNIClass类作为C函数名,则可表示为:com_test_zenny_1chen_test_MyJNIClass。
  3. 最后,将上面两条拼接起来,以形成完整的函数名。
    比如,MyJNIClass类中的nativeInstanceMethod实例方法,在JNI中所对应的全局函数名就应该为:Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod。这样,函数名就确定了。

由于我们在JNI侧最重要生成的是动态库,因此我们需要遵循各个系统平台对动态库输出符号的声明规则。比如在Windows平台,动态库中允许被外部动态加载的符号需要用__declspec(dllexport)进行声明,而对于i386架构的处理器,还需要用__stdcall函数调用约定等等。因此在<jni.h>头文件中为了兼容各个平台对于动态库输出符号的声明,用了一些宏:

  1. JNIEXPORT:表示需要输出给外部程序进行动态加载访问的说明符。
  2. JNICALL:表示可被JVM调用的,遵循JNI调用约定的函数说明符。
    因此整个JNI侧的Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函数的声明如下:
/// 这个函数与com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod这一实例方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param instance 表示对当前调用此实例方法的MyJNIClass对象实例的引用
/// @param i 调用此实例方法时所传入的整型参数
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)

尽管在安卓系统上,JNIEXPORT、JNICALL这两个宏可以完全缺省,但出于跨平台性的考虑,笔者这里加上能给各位一个更完整的认知。

下面先介绍一下参数。env这个参数表示JVM在调用此JNI函数时所传入的当前JNI环境的句柄handle)。后面对Java层的类、属性以及方法的访问都需要借助这个句柄。
instance这个参数表示当前调用nativeInstanceMethod方法的对象引用。
参数i表示在Java端调用nativeInstanceMethod方法时所传入的参数值。
这个方法返回一个整数值。
对于实例方法,instance参数指向调用当前方法的对象的引用;而对于类方法,也包含此参数,它指向当前类本身,在JNI侧就是用一个jclass对象来表示的。因此,无论是类方法还是实例方法,第一个参数总是JNIEnv* env。而对于第二个参数,如果是实例方法,那么对应的是jobject instance;如果是类方法,那么对应的是jclass cls,当然,我们下面会看到jclass其实是属于jobject的子类型,所以jclass是兼容于jobject的。从后面开始则是Java端该方法自己的参数列表了,如果参数列表为空,则在JNI层就直接这两个参数。

从上面我们看到Java层映射到JNI层的类型,在Java层原本用int类型的,在JNI层则是用jint来表示。JNI规范约定了以下这些基本类型与Java端相对应。

1.png

以上所表示的都是Java中的基本类型(即值类型)到JNI层的映射,除此之外的类型不是类类型就是对象类型,即都属于引用类型。类类型用jclass表示;对象类型则是用jobject来表示。为了方便对一些常用的对象类型进行操作,JNI侧还约定了以下这些jobject的子类型:

3.png

我们可以看到,其实jclass类型也属于jobject类型的子类型。而事实也是如此,在Java中Class类的声明如下:

public final class Class<T> extends Object

另外,我们还看到了JNI层对Java的数组支持得非常完整。尽管Java中的数组是一种比较特殊的表现方式,但就其类型而言仍然是属于Object的子类类型,并且数组也是一个引用类型,我们可以看以下代码:

Object obj = new int[]{1, 2, 3};

关于Java数组类型映射到JNI的类型,这里举些例子进行说明。比如,Java端的int[]类型对应于JNI侧的jintArray;Java端的char[]类型对应于JNI侧的jcharArray;Java端的String[]类型则对应于JNI侧的jobjectArray类型。
有了这些类型之后,我们就可以在JNI侧对Java层的类、方法与属性进行交互访问了。

有了类型之间的映射关系之后还不够,因为我们知道Java中的方法是可被重载的overloadable),因此为了要准确描述一个方法,既要获得该方法的种类(类方法还是实例方法),还要获得它的名称(即方法标识符)以及类型(包括参数类型以及返回类型)。在JNI层可以通过后续要描述的接口来指定访问的是类方法还是实例方法。而要表示方法或属性的类型,JNI层提供了一套类型签名type signature)机制,如下图表示。

2.png

我们下面来举一些例子。Java端的 long 类型,其签名为:J;Java端的 String 类型,其签名为:Ljava/lang/String;,注意这里的前缀大写字母 L,最后的分号也注意别漏,签名中包名与类名的分隔符用的是 / 符号。Java端的 short[] 类型,其签名为:[S;Java中的 String[] 类型,其签名为:[Ljava/lang/String;
而对于Java的方法类型签名,则需要其完整的参数类型与返回类型。如果没有参数列表,则直接用 () 来表示。为了清晰描述方法类型的签名机制,这里采用类Swift编程语言的类型表达方式,Kotlin也同样如此。比如,Java端的 () -> void 类型,其签名为:()V;Java端的 (int) -> void 类型,其签名为:(I)V;Java端的 (int, boolean) -> String 类型,其签名为:(IZ)Ljava/lang/String;;Java端的 (int, String, int) -> int[] 类型,其签名为:(ILjava/lang/String;I)[I

有了上述这些知识之后,我们下面来先写一个最最简单的JNI侧C代码的例子。我们可以将下面的代码粘贴到test.c中去:

#include <jni.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    return i + 100;
}

随后,我们可以在Activity中添加以下代码来观察结果:

        MyJNIClass jniCls = new MyJNIClass();
        int value = jniCls.nativeInstanceMethod(10);
        System.out.println("value = " + value);

这样,Java端调用JNI层的整个逻辑就完成了。


在JNI层访问Java类、属性以及方法

如果我们在JNI层去访问Java层的属性或方法,需要进行以下三步骤:

  1. 找到所要访问的属性或方法所属的类
  2. 获得属性或方法的ID
  3. 访问属性或调用方法

这些步骤所牵涉到的JNI层的接口都需要通过JNIEnv句柄去访问。

在JNI层获取Java类对象

要获得一个指定的类,需要通过FindClass这一接口。这个接口有两个参数,第一个参数就是env句柄;第二个参数是一个C字符串,用于描述该类的全名。与类型签名一样,类的全名在这里也需要将包名与类名写完整,并且其中的 . 符号需要用 / 符号来代替。FindClass这一接口所返回的对象则是一个jclass类型的对象。
比如,要获得一个Java层String的类,我们可以这么做:

jclass cls = (*env)->FindClass(env, "java/lang/String;");

那么我们要获取本例子中的MyJNIClass类,就需要这么做:

jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");


在JNI层获取属性ID

Java中有类属性与实例属性这两类,所以要获取属性ID的接口也有两套。下面先介绍一下获取实例属性ID的接口:

jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

这里,clazz参数就是我们之前获得的jclass对象。name参数指定了实例属性的名称。sig参数指定了该实例属性的类型签名。该接口返回一个jfieldID的对象,用于标识此特定的实例属性。
如果我们要获取MyJNIClass类的mField这一实例属性,可以这么做:

jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");

而要获得类属性的JNI接口是:

jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

我们可以看到,该接口的参数列表以及返回类型与GetFieldID都完全一样,指示接口名不一样而已。所以在用法上也完全一样。各位在获取属性的时候一定要注意,该属性是类属性还是实例属性,必须调用针对性的接口,不能用错。


访问属性

访问属性有两种模式,一种是读属性,还有一种就是写属性,类似于我们在Java中常用的getter方法与setter方法。无论是读属性还是写属性,根据该属性是类属性还是实例属性,也各分为两套。下面我们先讨论实例属性的读写方法。


访问对象实例属性

对象实例属性的读方法接口形式如下:

Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);

其中,<type>根据属性不同的类型而有所不同。obj参数就是在Java端调用此方法的对象引用。fieldID参数就是我们刚才获取到的此属性的ID。
下面列出官方给出的实例属性的读接口列表:

屏幕快照 2018-09-27 下午5.11.36.png

比如,在本demo中,如果我们要MyJNIClass对象中的mField实例方法,则可用这么用:

int value = (*env)->GetIntField(env, instance, fieldID);

实例属性的写方法接口形式如下:

void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

这里,<type>根据属性不同的类型而有所不同。obj参数就是在Java端调用此方法的对象引用。fieldID参数就是我们刚才获取到的此属性的ID。NativeType则是<type>所对应的JNI侧的类型。参数value就是要给该属性所设置的值。
下面列出官方给出的实例属性的写接口列表:

屏幕快照 2018-09-27 下午6.22.55.png

比如,在本demo中,如果我们要MyJNIClass对象中的mField实例方法,则可用这么用:

(*env)->SetIntField(env, instance, fieldID, 10);

上述代码就是将instance所引用对象的mField实例属性赋值为10。
根据上面所描述的对mField实例属性的读写方法,我们改造一下Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函数,各位可以运行一下看下效果:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField实例属性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 获取当前mField实例属性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后对mField实例属性进行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);

    return value + 10;
}

输入完之后,我们用ndk-build命令重新编译构建。随后,在Java端的Activity中填写以下代码:

        MyJNIClass jniCls = new MyJNIClass();
        int value = jniCls.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + jniCls.mField);

重新运行后我们就能看到新的结果了。


访问类属性

下面再来谈谈JNI访问Java层的类属性的接口。首先介绍读类属性的接口,其形式如下:

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);

这里,clazz参数是我们之前通过FindClass接口所获得的该类属性所属的类在JNI侧的对象。fieldID参数则是我们之前通过GetStaticFieldID接口所获得的指定类属性的ID。该接口返回的是一个NativeType类型,它是<type>在JNI侧所对应的一个类型。下面列出官方给出的关于此接口的所有函数列表:

屏幕快照 2018-09-27 下午7.54.20.png

比如,在本demo中,如果我们要MyJNIClass类的cField类属性,可以使用以下代码:

jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
int value = (*env)->GetStaticIntField(env, cls, fieldID);

上述代码片段中,cls就是通过FindClass接口所找到的MyJNIClass类在JNI侧所对应的类对象。

然后,写类属性的接口,其形式如下:

void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);

下面列出官方给出的关于此接口的所有函数列表:

屏幕快照 2018-09-27 下午8.17.35.png

比如,在本demo中,如果我们要MyJNIClass类的cField类属性,可以使用以下代码:

(*env)->SetStaticIntField(env, cls, fieldID, 10);

以上代码片段就是将MyJNIClass类的cField类属性赋值为10。
这么一来,我们可以再整合一下Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod,把类属性的读写也放进去:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField实例属性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 获取当前mField实例属性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后对mField实例属性进行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 获取类属性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 获取当前类属性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);

    return value + 100;
}

此外,在Activity中也可以把MyJNIClass类的cField类属性的值也进行输出,便于观察:

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);


JNI中的方法调用

在Java中,方法与属性类似也分为两大类:一类是类方法,另一类是实例方法。与访问属性的步骤类似,我们要调用一个方法之前,首先需要获得该方法的ID,随后再用该ID去做方法调用。我们下面先讨论调用Java对象的实例方法。


调用对象的实例方法

首先我们先看一下如何获得实例方法的ID,该接口的形式如下:

jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

该接口的参数与获取实例属性的一样,这里不再赘述。这个接口所返回的就是一个方法ID,以标识当前所获取的方法在JNI中的表示。
在本demo中,如果我们要获取MyJNIClassincreasedInt实例方法的ID,那么可以用以下方式:

    // 获取MyJNIClass类的increasedInt实例方法;其类型为:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");

获取了方法ID之后我们就可以去调用此方法了。调用实例方法的接口如下:

NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);

这其中的<type>与访问属性所用的<type一致,各位可以参考上面。这里后面的...是C语言的不定参数列表,表示对应于Java层实例方法的参数。如果Java层的实例方法没有参数,则不填任何东西,如果有参数,则依次对应填进去即可。该接口的返回类型对应于Java层实例方法的返回类型。
在本demo中,如果我们要调用MyJNIClassincreasedInt实例方法,那么可以用以下形式:

    // 调用this对象的increasedInt实例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);

至此,我可以再把Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函数补充完整:

JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField实例属性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 获取当前mField实例属性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后对mField实例属性进行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 获取类属性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 获取当前类属性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 获取MyJNIClass类的increasedInt实例方法;其类型为:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 调用this对象的increasedInt实例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);

    return value + 100;
}

各位可以查看整个app的运行结果。

获取类方法的方法ID的接口如下描述:

jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);

此方法的参数以及返回类型跟获取实例方法ID的接口一样。
在本例中,我们要获取MyJNIClass中的print类方法的ID,可以用以下方式:

    // 找到print类方法;其类型为:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");


调用类方法

在JNI侧对类方法的调用与对实例方法的调用形式差不多,采用以下接口:

NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);

其参数与返回类型与实例方法基本一样,除了第二个参数。这里第二个参数是调用当前Java类方法的类类型所在JNI侧的对象。
在本demo中,我们要调用MyJNIClass类的print方法如下所示:

/// 在JNI侧的打印函数
/// @param 指定的要打印输出的C字符串
static void JNIPrint(JNIEnv* env, const char *s)
{
    if(s == NULL)
        return;
    
    // 将C字符串转换为Java字符串对象
    jstring jstr = (*env)->NewStringUTF(env, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print类方法;其类型为:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");
    
    // 调用print这一类方法
    (*env)->CallStaticVoidMethod(env, cls, methodID, jstr);
}

我们在这里定义了一个可在JNI侧进行控制台打印输出的函数JNIPrint。我们可以在Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函数以及Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函数中均可调用此函数。下面我们将Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod函数补充完整,顺便再给出Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函数的实现。

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod这一成员方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param instance 表示对当前调用此实例方法的MyJNIClass对象实例的引用
/// @param i 调用此实例方法时所传入的整型参数
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField实例属性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 获取当前mField实例属性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后对mField实例属性进行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 获取类属性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 获取当前类属性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 获取MyJNIClass类的increasedInt实例方法;其类型为:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 调用this对象的increasedInt实例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);
    
    char strBuffer[128];
    sprintf(strBuffer, "native value is: %d\n", value);
    NativePrint(strBuffer);
    
    return value + 100;
}

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.nativeStaticMethod这一类方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param cls 指向在Java层调用此方法类方法的类类型
/// @param js 调用此类方法时所传入的Java字符串对象
JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    size_t length = strlen(cs);
    
    // 获取类属性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
    
    char buffer[128];
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    JNIPrint(env, buffer);
    (*env)->ReleaseStringUTFChars(env, js, cs);
}

下面我们再调整一下Activity中的Java代码,来观察修改后的结果输出:

        MyJNIClass.nativeStaticMethod("Hello, world!");

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);

这样一来,我们就把基本的JNI属性与方法的基本操作接口都讲解完了,下面我们将再介绍一下JNI侧对Java层的字符串操作以及数组操作方式。


字符串操作

众所周知,Java中所用的字符串编码格式为Unicode。由于Java诞生地非常早,1995年就发布了第一个版本,那个时候Unicode标准才刚启动没多久,所以就其编码格式名称而言一直把“Unicode”这个称呼沿用至今。但是对于现代化的Unicode标准早就引入了若干细分编码形式,最常用的是UTF-8、UTF-16以及UTF-32,尤其是前两者几乎被各大系统以及所有网页所支持。之前较老的Java版本支持UCS-2编码格式,现在默认情况下都直接使用了UTF-16编码,而UCS-2是UTF-16的一个子集,坐落于Unicode码点表中的基本多语言平面Basic Multilingual Plane)中。下面我们就证明一下当前就Java SE 8而言,默认采用的是UTF-16编码格式:

        String emojiStr = "😄";
        String fs = String.format("First code is: %1$4X, second code is: %2$4X",
                (int)emojiStr.charAt(0), (int)emojiStr.charAt(1));
        // 输出:First code is: D83D, second code is: DE04
        System.out.println(fs);

笔者用的系统是macOS 10.14 Mojave,Android 9.0系统的模拟器。上述的一个Emoji表情😄,其Unicode码点值为0x1F604,位于增补多语言平面Supplementary Multilingual Plane)之中。它所对应的UTF-16编码,高16位是0xD83D,低16位是0xDE04。所以各位对于Java所采用的字符串编码有一定了解之后,再来看JNI侧如何对Java层的字符串进行操作就可以有更好地把握了。

我们先介绍一下获取Java字符串内容。JNI API主要提供了获取UTF-16与UTF-8这两种字符编码的字符串的接口。要获取UTF-16字符编码的字符串使用GetStringChars()这个接口,其形式如下:

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

此接口第二个参数string就表示Java层传递过来的Java字符串对象。第三个参数isCopy是一个暗示性参数,可以为空。如果不空,那么我们可以先定义一个变量,指示当前获取字符串的形式是使用拷贝方式还是非拷贝方式。如果是JNI_TRUE,则指示使用拷贝方式;JNI_FALSE则指示使用非拷贝方式。但实际是否用拷贝方式,我们在调用完此接口之后还需要通过所传入的实参去查看。
该接口返回一个指向UTF-16编码格式的字符串,其中jchar类型在之前类型对照表中也列出来过,表示无符号16位整数类型。

由于此接口所返回的存放字符串的缓存是通过Java虚拟机来分配的,因此当我们使用完这组字符串之后需要调用ReleaseStringChars接口去释放。该接口声明如下:

void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);

其中,第二个参数为之前所获取的Java字符串对象;第三个参数为之前所返回的Java字符串缓存。

如果我们要获取一个Java字符串的长度,在JNI侧提供了GetStringLength这一接口,其声明如下:

jsize GetStringLength(JNIEnv *env, jstring string);

所以有了这个接口之后,即便我们当前使用的C语言不含Unicode库的,也能获取当前的字符串长度。

下面我们就来举一个综合性的例子:

JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16编码形式获取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 获取当前字符串的长度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后进行释放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 输出结果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
}

我们修改了Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod函数。随后,我们在Activity中将原本传入的字符串内容改为"😄",即可查看到运行结果。这里我们可以看到,一个😄Emoji字符占用2个字符。

下面我们介绍一下获取UTF-8编码格式字符串的接口。该接口形式如下:

const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);

该接口的三个参数与上面所描述的获得UTF-16字符串接口的三个参数一样。该接口返回一个存放标准的C字符串的缓存首地址。

同样,获取UTF-8字符串的接口也对应有一个释放字符串的接口,其形式如下:

void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);

我们在使用完GetStringUTFChars所创建的字符串缓存之后需要调用此接口进行释放。

JNI侧也提供了GetStringUTFLength接口用于获取指定Java字符串以UTF-8编码格式所表示的字符串的长度。其形式如下:

jsize GetStringUTFLength(JNIEnv *env, jstring string);

当然,如果字符串长度不太长的话,我们用C语言标准库的strlen函数在性能上会更高些。

下面我们来举一个综合性的例子:

JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16编码形式获取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 获取当前字符串的长度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后进行释放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 输出结果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
    
    // 获取UTF-8字符串
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    
    // 获取当前以UTF-8编码所表示的字符串的长度
    length = (*env)->GetStringUTFLength(env, js);
    
    // 组织所要打印c输出的字符串
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    
    // 释放UTF-8字符串缓存
    (*env)->ReleaseStringUTFChars(env, js, cs);
    
    // 打印输出:The input string is: 😄, and the length is: 4
    JNIPrint(env, buffer);
    
    // 获取类属性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
}

我们可以运行一下程序查看结果。


下面我们来谈谈如何在JNI层将一个UTF-16字符数组来创建一个Java字符串对象。JNI API提供了NewString接口来实现这个操作,其形式如下:

jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);

其中,第二个参数unicodeChars表示该我们所传入的UTF-16字符数组的缓存首地址;第三个参数len用于指示该缓存中有多少个字符参与构建。

当然,JNI层也提供了接口,用于将一个指定的UTF-8字符数组来创建一个Java字符串对象。其形式如下:

jstring NewStringUTF(JNIEnv *env, const char *bytes);

这个接口没有第三个参数用于指定字符串的长度,而是用\0字符表示该字符串的结束符。我们可以回顾JNIPrint函数中对此接口的使用。


对Java数组对象的操作

我们首先介绍获取Java数组对象的元素个数的接口。其形式如下:

jsize GetArrayLength(JNIEnv *env, jarray array);

这个接口非常简单,我们不做过多介绍。

下面介绍获取Java数组对象的元素。这里根据Java数组元素的不同类型,其接口形式也有所不同。获取元素类型为对象类型的接口是GetObjectArrayElement,其形式如下:

jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

这里第二个参数array是Java数组对象。第三个参数index指定所要获取数组元素对象的索引。

而对于获取数组元素为基本类型的数组,可以通过两种接口进行获取,第一种是Get<PrimitiveType>ArrayElements接口,其形式如下:

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

此接口的参数与获取字符串的参数差不多。下面列出<type>以及相应的NativeType的列表。

屏幕快照 2018-10-02 下午3.32.28.png

该接口所返回的基本类型的元素的缓存首地址也是受JVM管理的。因此,当我们用完此缓存元素之后需要调用Release<PrimitiveType>ArrayElements接口进行释放。此接口形式如下:

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

第二个参数array表示对应的Java数组对象。第三个参数elems是上面Get接口所返回的缓存首地址。第四个参数mode表示释放模式,它目前有三种值:

  1. 0表示将elems中存放的元素拷贝回array数组对象,然后将所分配的elems缓存释放掉。
  2. JNI_COMMIT表示将elems中存放的元素拷贝回array数组对象,但不释放elems缓存。
  3. JNI_ABORT表示不拷贝回当前elems中存放的元素,而直接释放elems缓存。
    此接口的所有具体接口名列表如下所示:
屏幕快照 2018-10-02 下午3.41.41.png

还有一个接口是Get<PrimitiveType>ArrayRegion,其形式如下:

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

这个第二个参数array表示Java数组对象。第三个参数start指示从第几个元素开始获取。第四个参数len指示获取多少个元素。第五个参数buf指向存放数组元素的缓存。由于此接口是由程序员自己负责分配存放数组元素的空间,因此JNI API不提供相应的释放接口。

下面我们介绍一下Java数组元素的设置接口。与获取数组元素的接口类似,Java数组元素的设置接口也是根据不同的元素类型而有所不同。如果要设置元素类型为Java对象的数组对象,则使用SetObjectArrayElement接口。该接口形式如下:

void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

该接口第二个参数array用于指定所要操作的Java数组对象。第三个参数index用于指定设置元素对象的索引。而第四个参数value就是所要设置的对象。

而要设置数组元素类型为基本类型的接口就只有一种,即Set<PrimitiveType>ArrayRegion。其形式如下:

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

其中,第二个参数array为Java层的数组对象。第三个参数start表示要对array数组进行元素设置的起始索引。第四个参数len指定了所要设置元素的个数。第五个参数buf则是在JNI侧准备好的要对array进行设置的元素缓存。下面列出所有具体<PrimitiveType>的相关接口:

屏幕快照 2018-10-03 下午2.23.15.png

下面我们将对数组操作举一个综合性的例子。大家先在Java端的MyJNIClass类中添加以下方法:

    /**
     * 声明一个类方法,它在JNI中实现
     * @param array 一个int数组对象
     * @return 一个int数组对象
     */
    public static native int[] arrayOpMethod(int[] array);

随后在JNI侧的test.c中添加以下函数:

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.arrayOpMethod这一类方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param cls 指向在Java层调用此方法类方法的类类型
/// @param array 调用此类方法时所传入的Java端的int数组对象
JNIEXPORT jintArray JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_arrayOpMethod(JNIEnv* env, jclass cls, jintArray array)
{
    // 获取Java数组的长度
    const size_t arrayCount = (*env)->GetArrayLength(env, array);
    
    jboolean isCopy = false;
    
    // 获取Java数组中的所有元素
    int *cArray = (*env)->GetIntArrayElements(env, array, &isCopy);
    
    // 我们将该数组中的所有元素的值做加1操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i]++;
    
    // 将更新后的数组替换原Java数组对象中的元素
    (*env)->SetIntArrayRegion(env, array, 0, arrayCount, cArray);
    
    // 新建一个新的数组作为后面返回的结果数组
    jintArray resultArray = (*env)->NewIntArray(env, arrayCount);
    
    // 我们将之前数组中的所有元素的值再做乘以2的操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i] *= 2;
    
    // 将更新后的数组替换resultArray这一Java数组对象中的元素
    (*env)->SetIntArrayRegion(env, resultArray, 0, arrayCount, cArray);
    
    (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT);
    
    return resultArray;
}

最后,我们在Activity层添加以下代码即可观察到运行结果:

        int[] array = {1, 2, 3};
        int[] dstArray = MyJNIClass.arrayOpMethod(array);
        String output = String.format("array [0] = %1$d, [1] = %2$d, [2] = %3$d",
                array[0], array[1], array[2]);
        System.out.println(output);

        output = String.format("dstArray [0] = %1$d, [1] = %2$d, [2] = %3$d",
                dstArray[0], dstArray[1], dstArray[2]);
        System.out.println(output);

OK!下面我们较完整的展示一下我们较完整的项目代码。

首先列出完整的test.c源文件:

#include <jni.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <cpu-features.h>

/// 全局JNI环境指针变量
JNIEnv* gEnv;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    jint result = -1;
    
    if((*vm)->GetEnv(vm, (void**)&gEnv, JNI_VERSION_1_6) != JNI_OK)
        return -1;
    
    assert(gEnv != NULL);
    
    return JNI_VERSION_1_6;
}

/// 在JNI侧的打印函数
/// @param 指定的要打印输出的C字符串
static void NativePrint(const char *s)
{
    if(s == NULL)
        return;
    
    // 将C字符串转换为Java字符串对象
    jstring jstr = (*gEnv)->NewStringUTF(gEnv, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*gEnv)->FindClass(gEnv, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print类方法;其类型为:(String) -> void
    jmethodID methodID = (*gEnv)->GetStaticMethodID(gEnv, cls, "print", "(Ljava/lang/String;)V");
    
    // 调用print这一类方法
    (*gEnv)->CallStaticVoidMethod(gEnv, cls, methodID, jstr);
}

/// 在JNI侧的打印函数
/// @param 指定的要打印输出的C字符串
static void JNIPrint(JNIEnv* env, const char *s)
{
    if(s == NULL)
        return;
    
    // 将C字符串转换为Java字符串对象
    jstring jstr = (*env)->NewStringUTF(env, s);
    if(jstr == NULL)
        return;
    
    // 找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到print类方法;其类型为:(String) -> void
    jmethodID methodID = (*env)->GetStaticMethodID(env, cls, "print", "(Ljava/lang/String;)V");
    
    // 调用print这一类方法
    (*env)->CallStaticVoidMethod(env, cls, methodID, jstr);
}

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.nativeInstanceMethod这一成员方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param instance 表示对当前调用此实例方法的MyJNIClass对象实例的引用
/// @param i 调用此实例方法时所传入的整型参数
JNIEXPORT jint JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeInstanceMethod(JNIEnv* env,
                                                jobject instance, jint i)
{
    // 先找到com.test.zenny_chen.test.MyJNIClass类
    jclass cls = (*env)->FindClass(env, "com/test/zenny_chen/test/MyJNIClass");
    
    // 找到mField实例属性的ID
    jfieldID fieldID = (*env)->GetFieldID(env, cls, "mField", "I");
    
    // 获取当前mField实例属性的值
    int value = (*env)->GetIntField(env, instance, fieldID);
    
    // 最后对mField实例属性进行修改
    (*env)->SetIntField(env, instance, fieldID, value + i);
    
    // 获取类属性cField的ID
    fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 获取当前类属性cField的值
    value = (*env)->GetStaticIntField(env, cls, fieldID);
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, value - i);
    
    // 获取MyJNIClass类的increasedInt实例方法;其类型为:(int) -> int
    jmethodID methodID = (*env)->GetMethodID(env, cls, "increasedInt", "(I)I");
    
    // 调用this对象的increasedInt实例方法
    value = (*env)->CallIntMethod(env, instance, methodID, i);
    
    char strBuffer[128];
    sprintf(strBuffer, "native value is: %d\n", value);
    NativePrint(strBuffer);
    
    return value + 100;
}

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.nativeStaticMethod这一类方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param cls 指向在Java层调用此方法类方法的类类型
/// @param js 调用此类方法时所传入的Java字符串对象
JNIEXPORT void JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_nativeStaticMethod(JNIEnv* env, jclass cls, jstring js)
{
    jboolean isCopy = false;
    // 以UTF-16编码形式获取Java字符串
    const jchar *utf16Str = (*env)->GetStringChars(env, js, &isCopy);
    
    // 获取当前字符串的长度
    size_t length = (*env)->GetStringLength(env, js);
    
    char buffer[128];
    sprintf(buffer,
            "The string length is: %zu, first code: %.4X, second code: %.4X\n",
            length, utf16Str[0], utf16Str[1]);
    
    // 用完之后进行释放
    (*env)->ReleaseStringChars(env, js, utf16Str);
    
    // 输出结果:The string length is: 2, first code: D83D, second code: DE04
    JNIPrint(env, buffer);
    
    // 获取UTF-8字符串
    const char *cs = (*env)->GetStringUTFChars(env, js, &isCopy);
    
    // 获取当前以UTF-8编码所表示的字符串的长度
    length = (*env)->GetStringUTFLength(env, js);
    
    // 组织所要打印c输出的字符串
    sprintf(buffer, "The input string is: %s, and the length is: %zu\n", cs, length);
    
    // 释放UTF-8字符串缓存
    (*env)->ReleaseStringUTFChars(env, js, cs);
    
    // 打印输出:The input string is: 😄, and the length is: 4
    JNIPrint(env, buffer);
    
    // 获取类属性cField的ID
    jfieldID fieldID = (*env)->GetStaticFieldID(env, cls, "cField", "I");
    
    // 最后对类属性cField的值进行修改
    (*env)->SetStaticIntField(env, cls, fieldID, 30);
}

/// 这个函数与com.test.zenny_chen.test.MyJNIClass.arrayOpMethod这一类方法对应
/// @param env 表示当前JVM所传入的JNI环境
/// @param cls 指向在Java层调用此方法类方法的类类型
/// @param array 调用此类方法时所传入的Java端的int数组对象
JNIEXPORT jintArray JNICALL
Java_com_test_zenny_1chen_test_MyJNIClass_arrayOpMethod(JNIEnv* env, jclass cls, jintArray array)
{
    // 获取Java数组的长度
    const size_t arrayCount = (*env)->GetArrayLength(env, array);
    
    jboolean isCopy = false;
    
    // 获取Java数组中的所有元素
    int *cArray = (*env)->GetIntArrayElements(env, array, &isCopy);
    
    // 我们将该数组中的所有元素的值做加1操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i]++;
    
    // 将更新后的数组替换原Java数组对象中的元素
    (*env)->SetIntArrayRegion(env, array, 0, arrayCount, cArray);
    
    // 新建一个新的数组作为后面返回的结果数组
    jintArray resultArray = (*env)->NewIntArray(env, arrayCount);
    
    // 我们将之前数组中的所有元素的值再做乘以2的操作
    for(int i = 0; i < arrayCount; i++)
        cArray[i] *= 2;
    
    // 将更新后的数组替换resultArray这一Java数组对象中的元素
    (*env)->SetIntArrayRegion(env, resultArray, 0, arrayCount, cArray);
    
    (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT);
    
    return resultArray;
}

随后列出MyJNIClass.java源文件:

package com.test.zenny_chen.test;

/**
 * 我们定制的JNI测试类
 */
public class MyJNIClass {

    static {
        // 在访问MyJNIClass时,加载jni_test动态库
        System.loadLibrary("jni_test");
    }

    /**
     * 声明一个实例方法,它在JNI中实现
     * @param i 指定一个整数参数
     * @return 某一整数
     */
    public native int nativeInstanceMethod(int i);

    /**
     * 声明一个类方法,它在JNI中实现
     * @param s 指定一个字符串
     */
    public native static void nativeStaticMethod(String s);

    /**
     * 声明一个类方法,它在JNI中实现
     * @param array 一个int数组对象
     * @return 一个int数组对象
     */
    public static native int[] arrayOpMethod(int[] array);

    /**
     * 当前类的实例方法,将指定参数的值加1然后返回
     * @param i 指定的一个整数
     * @return 参数加1后的值
     */
    public int increasedInt(int i) {
        return i + 1;
    }

    /**
     * 当前类的类方法,用于实现打印输出指定的字符串
     * @param s 指定的字符串内容
     */
    public static void print(String s) {
        System.out.println(s);
    }

    /** 当前类的一个实例属性 */
    public int mField;

    /** 当前类的一个类属性 */
    public static int cField;
}

最后列出Activity端的测试代码:

        MyJNIClass.nativeStaticMethod("😄");

        MyJNIClass obj = new MyJNIClass();
        int value = obj.nativeInstanceMethod(10);
        System.out.println("value = " + value);
        System.out.println("mField = " + obj.mField);
        System.out.println("cField = " + MyJNIClass.cField);

        int[] array = {1, 2, 3};
        int[] dstArray = MyJNIClass.arrayOpMethod(array);
        String output = String.format("array [0] = %1$d, [1] = %2$d, [2] = %3$d",
                array[0], array[1], array[2]);
        System.out.println(output);

        output = String.format("dstArray [0] = %1$d, [1] = %2$d, [2] = %3$d",
                dstArray[0], dstArray[1], dstArray[2]);
        System.out.println(output);

关于上述代码,有一个JNI_OnLoad函数我们没有讲解到。这个函数用于保存全局JNI环境所提供的。我们有了JNI全局环境句柄之后就不需要依赖每次通过调用的Java层native方法所传递过来的env参数,而可直接使用JNI环境句柄来访问各个类以及属性与方法了。这样对公共库的构建而言显然要方便很多。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,577评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,484评论 2 59
  • 从本书中,提炼出获得幸福的小习惯,希望可以为我们的生活带来小幸运。 1.每晚睡前10分钟采集三个幸福时刻,记录他们...
    懿拾阅读 280评论 2 0
  • 上午闲来无事便点开听了前段时间在微信十点读书栏目订阅的《听蒋勋讲中国文学》,听蒋大师用他那带有磁性浑厚的声音把悠...
    五月的荷阅读 270评论 4 12
  • 要按照规定来 做事心里要有个大概的规划 今天本来排好了教室 因为自己大意疏忽差点出了乱子 心情好看什么都好 心情...
    都被注册了2阅读 137评论 2 1