Java和C/C++混编技术是一种常用的开发技术,目的是使用C/C++弥补Java在高性能计算方面的不足。如Android的Native开发,我们经常是使用Native代码来处理一些复杂的计算,比如图象处理。实现Java/Kotlin和C/C++通讯的接口叫Jni,下面我们来看一下Jni接口的基本用法。
1. Java/Kotlin调用C/C++方法
首先在Java/Kotlin中声明一个Native的空方法
package com.test.native;
public class Main {
//Java: 使用native关键字声明
public native String getString(int index);
//Kotlin: 使用external关键字声明
public external fun getString(index: Int): String
}
然后,在使用这个方法之前,如果你的C/C++库是动态的so库,那么必须提前在静态代码块中加载Native库,例如:
//Java: 在类的静态代码块中加载
static {
System.loadlibrary("native-lib");
}
//Kotlin: 在伴生对象的初始化代码块中加载
companion object {
init {
System.loadlibrary("native-lib")
}
}
参数名表示库的名称,这个"native-lib"完整的库文件名称是“libnative-lib.so”(为啥叫这个?这是Android Studio自动生成的,大家都懒得改名),libxxx.so中间这个xxx就是动态库的名称,这是C/C++通用的命名方式。
注意,一般情况下我们的程序一定有一个动态库作为主库,主库可以链接其他的库,然后Java/Kotlin代码中通过加载这个动态的主库来实现调用。
之后,我们需要在Native代码中写一个对应的Jni方法。在我们的native-lib.cpp中:
extern "C" {
JNIEXPORT jstring
JNICALL Java_com_test_native_Main_getString(JNIEnv * env, jobject obj, jint index) {
return env->NewStringUTF(sprintf("from native index: %d", index));
}
}
这个方法有一系列的修饰符和一个很长很长的方法名,JNIEXPORT后边是返回值类型,JNICALL后边是方法的全名,名称第一个单词是Java(kotlin也得写Java,而不是用Kotlin开头),然后跟完整的包名和Java/Kotlin方法名,参数列表的前两个是默认传递的Java参数,包括Java环境和当前类的实例,之后才是真正需要的参数列表。Java中的类型都被映射成了jstring、jint等等,部分可以直接强转成C/C++类型,如jint,但是jstring不行,必须通过JNIEnv操作转换才能确保正确。
Java/Kotlin调用C/C++就这么简单几步,然后你就可以在JNI的方法里写你想用的功能了。
2. C/C++调用Java/Kotlin方法、属性
(1) 方法
这个相对麻烦一点,因为方法分了静态和非静态、有返回值和无返回值等等。
首先,有一个Java/Kotlin类,test.java:
package com.test.native;
class Main {
int index;
static String name;
public String getString(int index );
public static String getName();
}
然后再C/C++中调用这个方法:
jclass Main = env->FindClass("com/test/native/Main");
jmethodID main_constructor = env->GetMethodID(Main, "<init>", "()V");
jobject main = env->NewObject(Main, main_constructor);
jmethodID method = env->GetMethodID(Main, "getString", "(I)Ljava/lang/String;");
int index = 1;
env->CallObjectMethod(main, method, index);
主要过程分为三步:1,映射Main这个类,并且新建对象(也可以从Java传过来);2,找到需要调用的方法 getString
;3,调用方法。
其中,类的分隔符使用 /
,构造方法使用 <init>
来代替。在获取方法的时候,最后一个参数叫做方法的签名,如 (I)Ljava/lang/String;
。括号内是方法的参数,括号后边是方法的返回类型,I
代表int类型, Ljava/lang/String
代表String类型。完整的类型对应表:
类型 | 缩写 |
---|---|
short | S |
int | I |
long | J |
float | F |
double | D |
boolean | Z |
byte | B |
char | C |
void | V |
对象类型 | L包名,如:Ljava/lang/String |
数组类型 | 前边加 [ 修饰符, 如: [S |
对象参数或返回值后边必须要加分号,基本类型不用加,如 "(Ljava/lang/String;I)V","(IB)Ljava/lang/String;"。
静态方法调用
jclass Main = env->FindClass("com/test/native/Main");
jmethodID static_method = env->GetStaticMethodID(Main, "getName", "()Ljava/lang/String;");
env->CallStaticObjectMethod(Main, static_method);
流程类似,只是少了创建对象的过程。
(2) 属性
属性和方法比较类似,只是多了个修改的步骤:
jclass Main = env->FindClass("com/test/native/Main");
jmethodID main_constructor = env->GetMethodID(Main, "<init>", "()V");
//属性
jfieldID field = env->GetFieldID(Main, "index", "I");
jint index = env->GetIntField(main, field);
//更改属性值
env->SetIntField(main, field, 2);
//静态属性
jfieldID static_field = env->GetStaticFieldID(Main, "name", "Ljava/lang/String");
jobject name = env->GetStaticObjectField(Main, static_field);
//更改静态属性值
env->SetStaticObjectField(Main, static_field, env->NewStringUTF("main"));
代码看着多了点,其实原理并不复杂。