Java之JNI开发流程

2022-11-15 21:30:16 浏览数 (3)

​ 之前介绍过C/C 和Python的相互调用,这一次笔者讲解C/C 和Java的相互调用。Java与C的相互调用需要使用JNI,JNI即Java Native Interface(Java本地接口)。Google提供了NDK(Native Development Kit), NDK包含了一套Android的交叉编译环境和开发库,使用它可以编写C/C 程序后编译成Android环境下使用的动态链接库,Java代码使用JNI规范调用C/C 实现的动态链接库。本文先介绍在命令行下使用JNI,随后介绍在Android Studio中使用JNI。

Java在命令行下使用JNI

笔者以Java中调用C编写的add函数为例讲解,首先创建Hello.javanative.c。在Android Studio下使用JNI中会讲解C与C 在JNI中的不同,并采用C 来讲解JNI。

声明本地方法

Hello.java中声明一个本地方法,并在静态代码块中加载对应的动态链接库。

代码语言:javascript复制
public class Hello {

    static {
        // 加载动态链接库    注意:对于libnative.so只需要写native
        System.loadLibrary("native");
    }

    // 声明本地方法
    public static native int addFromC(int a, int b);

    public static void main(String[] argv) {
        // 调用本地方法
        System.out.println("1   2 = "   addFromC(1, 2));
    }
}

实现C函数

Java调用C函数需要做C函数和Java本地方法的映射,建立该映射有两种方式: 显式映射和隐式映射。

显式映射

确保Java文件中不指定包名,指定了包名后在命令行下可能会出错,一般步骤如下:

1.包含jni.h头文件

/usr/lib/jvm/java-1.8.0-openjdk-amd64/include

其中jin.h又包含了jni_md.h

/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux

2.实现C函数

3.将C函数加入到映射数组中

4.实现JNI_OnLoad函数

native.c中实现以上步骤

代码语言:javascript复制
#include <jni.h>

#define ARRAY_SIZE(arr)   (sizeof(arr) / sizeof((arr)[0]))

// C函数需要比Java本地方法多出两个参数,这两个参数之后的参数列表与Java本地方法保持一致
// 第一个参数表示JNI环境,该环境封装了所有JNI的操作函数
// 第二个参数为Java代码中调用该C函数的对象
// jint表示JNI的int类型,在本文后面会给出所有JNI类型
jint add(JNIEnv *env, jobject thiz, jint a, jint b)
{
    return a   b;
}

static const JNINativeMethod methods[] = {
    // 第一个参数为Java本地方法名
    // 第二个参数为函数签名:(参数签名)返回值签名, 在本文后面会给出所有签名符号
    // 第三个参数为C函数
    {"addFromC", "(II)I", (void *)add},   // 建立Java本地方法和C函数的映射
};

// 在Java中调用System.loadLibrary方法时会调用到该函数
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
    JNIEnv *env;
    jclass cls;

    // 获取JNI环境
    if ((*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_8)) {
        return JNI_ERR;
    }

    // 获取Java类
    // JNI_OnLoad函数写法基本固定, 唯一需要修改的是FindClass的第二个参数,即类名
    cls = (*env)->FindClass(env, "Hello");
    if (cls == NULL) {
        return JNI_ERR;
    }

    // 注册本地方法
    if ((*env)->RegisterNatives(env, cls, methods, ARRAY_SIZE(methods)) < 0)
        return JNI_ERR;

    return JNI_VERSION_1_8;
}

编译运行

代码语言:javascript复制
# 生成动态链接库
gcc -shared -fPIC -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux/ -o libnative.so native.c 
javac  Hello.java   # 编译Java
java  -Djava.library.path=.   Hello   # 运行Java,并指定动态链接库的路径
隐式映射

Hello.java的第一行指定包名

代码语言:javascript复制
package cn.caiyifan.jni;

采用隐式映射的方式不需要程序员去手动建立链接,JNI规范已经使用了一套映射规范,在C函数中实现的函数名格式:Java_包名_类名_Java方法名,需要注意的是包名以’_‘隔开,而不是’.‘

代码语言:javascript复制
#include <jni.h>

// C函数需要比Java本地方法多出两个参数,这两个参数之后的参数列表与Java本地方法保持一致
// 第一个参数表示JNI环境,该环境封装了所有JNI的操作函数
// 第二个参数为Java代码中调用该C函数的对象
// 函数名格式: Java_包名_类名_Java方法名
jint Java_cn_caiyifan_jni_Hello_addFromC(JNIEnv *env, jobject thiz, jint a, jint b)
{
    return a   b;
}

编译运行

代码语言:javascript复制
# 生成动态链接库
gcc -shared -fPIC -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/ -I/usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux/ -o libnative.so native.c 
javac -d . Hello.java    # 编译Java并生成完整包名路径
java -Djava.library.path=. cn.caiyifan.jni.Hello   # 运行Java,并指定动态链接库的路径

Android Studio下使用JNI

在Android Studio中使用JNI,借助IDE带来的自动生成功能,就变得很方便。注意笔者使用的Android Studio版本是3.4.2。先讲解JNI中C与C 的不同后,再在Android Studio下使用C 来进行JNI开发。

JNI中C与C 的不同

jni.h源码中可以看到JNIEnv的类型是不同的

代码语言:javascript复制
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
#else
typedef const struct JNINativeInterface* JNIEnv;
#endif

由于C 是面向对象的,而C非面向对象,但C如果需要以面向对象方式封装JNI的操作函数,则需要将函数指针封装在结构体内,调用的时候需要传递本结构体的地址,所以在C中调用JNI的方法是下面这样调用的,以NewStringUTF为例

代码语言:javascript复制
(*env)->NewStringUTF(env, "hello world");

通过jni.h源码可知,C 的JNIEnv的作法是包裹C的JNIEnv后,在内部传递this指针进行调用的。所以在C 中直接以对象调用方法的方式调用即可

代码语言:javascript复制
env->NewStringUTF("hello world");

安装JNI开发插件

创建工程

创建Android工程时,选择Native C 。

创建完的工程会比常规的Android工程在src/main下多出一个cpp目录,这是IDE自动生成,编写的C/C 函数放在这个目录下即可。

Java中调用C

创建一个Jni.java 文件,将Jni的native接口封装成一个单例类。

代码语言:javascript复制
package cn.caiyifan.jnidemo;

/**
 * 用来封装Jni的native接口
 */
public class Jni {
    static {
        System.loadLibrary("native-lib");
    }

    private static Jni jni;

    private Jni() {}

    public static Jni getInstance() {
        if (jni == null) {
            jni = new Jni();
        }
        return jni;
    }
}

并在Jni类中添加一个getStringFromJni的native方法。

代码语言:javascript复制
public native String getStringFromJni();

这时候Android IDE会报错,提示Cannot resolve corresponding JNI function Java_cn_caiyifan_jnidemo_Jni_getStringFromJni,这个报错是因为没有实现对应的本地函数,只需要按下快捷键Alt enter,就会在对应的C/C 文件中生成对应的函数接口。

代码语言:javascript复制
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_caiyifan_jnidemo_Jni_getStringFromJni(JNIEnv *env, jobject instance) {

    // TODO


    return env->NewStringUTF(returnValue);
}

可以看到函数名正是JNI规范要求的格式。修改该函数

代码语言:javascript复制
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_caiyifan_jnidemo_Jni_getStringFromJni(JNIEnv *env, jobject instance) {
    // env->NewStringUTF 将 char *转换成jstring类型
    return env->NewStringUTF("hello from cpp");
}

然后就可以在MainActivity中调用cpp函数了

代码语言:javascript复制
package cn.caiyifan.jnidemo;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /**
         * 调用 getStringFromJni native方法
         */
        // 获取Jni对象
        Jni jni = Jni.getInstance();
        // 调用native方法
        String str = jni.getStringFromJni();
        // 显示到Toast上
        Toast.makeText(this, str, Toast.LENGTH_LONG).show();
    }
}

运行到模拟器后,就可以发现成功调用了。

C 中调用Java

在C 中调用Java一般分为四步:

1.获取字节码对象

2.获取jmethodID对象

3.通过字节码对象创建jobject对象

4.通过jobject对象调用方法

其中第3步可视情况省略,当需要调用的Java方法正好位于调用该本地函数的类内,那么JNI函数的第二个参数即表示该对象

Jni.java中创建一个log_i方法,该方法用来输出log,供C 调用。并且声明一个native方法,在对于的Jni函数中来回调log_i方法。

代码语言:javascript复制
public void log_i(String tag, String msg) {
    Log.i(tag, msg);
}

public native void callBackFromCpp();

在对应的Cpp函数中回调该log_i方法。对象

代码语言:javascript复制
extern "C"
JNIEXPORT void JNICALL
Java_cn_caiyifan_jnidemo_Jni_callBackFromCpp(JNIEnv *env, jobject thiz) {
    // 1. 获取字节码对象
    //    参数: 要调用的Java方法所在类的路径
    jclass clazz = env->FindClass("cn/caiyifan/jnidemo/Jni");
    // 2. 获取jmethodID对象
    //    第一个参数: 字节码对象对象对象
    //    第二个参数: Java方法名
    //    第三个参数: Java方法签名     该签名如何编写见文末
    jmethodID methodId = env->GetMethodID(clazz, "log_i", "(Ljava/lang/String;Ljava/lang/String;)V");
    // 3. 通过字节码对象创建jobject对象    此时Jni函数的第二个参数即为jobject对象,所以无需再创建
    // 4. 通过jobject对象调用方法
    //    第一个参数: Jobject对象
    //    第二个参数: jmethodID对象
    //    剩下的可选参数: 调用Java方法所传递的参数
    env->CallVoidMethod(thiz, methodId, env->NewStringUTF("test"), env->NewStringUTF("hello from java"));
}

最后在MainActivity.java中调用该本地方法

代码语言:javascript复制
// 获取Jni对象
Jni jni = Jni.getInstance(getApplicationContext());
jni.callBackFromCpp();

运行后会发现成功在logcat上进行了打印。

JNI类型与签名

签名的格式为: (参数签名)返回值签名

Java类型

JNI类型

C/C 类型

签名

boolean

jboolean

unsigned char

Z

byte

jbyte

char

B

char

jchar

unsigned short

C

short

jshort

short

S

int

jint

int

I

long

jlong

long long

J

float

jfloat

float

F

double

jdouble

double

V

jobject

void *

L用/隔开的全类名;

类: 例如String的签名为Ljava/lang/String; 注意: 包名和类名用/隔开, 结尾有一个; 数组:用[表示数组签名, 例如int[]的签名为[I

javah和javap命令的使用

javah可以生成Java本地方法对应的C/C 函数接口,用法是指定一个class文件,不过在Android Studio中已经可以快捷键生成了。

代码语言:javascript复制
javah cn.caiyifan.jnidemo.Jni

javap -s可以生成一个Java文件所有方法的签名,用法与javah一样

代码语言:javascript复制
javap -s cn.caiyifan.jnidemo.Jni

但在Android Studio中目录结构确定编译后的class目录比较复杂,可以在工程根目录下使用以下命令

代码语言:javascript复制
javap -s `find -name Jni.class`

本文作者: Ifan Tsai  (菜菜)

本文链接: https://cloud.tencent.com/developer/article/2164592

版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

0 人点赞