前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java之JNI开发流程

Java之JNI开发流程

作者头像
菜菜cc
发布2022-11-15 21:30:16
1.6K0
发布2022-11-15 21:30:16
举报
文章被收录于专栏:菜菜的技术博客

​ 之前介绍过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 国际许可协议 进行许可。转载请注明出处!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java在命令行下使用JNI
    • 声明本地方法
      • 实现C函数
        • 显式映射
        • 隐式映射
    • Android Studio下使用JNI
      • JNI中C与C++的不同
        • 安装JNI开发插件
          • 创建工程
            • Java中调用C++
            • C++中调用Java
            • JNI类型与签名
              • javah和javap命令的使用
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档