“初中”
如果你做android原生开发的话,就会用到android和C++交互,也就是JNI开发。而每次新增接口最麻烦的部分,也是JNI这一层。需要做各种java/c++的类型转换,以及回调的反射等处理。接下来,详细记录一下android如何通过jni和C++代码通信,以及过程中可能出现的问题,方便自己,也方便其他人参考。这就是写这篇文章的“初中“啦。
JNI简介
JNI(Java Native Interface),是Java程序设计语言功能最强的特征,它允许Java类的某些方法原生实现,同时让它们和普通Java方法一样调用。原生方法也可以创建、使用Java对象。
Hello World
按照惯例,先从HelloWorld(官方Demo)开始。
Java native接口声明:
package com.example.hellojni;
public class HelloJni{
static{
System.loadLibrary("hello-jni");
}
...
public native String stringFromJNI();
}
对应的原生实现:
#include <string.h>
#include <jni.h>
...
Jstring
Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz)
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
添加原生实现的步骤:
- 声明Java的native方法声明;
- 编写native对应的原生实现;
- 将原生实现编译成动态库;
- 在Java代码中加载该库;
如此之后,就可以像使用java方法一样调用原生方法了。但是,我们发现java接口对应的JNI接口好像很复杂的样子,这个是怎么对应的,该如何写出这个JNI接口?
其实,我们仔细观察,还是很有规律的:
返回类型 Java_全限定类名(.换成_)_方法名(JNIEnv*,jobject)
其中,前两个参数是固定的,所有方法都有的,这两个参数我们后面再讲。
由此可以看出,想要写出Java native方法对应的原生接口声明,是一件非常麻烦且容易岀错的苦差事。而且,原生接口使用下划线分割包名,那么问题来了:我java方法本身就有下划线,肿么办?
其实不用担心,这些问题官方爸爸都替我们想了。
我们只需要写好Java的native方法,使用javah命令就可以直接生成对应的JNI接口声明。
javah -classpath/bin/ com.example.hellojni.HelloJni
java类的定义:
package com.example.hellojni;
public class HelloJni {
public static native String stringFromJNI();
public static native String string_from_jni();
}
运行javah命令,可在bin目录下生成com_example_hellojni_HelloJni.h头文件,其中包含了HelloJni中所有native方法对应的JNI方法声明,如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_hellojni_HelloJni */
#ifndef _Included_com_example_hellojni_HelloJni
#define _Included_com_example_hellojni_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_hellojni_HelloJni
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI
(JNIEnv *, jclass);
/*
* Class: com_example_hellojni_HelloJni
* Method: string_from_jni
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_string_1from_1jni
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
如此,是不是很方便!方法包含下划线的话,只需要在原来的下划线后面跟一个字符’1"即可。
光有这些,我们应该还有很多问题:Java跟C++之间数据类型如何对应和转换?我想从原生实现回调结果怎么办?岀参该怎么实现?…
接下来,我们一一介绍。
java/jni/c++类型对照
先来看看java、jni、c++之间的类型对照:
1、基本数据类型
基本数据类型可以直接和C++的相应基本数据类型映射,如下表所示。JNI使用类型定义使得这种映射对开发人员透明,也就是JNI中的类型是定义在jni.h头文件中的宏,C++开发可以直接看到。
表1.基本数据类型对照表
Java | Jni | C++ | size |
---|---|---|---|
boolean | jboolean | unsigned char | 无符号8位 |
byte | jbyte | signed char | 有符号8位 |
char | jbyte | unsigned short | 无符号16位 |
short | jshort | short | 有符号16位 |
int | jint | int | 有符号32位 |
long | jlong | long long | 有符号64位 |
float | jfloat | float | 32位 |
double | jdouble | double | 64位 |
2、引用类型
与基本类型不同,引用类型对原生方法是不透明的,它们的内部数据结构并不直接向原生代码公开。引用类型映射表如下。
表2.引用类型对照表
Java | 原生类型 |
---|---|
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
Other objects | jobject |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
java.lang.Object[] | jobjectArray |
Other arrays | jarray |
从表中可以看到,除了几个特殊的类,其他类对应的原生类型都是jobject,也就是上面说的不透明。但是不透明的话,我们如何获取该对象的字段和方法呢?这个就涉及到如何将JNI类型转换为C++类型了,我们继续往下看。
Java/JNI/C++类型转换
接下来,看看这些类型之间该如何转换。
1、基本类型转换
基本类型boolean,byte,char,int,shrot,long,float,double和C++中类型是一一对应的,无需转换,可直接赋值,见表1。
2、数组
以byte数组为例:
//参数说明:
java --> byte[] in;
jni --> jbyteArray jIn;
JNIEnv* env;
//类型转换
jbyte *srcBytes = env->GetByteArrayElements(jIn, 0);
//use srcBytes as char* in C++
//不要忘记释放哦
env->ReleaseByteArrayElements(jIn, srcBytes, 0);
通过GetByteArrayElements函数获取到字节数组jIn的内存指针,有了这个指针,C++就可以操作和使用该内存了。用完别忘记释放哦!
其他数组都类似,使用函数对GetXXXArrayElements、ReleaseXXXArrayElements即可。
3、对象类型
对象类型的转换比较麻烦,不过套路是一样的,一通可百通。
先来看个最常用的String类型的转换吧
//参数说明:
java --> String str;
jni --> jstring jstr;
JNIEnv* env;
//类型转换,先jstring-->char*,随后就可正常使用了
char* szStr = (char*)env->GetStringUTFChars(jstr, 0);
//使用完成不要忘记释放哦
env->ReleaseStringUTFChars(jstr,szStr);
接下来再看一个自定义类型的转换
本示例将java类com.example.Temp转换成C++结构体Temp,其中
java类的定义:
com.example;
public class Temp{
public int a;
public boolean b;
}
c++结构体定义:
typedef _temp{
int a;
bool b;
}Temp;
类型转换:
//参数说明:
java --> Temp temp;
jni --> jObject jTemp;
JNIEnv* env;
Temp p;
//首先,获取java类
jclass tempCls = env->FindClass("com/example/Temp");
//然后,获取字段ID
jfieldID aaId = env->GetFieldID(tempCls, "a", "I");
jfieldID bbId = env->GetFieldID(tempCls, "b", "Z");
//最后,获取字段值
jint aa = env->GetIntField(jTemp, aaId);
p.a = aa;
jboolean bb = env->GetBooleanField(jTemp, bbId);
p.b = bb;
套路总结:
- 通过java类的包名,获取jclass。
jclass cls = env->FindClass(“pkgName”); - 通过字段名和字段签名,获取字段ID。
fieldID = env->GetFieldID(cls, “memberName”,“memberSign”); - 通过对象和字段ID,获取字段值。
XXX a = env->GetXXXField(obj,fieldID);
签名
这里可以看到,在获取类字段ID、方法ID时,都分别需要字段类型签名和方法签名。这里重点讲一下签名问题,这是很多新手容易忽视和出错的地方。签名映射可以通过下表获取:
表3.Java类型签名映射表
java类型 | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
fully-qualified-class | Lfully-qualified-class; |
type[] | [type |
method type | (arg-type)ret-type |
在写代码时,由于该签名参数是字符串,没有语法检查,所以如果不小心写错的话,不容易排查。如果只是JNI函数参数的话,因为都是单个的,一般不会岀错(如果你硬是出错了,那我只能说:你不是一般人!)。但如果是回调的话,其中方法的签名比较长,对象数组和基本类型混合的话,就很容易岀错了。因此涉及到签名的,请格外小心,写完代码后做好review。
今天就先写到这里啦!
未完
待续...