JNI是一种本地编程接口,也就是桥接语言,它能够使Java与其他编程语言(C、C++、汇编)进行交互。下面我先列出来Java的数据类型对应的JNI类型的表,这里可以暂时先不看,之后的例子中会有介绍,当做字典来用即可。
Java类型 | 本地类型 | 描述 |
---|---|---|
boolean | jboolean | C/C++8位整型 |
byte | jbyte | C/C++带符号的8位整型 |
char | jchar | C/C++无符号的16位整型 |
short | jshort | C/C++带符号的16位整型 |
int | jint | C/C++带符号的32位整型 |
long | jlong | C/C++带符号的64位整型 |
float | jfloat | C/C++32位浮点型 |
double | jdouble | C/C++64位浮点型 |
Object | jobject | 任何Java对象,或者没有对应java类型的对象 |
Class | jclass | Class对象 |
String | jstring | 字符串对象 |
Object[] | jobjectArray | 任何对象的数组 |
boolean[] | jbooleanArray | 布尔型数组 |
JNI编程
关于Android项目的创建过程我就不赘述了,可以自行百度,我这里直接进入正题
在MainActivity中会有这么一句代码,而这个代码的作用就是动态加载native-lib的库文件,这个库文件是根据src/main/cpp中的native-lib.cpp文件生成的,我们的JNI代码都是写在这个cpp文件中
static {
System.loadLibrary("native-lib");
}
1.基本数据类型及字符串类型
在MainActivity中创建一个native方法,有3个类型的参数:int,float,String,返回值为String类型
native String perInfo(int age,float height,String name);
此时我们需要在刚刚所说的native-lib.cpp文件中创建如下对应的方法。我做了一些说明
extern "C"
JNIEXPORT jstring JNICALL
Java_com_gzc_jnitest_MainActivity_perInfo(JNIEnv *env, jobject instance,
jint jage, jfloat jheight,jstring jname){
//将jstring类型转换为char*类型
const char* str = env->GetStringUTFChars(jname,JNI_FALSE);
char returnStr[100];
sprintf(returnStr,"name:%s,age:%d,height:%.2f",str,jage,jheight);
//创建一个新字符串
return env->NewStringUTF(returnStr);
}
需要注意的地方:
(1)方法名中Java为固定的格式;com_gzc_jnitest是类名,只是把点改成_;MainActivity为native方法所在的类的名称;perInfo为native方法名
(2)jint,jfloat所对应的都是基本数据类型,可以直接使用;而jstring对应的是引用类型,需要进行转换才可以使用
(3)方法中除了native对应的三个参数外,还有两个参数:
JNIEnv *env:可以把它看成JVM的引用对象,关于数据的一些处理都要用到;
jobject instance:这个参数其实就是native方法所在类的对象,这里就是MainActivity
(4)GetStringUTFChars的第二个参数是一个boolean类型:
true:表示将jstring的数据拷贝到新的内存中,之后使用的都是新内存的数据
false:表示不进行拷贝,之后使用的都是原数据
2.基本数据类型及String数据的数组
同样的我们在MainActivity中写相应的native方法,在native-lib.cpp文件中写对应的JNI代码
native void printPeopleInfo(int[] ages,float[] heights,String[] names);
#include <android/log.h>
// __VA_ARGS__ 代表... 可变参数
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__);
extern "C"
JNIEXPORT void JNICALL
Java_com_gzc_jnitest_MainActivity_printPeopleInfo(JNIEnv *env,jobject instance,
jintArray jages,jfloatArray jheights,jobjectArray jnames){
//取出数组首元素的内存地址
jint *ageFirst = env->GetIntArrayElements(jages,JNI_FALSE);
jfloat *heightFirst = env->GetFloatArrayElements(jheights,JNI_FALSE);
//获取数组的长度
int32_t size = env->GetArrayLength(jages);
for (int i = 0; i <size; ++i) {
jstring jname = static_cast<jstring>(env->GetObjectArrayElement(jnames,i));
const char* name = env->GetStringUTFChars(jname,JNI_FALSE);
LOGE("第%d位的信息:姓名:%s,年龄:%d,身高:%.2f",i+1,name,*(ageFirst+i),*(heightFirst+i));
env->ReleaseStringUTFChars(jname,name);
}
env->ReleaseIntArrayElements(jages,ageFirst,0);
env->ReleaseFloatArrayElements(jheights,heightFirst,0);
}
需要注意的地方:
(1)数组其实就是内存中一段连续的地址,我们拿到首元素的地址,就可以获得每个元素的值。这里feageFirst和heightFirst分别就是int[]和float[]的首元素地址。
(2)获取字符串数组每个元素的值,首先先将每个元素转换成jstring类型,然后再转换成char*类型输出
(3)LOGE是在开头位置定义的宏,等同于
__android_log_print(ANDROID_LOG_ERROR,"JNI","第%d位的信息:姓名:%s,年龄:%d,身高:%.2f",i+1,name,*(ageFirst+i),*(heightFirst+i));
(4)注意要释放字符串:env->ReleaseStringUTFChars(jname,name)
(5)释放数组比释放字符串多了一个参数:
0:刷新java数组 并 释放c/c++数组。比如在native-lib.cpp中改变了数组的值,在java代码中的数组的值也就改变了
1(JNI_COMMIT):只刷新java数组。同上
2(JNI_ABORT):只释放c/c++数组。比如改变了native-lib.cpp中数组的值,java代码中的数组不改变
3.JavaBean类型的获取及创建
创建一个名为PerInfoBean的类
public class PerInfoBean {
private int age;
private float height;
private String name;
public PerInfoBean(int age, float height, String name) {
this.age = age;
this.height = height;
this.name = name;
}
public static void printInfo(String info){
Log.e("JNI",info);
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在MainActivity中写native方法
native void perInfoBeanOption(PerInfoBean bean);
native-lib中的JNI方法,我做了详细的介绍,结尾处是注意事项
extern "C"
JNIEXPORT void JNICALL
Java_com_gzc_jnitest_MainActivity_perInfoBeanOption(JNIEnv *env,jobject instance,jobject jobj){
//1.获取java对应的class方法
jclass jPerInfoBean = env->GetObjectClass(jobj);
//2.找到要调用的方法。第二个参数:方法名 第三个参数:参数和返回值对应的签名
jmethodID jsetAge = env->GetMethodID(jPerInfoBean,"setAge","(I)V");
//3.调用方法
env->CallVoidMethod(jobj,jsetAge,100);
//我们再通过上述的方法调用getAge方法,看看值是否改变了
jmethodID jgetAge = env->GetMethodID(jPerInfoBean,"getAge","()I");
jint jage = env->CallIntMethod(jobj,jgetAge);
LOGE("修改后的age:%d",jage);
//static方法的调用,和普通方法步骤基本相同,只是在相应的JNI方法中带有static
//接受的参数是String类型,根据下面贴出的签名对照表可知是这样的书写方式
jmethodID jprintInfo = env->GetStaticMethodID(jPerInfoBean,"printInfo","(Ljava/lang/String;)V");
jstring jprintstr = env->NewStringUTF("static方法被调用");
//这里需要注意第一个参数和普通方法的不同
env->CallStaticVoidMethod(jPerInfoBean,jprintInfo,jprintstr);
//释放局部引用
env->DeleteLocalRef(jprintstr);
//创建JavaBean对象
//1.获取jclass
jclass jNewPerInfoBean = env->FindClass("com/gzc/jnitest/PerInfoBean");
//2.获取构造器方法
jmethodID jconstruct = env->GetMethodID(jNewPerInfoBean,"<init>","(IFLjava/lang/String;)V");
//3.创建对象
jstring jnewname = env->NewStringUTF("小海");
jobject jnewobj = env->NewObject(jNewPerInfoBean,jconstruct,24,173.0f,jnewname);
//若不再使用则删除
env->DeleteLocalRef(jnewobj);
env->DeleteLocalRef(jnewname);
env->DeleteLocalRef(jNewPerInfoBean);
//修改属性值
jfieldID jfieldage = env->GetFieldID(jPerInfoBean,"age","I");
env->SetIntField(jobj,jfieldage,30);
}
注意:
(1)获取JavaBean对象的相应方法时并不受修饰符的影响,无论是public还是private都能获得到
(2)注意无用对象的释放
(3)使用javap来获取指定方法的签名
第一步:cd到app\build\intermediates\classes\debug目录下
第二步:javap -s 包名.类名 注意:类名无后缀
基本数据类型的签名采用一系列大写字母来表示, 如下表所示:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。由 NewGlobalRef 函数创建
extern "C"
JNIEXPORT jstring JNICALL
Java_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
//正确
static jstring globalStr;
if(globalStr == NULL){
jstring str = env->NewStringUTF("C++字符串");
//删除全局引用调用 DeleteGlobalRef
globalStr = static_cast<jstring>(env->NewGlobalRef(str));
//可以释放,因为有了一个全局引用使用str,局部str也不会使用了
env->DeleteLocalRef(str);
}
return globalStr;
}
弱引用
与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。
在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象
extern "C"
JNIEXPORT jclass JNICALL
Java_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance) {
static jclass globalClazz = NULL;
//对于弱引用 如果引用的对象被回收返回 true,否则为false
//对于局部和全局引用则判断是否引用java的null对象
jboolean isEqual = env->IsSameObject(globalClazz, NULL);
if (globalClazz == NULL || isEqual) {
jclass clazz = env->GetObjectClass(instance);
//删除使用 DeleteWeakGlobalRef
globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz));
env->DeleteLocalRef(clazz);
}
return globalClazz;
}