NDK系列:JNI基础

·  阅读 430

通过本篇文章,你应该达成以下目标:

  1. 了解什么是JNI,了解JNI与Java的关系,了解JNI与JVM的关系。
  2. 了解Java如何调用JNI,如何通过c/c++实现Java的native方法。
  3. 了解如何在JNI内调用Java的方法。

macos c/c++工程构建参考:macos+vscode+cmake构建c/c++工程

1 基础

1.1 什么是JNI

JNI是Java Native Interface的缩写,通过使用Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。

本篇文章主要介绍Java如何通过JNI与C或者C++进行交互。

1.2 JNI数据类型

JNI在Java与C/C++之间起中转作用,考虑到Java与C/C++语言之间的数据类型不统一,JNI有一套自己的数据类型,分别与Java和C/C++进行对应,是一套中间数据类型。

JavaJNI描述
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloatsigned 32 bits
doublejdoublesigned 64 bits
Classjclassclass类对象
Stringjstring字符串对象
Objectjobject任何Java对象
byte[]jbyteArraybyte数组

1.3 JNI上下文

JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针,JNI函数可以对Java侧的代码进行操作。例如,创建Java类的对象,调用Java对象的方法,获取对象中的属性等。JNIEnv的指针会被传入到JNI侧的native方法的实现函数中,来对Java端的代码进行操作。例如:

jstring f1(JNIEnv *env, jclass jobj){
    return (*env)->NewStringUTF(env,"Hi Java");
}
复制代码

区分jobject与jclass:在JNI侧声明Java native方法的实现的时候,会有两个默认形参(除开native方法自己的传入参数),分别是JNIEnv指针,另外一个是jobject/jclass,这两个的区别在于: jobject:如果Java侧的native方法是非静态的,那么传给JNI的第二个参数是类的对象,所有类的对象在JNI侧都是jobject类型。jclass:如果Java侧的native方法是静态的,那么传给JNI的第二个参数是类的运行时类,所有运行时类在JNI侧都是jclass类型。

JNIEXPORT和JNICALL:JNIEXPORT和JNICALL是两个关键字,表明这个方法是JNI方法。

JNIEXPORT jint JNICALL Java_register_staticRegister_StaticRegister_func
(JNIEnv *env, jclass jobj,jintArray arr){
    int len = (*env)->GetArrayLength(env,arr);
    return len;
}
复制代码

1.4 Java签名

查看Java字段和方法的签名

javap命令javap -s -p JniTest.class

image.png

Java签名规范

在JNI标准中,Java方法签名有以下约定。

数据类型签名
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
voidV
objectL开头,然后以‘/’分隔包的完整类型,最后加上‘;’(分号)。
比如:
String签名是Ljava/lang/String;
Array[开头,后面加上元素类型的签名。
比如:
int[]的签名是[I;int[][]的签名是[[I;
object[]签名是[Ljava/lang/Object;。

举例

//Java:
import java.util.Data;
public class Hello {
    public int property;
    public int function(int fu,Date date,int[] arr) {
        return 0;
    }
    public native void test();
}
复制代码
//C++:
JNIEXPORT void JNICALL Java_Hello_test(JNIEnv *env, jobject jobj){
    jclass hello_clz = (*env) -> GetObjectClass(env, jobj);
    jfieldID fieldID_prop = (*env) -> GetFieldID(env, hello_claz, "property", "I");
    jmethodID methodID_func = 
        (*env) -> GetMethodID(env, hello_clz, "function", "(ILjava/util/Date;[I)I");
    (*env) -> CallIntMethod(env, jobj, methodID_func, 0L, NULL, NULL);
}
复制代码

image.png

2 Java调用JNI

Java定义native方法后,JNI具体实现native方法,将Java定义与JNI实现进行关联后,Java直接调用native方法即可使用到JNI的实现。上述的关联操作也被称为注册,换句话说,注册是指将JNI的函数与Java native方法关联在一起的必要操作,而注册分为静态注册动态注册

2.1 静态注册

步骤:

  • 编写Java类,比如Hello.java;
package learn.java.jni;

public class Hello {

    public static native String jniSaysHello();

    public static String javaSaysHello() {
        return "Java: Hello JNI, this is Java!";
    }

    public static void main(String[] args) {
        System.out.println(javaSaysHello());
        System.out.println(jniSaysHello());
    }
}
复制代码
  • 在.java源文件目录下,命令行输入“javac Hello.java”生成Hello.class文件;
  • 在Hello.class所属包所在目录下,命令行执行“javah learn.java.Hello”(完整类名无后缀),在包所在目录生成learn_java_jni_Hello.h头文件;
  • 如果是JDK 1.8或以上,以上步骤可简化为一步:在Hello.java目录下,命令行执行 javac -h . Hello.java,直接在当前目录下得到.class文件和.h文件;

image.png

  • 创建C/C++项目并拷贝learn_java_jni_Hello.h文件到项目目录;
  • 在C/C++项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在JAVA_HOME/include目录下,将这两个头文件拷贝到c/c++项目目录;
  • 在learn_java_jni_Hello.h中修改#include <jni.h>为#include "jni.h"
  • 可以看到,learn_java_jni_Hello.h文件里面就是一个Java方面native方法的一个JNI声明,格式为JNIEXPORT 关键字一 jstring 返回值的JNI类型 JNICALL 关键字二 Java_learn_java_jni_Hello_jniSaysHello Java_全类名_方法名 (JNIEnv *, jclass);如下;
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
/* Header for class learn_java_jni_Hello */

#ifndef _Included_learn_java_jni_Hello
#define _Included_learn_java_jni_Hello
#ifdef __cplusplus
extern "C"
{
#endif
  /*
 * Class:     learn_java_jni_Hello
 * Method:    jniSaysHello
 * Signature: ()Ljava/lang/String;
 */
  JNIEXPORT jstring JNICALL Java_learn_java_jni_Hello_jniSaysHello(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif
复制代码
  • 编写头文件learn_java_jni_Hello.h对应的learn_java_jni_Hello.c源文件,拷贝并实现learn_java_jni_Hello.h下的函数,如下:
#include "learn_java_jni_Hello.h"

JNIEXPORT jstring JNICALL Java_learn_java_jni_Hello_jniSaysHello(JNIEnv *env, jclass jc)
{
    return (*env)->NewStringUTF(env, "JNI: Hello Java, this is JNI!");
}
复制代码
  • 编写CMakeLists.txt文件,这是库的配置文件。最重要的是最后两个add_library(),其余的都是自动生成的。add_library()声明库的名字、类型和包含的.c&.h文件。SHARED关键字表示创建的库是动态库.dll(windows)/.dylib(mac),STATIC关键字表示创建的库是静态库.a(windows&mac)。注意:库本身有动态库和静态库之分,Java native方法也有静态注册和动态注册之分,二者没有关系。 这里将动态库命名为JniSaysHello

动态链接库与静态链接库的区别

cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)

include(CTest)
enable_testing()

add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码
  • 此时C/C++项目结构如下图,Build Project生成动态链接库,得到libJniSaysHello.dylib

image.png

image.png

image.png

  • 在Java侧,Hello.Java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:

image.png

package learn.java.jni;

public class Hello {

    static {
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
    }

    public static native String jniSaysHello();

    public static String javaSaysHello() {
        return "Java: Hello JNI, this is Java!";
    }

    public static void main(String[] args) {
        System.out.println(javaSaysHello());
        System.out.println(jniSaysHello());
    }
}
复制代码
  • 在Java侧运行,得到如下效果,Java成功调用了dylib中的方法,静态注册成功。

image.png

  • 上述过程,JNI使用Java_PACKAGENAME_CLASSNAME_METHODNAME与Java侧的方法进行匹配,这种方式是静态注册

2.2 动态注册

步骤:

  • 编写Java类,比如Hello.java,如下;
package learn.java.jni;

public class Hello {

    static {
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
    }

    private static final int[] array = {1, 2, 3};

    public static native String jniSaysHello();

    private static native String jniHowdy();

    protected static native String jniCalculateArrayLength(int[] array);

    public static String javaSaysHello() {
        return "Java: Hello JNI, this is Java!";
    }

    public static String javaHowdy() {
        return "Java: How are you doing?";
    }

    public static String javaDoNotKnowArrayLenth() {
        return "Java: Please help me calculate length of this array.";
    }

    public static void main(String[] args) {
        // static register
        System.out.println(javaSaysHello());
        System.out.println(jniSaysHello());
        // dynamic register
        System.out.println(javaHowdy());
        System.out.println(jniHowdy());
        System.out.println(javaDoNotKnowArrayLenth());
        System.out.println(jniCalculateArrayLength(array));
    }
}
复制代码
  • 在c/c++项目中添加jni.h头文件和jni_md.h头文件,这两个头文件是JDK自带的,在JAVA_HOME/include目录下,将这两个头文件拷贝到c/c++项目目录;
  • 新建C/C++源文件dynamic_register.c。在该.c文件中,实现两个函数,这两个函数将是native方法在JNI的实现,如下:
#include "jni.h"

jstring howdy(JNIEnv *env, jclass jcls)
{
    return (*env)->NewStringUTF(env, "JNI: I am fine, how are you?");
}

jstring calculate_array_len(JNIEnv *env, jclass jcls, jintArray arr)
{
    int len = (*env)->GetArrayLength(env, arr);
    char lenStr[100];
    sprintf(lenStr, "JNI: Length of this array is %d.", len);
    return (*env)->NewStringUTF(env, lenStr);
}
复制代码
  • 到目前,Java的jniHowdy方法,jniCalculateArrayLength方法与c/c++howdy函数,calculate_array_len还没有任何关联,我们需要手动管理关联
  • 继续在dynamic_register.c内,新建一个以JNINativeMethod结构体为元素的数组,如下:
static const JNINativeMethod mMethods[] = {
    {"jniHowdy", "()Ljava/lang/String;", (jstring *)howdy},
    {"jniCalculateArrayLength", "([I)Ljava/lang/String;", (jstring *)calculate_array_len},
};
复制代码
  • 以上数组中每一个元素,都是JNI侧实现方法与Java侧native方法的关联,前两个是Java侧native方法的描述,最后一个是JNI侧函数实现的描述,格式为:
{"Java侧的native方法名","方法的签名",函数指针}
复制代码
  • 然后,我们需要实现jni.h中的JNI_OnLoad()方法,该方法的实现方法是一个模板,如下:
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv *env = NULL;
    //获得 JNIEnv
    int r = (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4);
    if (r != JNI_OK)
    {
        return -1;
    }
    jclass mainActivityCls =
        (*env)->FindClass(env, "learn/java/jni/Hello");
    // 最后参数是需要注册的native方法的个数,如果小于0则注册失败。
    r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, 2);
    if (r != JNI_OK)
    {
        return -1;
    }
    return JNI_VERSION_1_4;
}
复制代码
  • 注意!第一:以上FindClass(env, "learn/java/jni/Hello")中的字符串是Java侧Hello类的全类名,注意此处的反斜杠/;第二:RegisterNatives(env, mainActivityCls, mMethods, 2)中的最后一个整型参数是需要动态注册的方法个数,手动添加注册或者删除注册都需要对应变化,当然可以直接把mMethod[]的长度传进去,一劳永逸,如下:
int cnt = sizeof(mMethods)/ sizeof(mMethods[0]);
r = (*env)->RegisterNatives(env, mainActivityCls, mMethods, cnt);
复制代码
  • 然后,配置CMakeList.txt,新增一个add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c),然后Cmake: Build
cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)

include(CTest)
enable_testing()

# static register
add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)

# dynamic register
add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码
  • 在Hello.java源文件中,添加静态代码块,使用System.load()方法加载该动态链接库,如下:
package learn.java.jni;

public class Hello {

    static {
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniSaysHello.dylib");
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniHowdyAndCalculaterArrayLen.dylib");
    }

    private static final int[] array = {1, 2, 3};

    public static native String jniSaysHello();

    private static native String jniHowdy();

    protected static native String jniCalculateArrayLength(int[] array);

    public static String javaSaysHello() {
        return "Java: Hello JNI, this is Java!";
    }

    public static String javaHowdy() {
        return "Java: How are you doing?";
    }

    public static String javaDoNotKnowArrayLength() {
        return "Java: Please help me calculate length of this array.";
    }

    public static void main(String[] args) {
        // static register
        System.out.println(javaSaysHello());
        System.out.println(jniSaysHello());
        // dynamic register
        System.out.println(javaHowdy());
        System.out.println(jniHowdy());
        System.out.println(javaDoNotKnowArrayLength());
        System.out.println(jniCalculateArrayLength(array));
    }
}
复制代码
  • Java侧运行,效果如下:

image.png

  • 动态注册成功。

system.load()与system.loadLibrary()的区别:

System.load()

System.load()参数必须为库文件的绝对路径, 例如: System.load("C:\Documents\TestJNI.dll"); //Windows System.load("/usr/lib/TestJNI.dylib"); //mac

System.loadLibrary()

System.loadLibrary()参数为库文件名,不包含库文件的扩展名。 System.loadLibrary("TestJNI"); //加载Windows下的TestJNI.dll本地库 System.loadLibrary("TestJNI"); //加载Linux下的libTestJNI.dylib本地库

注意:TestJNI.dll 或 libTestJNI.dylib 必须是在JVM属性java.library.path所指向的路径中。 loadLibary需要配置当前项目的java.library.path路径

3 JNI调用Java

3.1 JNI侧获取Java类运行时类

区分FindClass()与GetObjectClass()

  • FindClass():JNI侧函数,相当于Java中的Class.forName(),通过Java侧的全类名获取到JNI可用的jclass变量
jclass jclz = 
    (*env)->FindClass(env,"packageName/ClassName");
复制代码
  • GetObjectClass():JNI侧函数,相当于Java侧的object.getClass(),通过从Java传到JNI侧的jobject对象类似于反射获取到该jobject对应的jclass变量
jclass jclz = (*env)->GetObjectClass(env,jobj);
复制代码

3.2 JNI调用Java

调用字段

Java侧的类的字段分为非静态字段与静态字段,JNI有对应的GetXxx()/SetXxx()GetStaticXxx()/SetStaticXxx()方法,分为以下几个步骤:

  1. JNI注册:在JNI侧注册Java侧的native方法;
  2. 获取jclass:如果注册的是static的native方法,则在JNI实现中直接传入了jclass变量,可直接跳到下一个步骤;如果注册的是非static的native方法,则在JNI实现中传入了jobject变量,需要通过GetObjectClass()方法获取到jclass;
  3. 获取jfieldID:调用GetFieldID()/GetStaticFieldID();
  4. 获取到字段:调用GetXxxField()/GetStaticXxxField();
  5. 操作字段:调用SetXxxField()/SetStaticXxxField()。
// java
package learn.java.jni;

public class JNICallJavaField {

    static {
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniCallJavaField.dylib");
    }

    public int field1 = 1;

    public static int field2 = 8;

    public native void func1();//不传入field1,但是希望JNI能修改field1

    public static native void func2();//不传入field2,但是希望JNI能修改field2

    public static void main(String[] args) {
        JNICallJavaField jniCallJavaField = new JNICallJavaField();
        jniCallJavaField.func1();
        jniCallJavaField.func2();
        System.out.println(jniCallJavaField.field1);
        System.out.println(jniCallJavaField.field2);
    }
}
复制代码
// c/c++
#include "learn_java_jni_JNICallJavaField.h"

/*
 * Class:     learn_java_jni_JNICallJava
 * Method:    func1
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_learn_java_jni_JNICallJavaField_func1(JNIEnv *env, jobject jobj)
{
    jclass jclz = (*env)->GetObjectClass(env, jobj);
    jfieldID jfieldId = (*env)->GetFieldID(env, jclz, "field1", "I");
    jint field1 = (*env)->GetIntField(env, jobj, jfieldId);
    (*env)->SetIntField(env, jobj, jfieldId, field1 + 10000);
}

/*
 * Class:     learn_java_jni_JNICallJava
 * Method:    func2
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_learn_java_jni_JNICallJavaField_func2(JNIEnv *env, jclass jclz)
{
    jfieldID jfieldId = (*env)->GetStaticFieldID(env, jclz, "field2", "I");
    jint field2 = (*env)->GetStaticIntField(env, jclz, jfieldId);
    (*env)->SetStaticIntField(env, jclz, jfieldId, field2 + 10000);
}
复制代码
# CMakeList.txt
cmake_minimum_required(VERSION 3.0.0)
project(jnicppdemo VERSION 0.1.0)

include(CTest)
enable_testing()

# static register
add_library(JniSaysHello SHARED learn_java_jni_Hello.c learn_java_jni_Hello.h)

# dynamic register
add_library(JniHowdyAndCalculaterArrayLen SHARED dynamic_register.c)

# jni call java field
add_library(JniCallJavaField SHARED learn_java_jni_JNICallJavaField.c)

set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)
复制代码

Java侧运行效果:

image.png

调用方法

注意:反射无法反射抽象方法,也没有必要反射抽象方法

在JNI看来,Java侧的方法分为:构造方法/静态方法/非静态方法。

Java侧代码

在进入JNI侧调用Java方法之前,先展示Java侧代码:

// 一个与native方法无关的类,包含一个空参构造方法,一个非静态方法,一个静态方法。
package learn.java.jni;

public class JNICallJavaMethod {

    public JNICallJavaMethod(){
        System.out.println("Java: I am constructor.");
    }
    
    public int func(int i){
        System.out.println("Java: func is called.");
        return i+100;
    }
    
    public static int staticFunc(int[] a){
        System.out.println("Java: staticFunc is called.");
        int sum = 0;
        for(int i=0;i<a.length;i++){
            sum += a[i];
        }
        return sum;
    }
}
复制代码
// 一个包含native方法的类,还包含一个非静态方法。
package learn.java.jni;

public class JNICallJavaMethodNative {

    static {
        System.load("/Users/tongbo/Documents/Projects/jni/jnic/jnicppdemo/build/libJniCallJavaMethod.dylib");
    }

    public native int func1(int i);

    public String func2() {
        System.out.println("Java: I am func2 in the same class with the native func1.");
        return "Java: Hi JNI.";
    }

    public static void main(String[] args) throws InterruptedException {
        JNICallJavaMethodNative jniCallJavaMethodNative = new JNICallJavaMethodNative();
        int res = jniCallJavaMethodNative.func1(8);
        /**
         * 测试JNI侧的控制台打印与Java侧的控制台打印之间的顺序关系:
         * 结果发现,JNI侧的控制台打印一定是在Java之后,sleep也没用。
         */
        Thread.sleep(2000);
        System.out.println("Java: Result of native func1 is " + res);
    }
}
复制代码

如上述代码所示,我们有两个类,第一个类JNICallJavaMethod与native方法无直接关系,里面提供了构造方法,非静态方法,静态方法,供JNI调用。第二个类JNICallJavaMethodNative包含了native方法的声明,还包含了一个非静态方法供JNI调用;同时第二个类JNICallJavaMethodNative提供psvm作为整个程序的运行入口,程序开始运行后,在Java侧仅调用了native方法,得到了以下的结果:

image.png

JNI侧代码

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

JNIEXPORT jint JNICALL Java_learn_java_jni_JNICallJavaMethodNative_func1(JNIEnv *env, jobject jobj, jint ji)
{

    // JNI侧创建一个int array
    jintArray jArr = (*env)->NewIntArray(env, 4);
    jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);
    arr[0] = 0;
    arr[1] = 10;
    arr[2] = 20;
    arr[3] = 30;
    (*env)->ReleaseIntArrayElements(env, jArr, arr, 0);

    // 调用另一个类的构造函数
    jclass jclz1 = NULL;
    jclz1 = (*env)->FindClass(env, "learn/java/jni/JNICallJavaMethod");
    if (jclz1 == NULL)
    {
        printf("JNI: jclz is NULL.");
        return ji;
    }
    jmethodID jmethodId1 = NULL;
    jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
    if (jmethodId1 == NULL)
    {
        printf("JNI: jmethodId1 is NULL.");
        return ji;
    }
    jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
    (*env)->CallVoidMethod(env, jobj1, jmethodId1);

    // 调用另一个类的非静态方法
    jmethodID jmethodId2 = NULL;
    jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
    if (jmethodId2 == NULL)
    {
        return ji;
    }
    jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
    jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
    printf("JNI: func returns %d.\n", i1);

    // 调用另一个类的静态方法
    jmethodID jmethodId3 = NULL;
    jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
    if (jmethodId3 == NULL)
    {
        printf("JNI: jmethodId3 is NULL.");
        return ji;
    }
    jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
    printf("JNI: staticFunc returns %d.\n", i2);

    // 调用与native方法同属一个类的方法
    jclass jclz0 = NULL;
    jclz0 = (*env)->GetObjectClass(env, jobj);
    if (jclz0 == NULL)
    {
        printf("JNI: jclz0 is NULL.");
        return ji;
    }
    jmethodID jmethodId4 = NULL;
    jmethodId4 = (*env)->GetMethodID(env, jclz0, "func2", "()Ljava/lang/String;");
    if (jmethodId4 == NULL)
    {
        printf("JNI: jmethodId4 is NULL.");
        return ji;
    }

    // 接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
    printf("JNI: func2 returns %s\n", ptr_jstr);

    return ji + 100;
}
复制代码

显然,这段JNI代码是native方法在JNI侧的实现,其中先后

  1. 创建了一个jintArray;
  2. 调用了Java侧JNICallJavaMethod类的构造方法;
  3. JNICallJavaMethod类的非静态方法;
  4. JNICallJavaMethod类的静态方法;
  5. JNICallJavaMethodNative类的非静态方法。

我们分别分析,以下代码就是上述代码的分别分析。

调用构造函数

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 创建一个类的实例,即jobject
  4. 调用方法。

注意:

  1. MethodName形参直接传<init>即可;
  2. 构造函数在Java侧没有返回值,连void都不是;但是,在JNI侧的方法签名中,返回值是void。比如,一个类的默认构造函数的签名是()V
  3. 凡是JNI方法的GetXxx()过程,都必须进行判空处理。

代码:

    // 调用另一个类的构造函数
    jclass jclz1 = NULL;
    jclz1 = (*env)->FindClass(env, "learn/java/jni/JNICallJavaMethod");
    if (jclz1 == NULL)
    {
        printf("JNI: jclz is NULL.");
        return ji;
    }
    jmethodID jmethodId1 = NULL;
    jmethodId1 = (*env)->GetMethodID(env, jclz1, "<init>", "()V");
    if (jmethodId1 == NULL)
    {
        printf("JNI: jmethodId1 is NULL.");
        return ji;
    }
    jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
    (*env)->CallVoidMethod(env, jobj1, jmethodId1);
复制代码

调用非静态方法

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 创建一个类的实例,即jobject
  4. 调用方法。

注意:

  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
  2. Java侧来说,一个类的对象可以调用多个方法;但是JNI侧的jobject是与jmethodID一一对应的。所以,即使JNI侧调用的不同方法属于同一个类,也需要创建不同的jobject,不能共用;从创建jobject的JNI函数可以看出来:
//jobject与jmethodID是一一对应的关系:
jobject jobj1 = (*env)->NewObject(env, jclz1, jmethodId1);
复制代码

代码:

    // 调用另一个类的非静态方法
    jmethodID jmethodId2 = NULL;
    jmethodId2 = (*env)->GetMethodID(env, jclz1, "func", "(I)I");
    if (jmethodId2 == NULL)
    {
        return ji;
    }
    jobject jobj2 = (*env)->NewObject(env, jclz1, jmethodId2);
    jint i1 = (*env)->CallIntMethod(env, jobj2, jmethodId2, 5);
    printf("JNI: func returns %d.\n", i1);
复制代码

调用静态方法

步骤:

  1. 加载类,被调用方法所在类的运行时类,即jclass
  2. 获取方法ID,即jmethodID
  3. 调用方法。

注意:

  1. 因为该非静态方法与上述构造方法同属一个类,所以此时可以省去加载运行时类的步骤一,直接用已经获取到的jclass;
  2. 因为静态方法的调用不需要对象实例,所以调用Java静态方法时,不需要jobject。

代码:

    // 调用另一个类的静态方法
    jmethodID jmethodId3 = NULL;
    jmethodId3 = (*env)->GetStaticMethodID(env, jclz1, "staticFunc", "([I)I");
    if (jmethodId3 == NULL)
    {
        printf("JNI: jmethodId3 is NULL.");
        return ji;
    }
    jint i2 = (*env)->CallStaticIntMethod(env, jclz1, jmethodId3, jArr);
    printf("JNI: staticFunc returns %d.\n", i2);
复制代码

调用native方法所在类的方法

这里以非静态方法为例,因为Java侧方法与native方法在同一个类中,而JNI侧实现native方法时,会传入一个jclass/jobject,分别对应Java侧的native方法声明是static native/native。此时我们可以直接使用传入的jclass,或者利用(*env)->GetObjectClass(env,jobj)获取到运行时类。关键在于,调用哪个方法,首先需要加载该方法所在的类到JVM运行时环境中

代码:

    // 调用与native方法同属一个类的方法
    jclass jclz0 = NULL;
    jclz0 = (*env)->GetObjectClass(env, jobj);
    if (jclz0 == NULL)
    {
        printf("JNI: jclz0 is NULL.");
        return ji;
    }
    jmethodID jmethodId4 = NULL;
    jmethodId4 = (*env)->GetMethodID(env, jclz0, "func2", "()Ljava/lang/String;");
    if (jmethodId4 == NULL)
    {
        printf("JNI: jmethodId4 is NULL.");
        return ji;
    }

    // 接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
    printf("JNI: func2 returns %s\n", ptr_jstr);
复制代码

Q&A

JNI侧如何创建整形数组

步骤:

  1. 声明数组名字与数组长度,即jArr、4
  2. 获取数组元素类型(jint型)的指针,通过调用(*env)->GetIntArrayElements(env, jArr, NULL)
  3. 利用指针,为元素赋值;
  4. 释放指针资源,数组得以保留。

代码:

//todo:JNI侧创建一个int array
    jintArray jArr = (*env)->NewIntArray(env, 4);//步骤1
    jint *arr = (*env)->GetIntArrayElements(env, jArr, NULL);//步骤2
    arr[0] = 0;//步骤3
    arr[1] = 10;
    arr[2] = 20;
    arr[3] = 30;
    (*env)->ReleaseIntArrayElements(env,jArr,arr,0);//步骤4
复制代码
Java侧方法返回String,JNI调用时如何打印返回值?

步骤:

  1. 定义jstring变量,并用(jstring)强转jobject;
  2. 定义字符型指针,并用 (char *)强转;
  3. 打印。
    // 接收Java方法返回的字符串,并在JNI侧打印
    jstring jstr = (jstring)(*env)->CallObjectMethod(env, jobj, jmethodId4);
    char *ptr_jstr = (char *)(*env)->GetStringUTFChars(env, jstr, 0);
    printf("JNI: func2 returns %s\n", ptr_jstr);
复制代码
JNI侧与Java侧的控制台打印顺序

结论是:

JNI侧的控制台打印一定出现在Java侧程序运行结束之后。

我们可以调试看现象:

image.png

两遍I am constructor?

:在调用构造方法和非静态方法的两个调用过程中,都需要通过(*env)->NewObject(env, jclz, jmethodId)创建与jmethodID一一对应的jobject,所以调用了两次构造函数。

两遍func is called?

:待解答!

能否脱离native方法的实现来调用Java侧方法?

:可以,JNI是Java跨平台的实现机制,是Java与原生代码交互的机制。上述的过程我们一般都是在JNI侧的native方法实现中进行的,因为native方法的JNI实现中就有JNIEnv*指针,是获取JNIEnv*最容易的方式,并非唯一方式。如何获取JNIEnv*?待解答!

3.3 JNI处理从Java传来的字符串

Java与C字符串的区别

  • Java内部使用的是utf-16 16bit 的编码方式;
  • JNI里面使用的utf-8 unicode编码方式,英文是1个字节,中文3个字节;
  • C/C++ 使用ASCII编码,中文的编码方式GB2312编码,中文2个字节。

image.png

实战代码

//Java:
package JNICallJava;

public class GetSetJavaString {
    static {
        System.load("E:\\_Projects_\\JNI_Projects\\JNI_C\\cmake-build-debug\\libGetSetJavaStringLib.dll");
    }
    public static native String func(String s);
    public static void main(String[] args) {
        String str = func("--Do you enjoy coding?");
        System.out.println(str);
    }
}
复制代码
//C:
#include "stdio.h"
#include "jni.h"
JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
        (JNIEnv *,jclass,jstring);//没有用专门的.h文件,此声明可写可不写。

JNIEXPORT jstring JNICALL Java_JNICallJava_GetSetJavaString_func
        (JNIEnv *env,jclass jclz,jstring jstr){
    const char *chr = NULL;//字符指针定义与初始化分开
    jboolean iscopy;//判断jstring转成char指针是否成功
    chr = (*env)->GetStringUTFChars(env,jstr,&iscopy);//&iscopy位置一般直接传入NULL就好
    if(chr == NULL){
        return NULL;//异常处理
    }
    char buf[128] = {0};//申请空间+初始化
    sprintf(buf,"%s\n--Yes, I do.",chr);//字符串拼接
    (*env)->ReleaseStringUTFChars(env,jstr,chr);//编程习惯,释放内存
    return (*env)->NewStringUTF(env,buf);
}
复制代码
//CMakeLists.txt
add_library(GetSetJavaStringLib SHARED  GetSetJavaString.c)
复制代码

运行结果

image.png

异常处理

上述代码实例中,GetStringUTFChars()方法将JNI的jstring变量转换成C语言能操作的char指针,这个过程可能失败,其实任何转换过程都可能失败,这些过程的目标变量的定义和初始化都需要分开进行,并通过判空进行异常处理

C语言字符串拼接

在C语言中,没有String,字符串都是字符指针。其拼接过程不像Java等语言那么简单,分为以下过程:

  1. malloc申请空间
  2. 初始化
  3. 拼接字符串
  4. 释放内存

灵活的静态注册

  • 此实战代码中,我们没有想一般的静态注册一样使用Java native产生的.h文件,而是直接在实现JNI方法之前写了一个JNI静态注册,这也是可行的,甚至这个提前的声明注册也是可以不写的。此时我们在CMakeLists.txt中的add_library()中值包含了该.c文件。核心在于add_library()中一定要包含native方法在JNI的实现函数,.h文件更多是Java命令生成的教你怎么写JNI实现的一个辅助,无关紧要。
  • JNI无视Java侧的访问控制权限,但会区别静态或非静态。
分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改