之前介绍过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中调用C编写的add函数为例讲解,首先创建Hello.java
和native.c
。在Android Studio下使用JNI中会讲解C与C++在JNI中的不同,并采用C++来讲解JNI。
在Hello.java
中声明一个本地方法,并在静态代码块中加载对应的动态链接库。
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));
}
}
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
中实现以上步骤
#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;
}
编译运行
# 生成动态链接库
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
的第一行指定包名
package cn.caiyifan.jni;
采用隐式映射的方式不需要程序员去手动建立链接,JNI规范已经使用了一套映射规范,在C函数中实现的函数名格式:Java_包名_类名_Java方法名,需要注意的是包名以’_‘隔开,而不是’.‘
#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;
}
编译运行
# 生成动态链接库
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,借助IDE带来的自动生成功能,就变得很方便。注意笔者使用的Android Studio版本是3.4.2。先讲解JNI中C与C++的不同后,再在Android Studio下使用C++来进行JNI开发。
从jni.h
源码中可以看到JNIEnv
的类型是不同的
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
#else
typedef const struct JNINativeInterface* JNIEnv;
#endif
由于C++是面向对象的,而C非面向对象,但C如果需要以面向对象方式封装JNI的操作函数,则需要将函数指针封装在结构体内,调用的时候需要传递本结构体的地址,所以在C中调用JNI的方法是下面这样调用的,以NewStringUTF
为例
(*env)->NewStringUTF(env, "hello world");
通过jni.h
源码可知,C++的JNIEnv
的作法是包裹C的JNIEnv
后,在内部传递this指针进行调用的。所以在C++中直接以对象调用方法的方式调用即可
env->NewStringUTF("hello world");
创建Android工程时,选择Native C++。
创建完的工程会比常规的Android工程在src/main下多出一个cpp目录,这是IDE自动生成,编写的C/C++函数放在这个目录下即可。
创建一个Jni.java
文件,将Jni的native接口封装成一个单例类。
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方法。
public native String getStringFromJni();
这时候Android IDE会报错,提示Cannot resolve corresponding JNI function Java_cn_caiyifan_jnidemo_Jni_getStringFromJni,这个报错是因为没有实现对应的本地函数,只需要按下快捷键Alt+enter,就会在对应的C/C++文件中生成对应的函数接口。
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_caiyifan_jnidemo_Jni_getStringFromJni(JNIEnv *env, jobject instance) {
// TODO
return env->NewStringUTF(returnValue);
}
可以看到函数名正是JNI规范要求的格式。修改该函数
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函数了
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一般分为四步:
1.获取字节码对象
2.获取jmethodID对象
3.通过字节码对象创建jobject对象
4.通过jobject对象调用方法
其中第3步可视情况省略,当需要调用的Java方法正好位于调用该本地函数的类内,那么JNI函数的第二个参数即表示该对象
在Jni.java
中创建一个log_i方法,该方法用来输出log,供C++调用。并且声明一个native方法,在对于的Jni函数中来回调log_i方法。
public void log_i(String tag, String msg) {
Log.i(tag, msg);
}
public native void callBackFromCpp();
在对应的Cpp函数中回调该log_i方法。对象
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
中调用该本地方法
// 获取Jni对象
Jni jni = Jni.getInstance(getApplicationContext());
jni.callBackFromCpp();
运行后会发现成功在logcat上进行了打印。
签名的格式为: (参数签名)返回值签名
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
可以生成Java本地方法对应的C/C++函数接口,用法是指定一个class文件,不过在Android Studio中已经可以快捷键生成了。
javah cn.caiyifan.jnidemo.Jni
javap -s
可以生成一个Java文件所有方法的签名,用法与javah
一样
javap -s cn.caiyifan.jnidemo.Jni
但在Android Studio中目录结构确定编译后的class目录比较复杂,可以在工程根目录下使用以下命令
javap -s `find -name Jni.class`
本文作者: Ifan Tsai (菜菜)
本文链接: https://cloud.tencent.com/developer/article/2164592
版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!