本篇为android NDK开发的第二部分JNI,这是NDK开发的重点,属于纯编码部分,这一部分将分为两节:第一节就是本篇要讲述的JNI操作java,第二节讲述JNI调用纯C,也就是非标准JNI的so库。
JNI基础知识
搭建NDK以及开发环境配置,请看第一部分: 使用CMake进行android NDK开发
1.认识JNI函数
创建一个NDK项目
其中,native-lib.cpp就是写JNI的地方,CMakeLists.txt就是配置。先来看一下native-lib.cpp
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_mmdet_jean_Test_helloWord(JNIEnv *env, jobject instance) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
解释说明:
【extern "C"】 :为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的,如果是C++文件(.CPP),就需要写这么一句。如果是C文件(.C)可写可不写。
【JNIEXPORT jstring JNICALL】:JNIEXPORT 和JNICALL是宏定义,被定义在jni.h里,固定写法。jstring 是函数返回类型。jstring 就相当于Sring.
【Java_cn_mmdet_jean_Test_helloWord】:这个是函数名,对应着本地方法,规则是:
Java开头,跟上包名+类名+方法名。所以可知,这个JNI函数对应着Test类中的helloWord方法,而Test位于包cn.mmdet.jean下。
【JNIEnv *env, jobject instance】这个是函数参数,固定的,每个JNI函数应该都有这两个参数,并且位置在第一和第二。env可以看做是Jni接口本身的一个对象,是一个指针,基本上JNI的所有操作都是基于env的,后面我们将会看到,指针是c语言具有的特性。instance表示 该JNI函数对应的本地方法所在的类。
2.本地方法
本地方法就是java层能够调用的函数,用native修饰,没有方法体,只有声明,对应着cpp文件里的JNI 函数。下面我们来创建一个对应helloWord的本地方法:
public class Test {
static {
System.loadLibrary("native-lib");
}
public native String helloWord();
}
说明:
新建一个Test类,加载本地函数库 System.loadLibrary,我的叫native-lib,与CMakeLists中定义的要一致,我的配置如下。
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp )
target_link_libraries(
native-lib
${log-lib} )
其中
public native String helloWord();
表示本地方法,对应着native-lib.cpp中Java_cn_mmdet_jean_Test_helloWord(JNIEnv *env, jobject instance),这里的instance就是Test类的实例。
2.带参数的本地方法
定义一个带参数的本地方法,如下:
public native void helloword(String from,int count);
对应的JNI函数方法如下:
extern "C"
JNIEXPORT void JNICALL
Java_cn_mmdet_jean_Test_helloword(JNIEnv *env, jobject instance, jstring from_, jint count) {
const char *from = env->GetStringUTFChars(from_, 0);
env->ReleaseStringUTFChars(from_, from);
}
快捷方式:像上面这么一行行敲,不仅累还容易出错。可以先定义好native方法,鼠标定位到方法前,使用ctrl+enter,选择第一项,IDE会自动帮我们创建好对应的JNI函数。
经过,上面相信你已经熟悉了JNI函数、本地方法了。
对于这中跨语言编程,最重要的最繁琐的也就是数据类型转换了,但是JNI已经帮我们封装好了。下面我们就来看一下数据类型的映射。
数据类型
在JNI里也存在类似的数据类型,与Java比较起来,其范围更具严格性,分为基本数据类型,如:int、 float 、char等基本类型,引用类型,如:类、实例、数组。用一张表格来表示应该会更清晰:
基本类型
Java | JNI | C/C++ |
---|---|---|
boolean | jboolean | C/C++8位整型 unsigned char |
byte | jbyte | C/C++带符号的8位整型 char |
char | jchar | C/C++无符号的16位整型 unsigned short |
short | jshort | C/C++带符号的16位整型short |
int | jint | C/C++带符号的32位整型 int |
long | jlong | C/C++带符号的64位整型e long |
float | jfloat | C/C++32位浮点型 float |
double | jdouble | C/C++64位浮点型 double |
Object | jobject | 任何Java对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
byte[] | jbyteArray | 比特型数组 |
char[] | jcharArray | 字符型数组 |
short[] | jshortArray | 短整型数组 |
int[] | jintArray | 整型数组 |
long[] | jlongArray | 长整型数组 |
float[] | jfloatArray | 浮点型数组 |
double[] | jdoubleArray | 双浮点型数组 |
了解完JNI的基本知识与数据类型转换,下面就是JNI操作Java的实例了。
JNI操作Java
这一部分主要讲解在JNI中调用Java类的方法,属性、实例化类等,分别以java代码、本地方法、JNI方法进行展示。
属性
java代码 定义一个属性name
public class Test{
static {
System.loadLibrary("native-lib");
}
public String name = "jean";
}
本地方法(后面将以native代替)
public class Test {
...
...
public native String getTestName();
}
JNI
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_mmdet_jean_Test_getTestName(JNIEnv *env, jobject instance) {
//通过instance获取jclass对象,这里相当于Test
jclass _jcalss = env->GetObjectClass(instance);
//通过GetFieldID获取FieldID
//GetFieldID方法参数分别为:jclass对象、jclass对象属性名、签名(数据类型)
jfieldID _jFieldId = env->GetFieldID(_jcalss,"name","Ljava/lang/String;");
//通过FieldID获取属性的值
jstring _jstring = (jstring)env->GetObjectField(instance,_jFieldId);
return _jstring ;
}
说明:
- GetFieldID方法中第三个参数为签名,对应着属性的数据类型,
如何填写,请参考下面:
Java | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
全限定的类 | L +class +;(注意这里的分号不能少) |
数组 type[] | [ + type |
函数 | (arg-type)return type |
举例说明:
- String name
Ljava/lang/String;
- int test(int a,String b,byte[] c)
(ILjava/lang/String;[B)
相信你已经会使用签名了,不会用也没关系,接下来我们还会用到哟,多练习几次就懂了。
方法
java
public class Test {
static {
System.loadLibrary("native-lib");
}
//Java
public String getName(){
return "1234";
}
//native
public native String getNameFromJava();
}
JNI
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_mmdet_jean_Test_getNameFromJava(JNIEnv *env, jobject instance) {
//获取jlass对象
jclass _jclass = env->GetObjectClass(instance);
//获取 方法ID GetMethodID
//参数说明:jlass对象、函数名、签名,这里签名有一个(),这就是方法的签名
jmethodID _jmethodID = env->GetMethodID(_jclass,"getName","()Ljava/lang/String;");
//通过MethodID调用方法,使用CallObjectMethod函数
jstring _jstring = (jstring)env->CallObjectMethod(instance,_jmethodID);
//将字符串转换为C语言的字符串 char*
char* tempStr = (char*)env->GetStringUTFChars(_jstring,NULL);
char text[20] = "success";
char* finResult = strcat(tempStr,text);
//将C语言字符串转为jstring返回
return env->NewStringUTF(finResult);
}
方法签名说明:
- 方法签名为(arg-type)return type
- 举例说明:
public native String getNameFromJava();
//方法返回值类型为String所以签名为Ljava/lang/String;,参数类型为空,所以不填()
()Ljava/lang/String;
public native byte[] getNameFromJava(String o);
//方法返回值类型为byte[] 所以签名为[B;,参数类型为String所以参数签名为Ljava/lang/String;
(Ljava/lang/String;)[B
构造函数
这里主要演示一下,JNI中如何调用一个类的构造函数,实例化一个类的对象 返回去。
java
//定义一个类
public class Person {
private String name;
private int age;
private boolean isUp;//是否成年
public Person(String name, int age, boolean isUp) {
this.name = name;
this.age = age;
this.isUp = isUp;
}
}
//native
public class Test {
static {
System.loadLibrary("native-lib");
}
//native
public native Person genPerson(String name,int age,boolean isUp);
}
JNI
extern "C"
JNIEXPORT jobject JNICALL
Java_cn_mmdet_jean_Test_genPerson(JNIEnv *env, jobject instance, jstring name_, jint age,
jboolean isUp) {
const char *name = env->GetStringUTFChars(name_, 0);
//首先 创建jclass对象,FindClass参数为Person所在的包名,用"/"分割
jclass cls = env->FindClass("cn/mmdet/jean/Person");
/**
* 调用构造函数其中函数名
* 固定为"<init>"
* 参数类型分别为String、int、boolean,这里有几个参数,就传几个参数类型的签名
* v表示void,因为构造函数返回值为void
*/
jmethodID methodID = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;IZ)V");
//使用NewObject创建java对象,前两个参数分别为class对象、方法ID,后面为构造函数的实参传入,有几个传几个
jobject object = env->NewObject(cls, methodID,name,age,isUp);
env->ReleaseStringUTFChars(name_, name);
return object;
}
上述JNI中,用了不少env调用的函数,如NewObject、GetMethodID等等,这些都是JNI封装在jni.h里的,我们通过#include <jni.h>就能使用,主要作用就是Java与C的互相转换。另外JNI部分的代码编写,需要你知道一些C/C++的知识。我们来看一些常用的转换函数:
native
public class Test {
static {
System.loadLibrary("native-lib");
}
private native String testType(String a,int b,byte[] c,String[] d,Person e);
}
然后使用快捷键,创建对应的JNI函数:
extern "C"
JNIEXPORT jstring JNICALL
Java_cn_mmdet_jean_Test_testType(JNIEnv *env, jobject instance, jstring a_, jint b, jbyteArray c_, jobjectArray d, jobject e) {
//将java字符串 转为C的字符串
const char *a = env->GetStringUTFChars(a_, 0);
//将byte数组 转为C能使用的jbyte*
jbyte *c = env->GetByteArrayElements(c_, NULL);
//指针释放
env->ReleaseStringUTFChars(a_, a);
env->ReleaseByteArrayElements(c_, c, 0);
//char* 转jstring
return env->NewStringUTF(returnValue);
}
这里只是简单的转换。
JNI调用Java的知识到这里就结束了。学完你应该会使用了,可能会有些生涩,多练习几遍就对了,慢慢消化。
一些复杂的操作将会在下一节里做讲解,如JNI调用so函数、指针参数、字节数组的返回、二维数组的返回等。