JNI是 Java Native Interface(Java 本地接口) 的缩写,它是为了方便Java调用C、C++等本地代码所封装的一层接口。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java虚拟机下。
NDK是android所提供的一个工具合集,通过NDK可以在android中更加方便地通过JNI来访问本地代码,比如C、C++。使用NDK有如下好处:
1. 提高代码的安全性。由于so库反编译比较困难,因此ndk提高了android程序的安全性。
2. 可以很方便地使用目前已有的C/C++开源库。
3. 便于平台间的移植。通过C/C++实现的动态库可以很方便地在其他平台上使用。
4. 提高程序在某些特定情形下的执行效率,但是并不能明显提升android程序的性能。
android studio现如今用于构建原生库的默认工具是CMake,所以本文就根据cmake来讲,至于如何配置cmake创建支持C/C++的新项目,以及向现有项目添加C/C++代码,就可以根据官方文档查看了解:向您的项目添加 C 和 C++ 代码
这里简单给个示例,代码实现地址:https://github.com/myloften/NDKSample ,这里简单说明下步骤(所用代码在上述链接中):
mTv1.setText(JniTest.test());
public static native String test();
static {
System.loadLibrary("myjni");
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_loften_ndksample_JniTest_test(JNIEnv *env, jclass type) {
return env->NewStringUTF("Hello World");
}
这里说的是如何在Android Studio配置快捷方式生成JNI头文件的方法:
Program:javah
/**
-bootclasspath $ModuleSdkPath$/platforms/android-24/android.jar表示引入路径,否则当参数或返回值有Android中的特有类型的时候会报找不到类的错误,请根据你自己的SDK进行修改指向的android.jar。(注:这里是在mac的环境下配置的,如在windows下配置则应将路径中的 "/" 换成 "\" )
*/
Parameter:-d src/main/jni/ -bootclasspath $ModuleSdkPath$/platforms/android-24/android.jar -classpath build/intermediates/classes/debug $FileClass$
Working directory:$ModuleFileDir$
具体使用:先Make下项目,生成中间文件,也就是类名.class文件。然后执行在你载入原生库的那个类(JniTest)右键->External Tools->Generate JNI Header File 。如果没报错的话,对应的C/C++头文件就生成在你项目的jni文件夹下(com_loften_ndksample_JniTest.h)
JNIEXPORT jstring JNICALL Java_com_loften_ndksample_JniTest_test
(JNIEnv *, jclass);
第一个参数JNIEnv是指向可用JNI函数表的接口指针;第二个参数则根据该方法是实例方法还是静态方法,静态方法jclass获取类引用,实例方法jobject则是该类实例的Java对象引用。
下面内容部分参考资Android C++高级编程
Java类型 | JNI类型 | C/C++类型 | 大小 |
---|---|---|---|
Boolean | Jblloean | unsigned char | 无符号8位 |
Byte | Jbyte | char | 有符号8位 |
Char | Jchar | unsigned short | 无符号16位 |
Short | Jshort | short | 有符号16位 |
Int | Jint | int | 有符号32位 |
Long | Jlong | long long | 有符号64位 |
Float | Jfloat | float | 32位 |
Double | Jdouble | double | 64位 |
Java类型 | JNI类型 |
---|---|
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
Other objects | jobjects |
java.lang.Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
Other arrays | Jarray |
后续都是以C++为主
//创建字符串
//C(创建UTF-8格式字符串)
jstring value = (*env)->NewStringUTF(env, "Hello World");
//C++(创建UTF-8格式字符串)
jstring value = env->NewStringUTF("Hello World");
//把Java字符串转换成C字符串
jstring value = env->NewStringUTF("Hello World");
const jchar *str;
jboolean isCopy;
str = (const jchar *) env->GetStringUTFChars(value, &isCopy);
if(0 != str){
printf("Java string: %s", str);
if(JNI_TRUE == isCopy){
printf("C string is a copy of the Java string.");
}else{
printf("C string points to actual string.");
}
}
//释放字符串
const char *value = env->GetStringUTFChars(value_, 0);
env->ReleaseStringUTFChars(value_, value);
//创建数组
jintArray javaArray = env->NewIntArray(10);
//将Java数组区复制到C数组中(当数组很大时,会引起性能问题)
jint nativeArray[10];
env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);
//对直接指针的操作
jint *nativeDirectArray;
jboolean isCopy;
nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
//释放指针(第四个参数是释放模式)
env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);
释放模式 | 动作 |
---|---|
0 | 将内容复制回来并释放原生数组 |
JNI_COMMIT | 将内容复制回来但是不释放原生数组,一般用于周期性地更新一个Java数组 |
JNI_ABORT | 释放原生数组但不用将内容复制回来 |
与数组 操作相比,NIO缓冲区的数据传送性能较好,更适合在原生代码和Java应用程序之间传送大量数据。
//创建直接字节缓冲区
unsigned char* buffer = (unsigned char*)malloc(1024);
jobject directBuffer = env->NewDirectByteBuffer(buffer, 1024);
//直接字节缓冲区获取
unsigned char* buffer = (unsigned char *) env->GetDirectBufferAddress(directBuffer);
注:原生方法中的内存分配超出了虚拟机的管理范围,且不能用虚拟机的垃圾回收器回收原生方法中的内存。原生函数英国通过释放未使用的内存分配以避免内存泄漏来正确管理内存。
Java有两类域:实例域和静态域。类的每个实例都有自己的实例域副本,而一个类的所有实例共享同一个静态域。
public class JavaClass{
//实例域
private String instanceField = "Instance Field";
//静态域
private static String staticField = "Static Field";
}
/**
* 获取域ID
*/
//用对象引用获得类
jclass clazz = env->GetObjectClass(instance);
//获取实例域的域ID
jfieldID instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String;");
//获取静态域的域ID
jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
/**
* 获取域
*/
//获得实例域名
string instanceField = env->GetObjectField(instance, instanceFieldId);
//获得静态域
string staticField = env->GetStaticObjectField(clazz, staticFieldId);
Java中有两类方法:实例方法和静态方法,JNI提供访问两类方法的函数。
public class JavaClass{
//实例方法
private String instanceMethod(){
return "Instance Method";
}
//静态方法
private static String staticMethod(){
return "Static Method";
}
}
/*
* 获取方法ID
*/
//获取实例方法的方法ID
jmethodID instanceMethodId = env->GetMethodID(clazz, "instanceMethod", "()Ljava/lang/String;");
//获取静态方法的方法ID
jmethodID staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");
注:与字段ID获取方法一样,两个函数的最后一个参数均表示方法描述符,在java中它表示方法签名。
/*
* 调用方法
*/
//调用实例方法
jstring instanceMethodResult = env->ClassStringMethod(instance, instanceMethodId);
//调用静态方法
jstring staticMethodResult = env->CallStaticStringMethod(clazz, staticMethodId);
获取域ID和方法ID均分别需要域描述符和方法描述符,域描述符和方法描述符均可通过下表的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要求在异常发生后显式地实现异常处理流
/*
*抛出异常的Java例子
*/
public class JavaClass{
//抛出方法
private void throwingMethod() throws NullPointerException{
throw new NullPointerException("Null pointer");
}
//访问方法(原生方法)
private native void accessMethods();
}
调用throwingMethod方法时,accessMethods原生方法需要显式地做异常处理。JNI提供了ExceptionOccurred函数查询虚拟机中是否有挂起的异常。在使用完之后,异常处理程序需要用ExceptionClear函数显式地清除异常
//原生代码中的异常处理
throwable ex;
...
env->CallVoidMethod(instance, throwingMethodId);
ex = env->ExceptionOccurred();
if( 0!=ex ){
env->ExceptionClear();
}
JNI也允许原生代码抛出异常。因为异常是Java类,应先用FindClass函数找到异常类,用ThrowNew函数可以初始化且抛出新的异常
//原生代码中抛出异常
jclass clazz;
...
clazz = env->FindClass("java/lang/NullPointerException");
if(0 != clazz){
env->ThrowNew(clazz, "Exception message");
}
因为原生函数的代码执行不受虚拟机的控制,因此抛出异常并不会停止原生函数的执行并把控制权转交给异常处理程序。到抛出异常时,原生函数应该释放所有已分配的原生资源,例如内存及合适的返回值等。通过JNIEnv接口获得的引用时局部引用且一旦返回原生函数,它们自动地被虚拟机释放。
//删除一个局部引用
jclass clazz;
class = env->FindClass("java/lang/String");
...
env->DeleteLocalRef(clazz);
//创建全局引用
jclass localClazz;
jclass globalClazz;
...
localClazz = env->FindClass("java/lang/String");
globalClazz = env->NewGlobalRef(localClazz);
...
//删除全局引用
env->DeleteLocalRef(localClazz);
//弱全局引用
jclass weakGlobalClazz;
weakGlobalClazz = env->NewWeakGlobalRef(localClazz);
//判断弱引用是否仍然指向活动的类实例
if(JNI_FALSE == env->IsSameObject(weakGlobalClazz, NULL)){
//对象仍然处于活动状态且可以使用
} else{
//对象被垃圾回收器收回,不能使用
}
//删除
env->DeleteWeakGlobalRef(weakGlobalClazz);
//Java 同步代码块
synchronized(obj){
//同步线程安全代码块
}
//Java同步代码块的原生等价
if(JNI_OK == env->MonitorEnter(obj)){
//错误处理
}
/* 同步线程安全代码块 */
if(JNI_OK == env->MonitorExit(obj)){
//错误处理
}
/*
* 原生线程
*/
//将当前线程与虚拟机附着和分离
JavaVM* cachedJvm;
...
JNIEnv* env;
...
//将当前线程附着到虚拟机
(*cachedJvm).AttachCurrentThread(&env, NULL);
/* 可以用JNIEnv 接口实现线程与Java应用程序的通信 */
//将当前线程与虚拟机分离
(*cachedJvm).DetachCurrentThread();
看了上述基础,明白了一个道理,就是 == 继续踩坑吧,少年!