NDK开发差不多结束了, 估计后面也不会再碰了诶, 想着还是写个总结什么的,以后捡起来也方便哈。既然是总结,我这里就不会谈具体的细节,只会记录下我觉得重要的东西, 所以这篇随笔不是为萌新学习新知识准备的, 而是复习用的, 有些知识默认读者知道,就算忘了也能根据提示想起来。这里虽然是总结有些地方还是很细的2333.
方法论:
1、 我在实践中大概是这样的流程, 想好大概的java和jni代码交互流程, 然后编写jni接口代码, 然后在接口代码里面调用c++或者c写的方法, 如果不跨线程的话, 我会传JNIEnv指针给本地代码层。这样相当于分了三层, java层, 中间层, 本地层, 这里的中间层指的按照jni规范命名的方法, 本地层不考虑java层逻辑, 而是设计的实现中间层逻辑的各种类的集合。
2、有些项目可能会使用三方的c/c++ sdk, 这些sdk可能并没有按java和jni交互的规范设计, 所以java层无法直接调用sdk里面的方法, 但是计算机里面有个重要的方法, 什么问题都能够通过加个中间层解决, 也可以认为是设计模式里面的适配器思想的范版,具体方法是 我们可以在自己的c/c++代码里面封装第三方的sdk, 然后java层调用我们的c/c++代码来间接的使用三方的sdk的效果。
知识点:
一、Java和c/c++接口:
本地方法通过native关键字来定义, 暗示编译器这个方法的通过其他语言实现, 这个方法通过分号终止, 因为本地方法没有方法体。
虽然我们定义了本地方法, 但是窝们还没有告诉java虚拟机怎么找到这个方法的实现, 这是后我们就要通过下面 这种方式告诉虚拟机去加载哪个动态库了。
static{
System.loadLibrary("hello-jni");
}
loadLibrary方法在静态代码块里面调用, 因为我们想本地方法在类被加载,第一次被初始化的时候动态库能够加载进来了。
java技术的一个设计目标是平台无关性, java框架的api作为一部分, loadLibrary的设计也一样, 这里动态库的名字是libhello-jni.so, 但是在这个方法里面只需要写库的名字就行了, 也就是模块的名字(), hello-jni, 系统在用的时候会添加前缀和后缀。 loadlibrary搜索的路径在System property里面的key java.library.path里面定义了, loadLibrary方法会搜索这个列表寻找动态库.java library的路径在android里面是 /vendor/lib 和 /system/lib;
要想虚拟机正确的找到本地方法,本地方法需要按照严格的规则命名函数, 这样虚拟机才能找到。
栗子:
java:
package com.demo;
class Sample{
static{
System.loadlibrary("hello-jni");
}
public native String stringFromJNI();
}
ndk:
jstring Java_com_demo_Sample(JNIEnv *env, jobject thiz){};
名为stringFromJNI的本地方法, 在c/c++层有一个精确的c层方法对, Java_com_demo_Sample, 试想如果java层方法和c层方法的名称没有精确的规则对应,虚拟机根据java层本地方法拿什么去匹配c/c++层代码, 或者设计者可以设计用注解注明c层代码名, 但是设计者没有这么做。
二、 数据类型
我们都知道java有两种数据类型
* 原始类型: boolean, byte, char, short, int, long, float, double
* 引用数据类型: String, 或者其他的类
1、原始类型
java原始类型和c类型对比
JavaType | JNIType | C/C++Type | Size |
Boolean | jboolean | unsigned char | unsigned 8 bits |
Byte | jbyte | char | singned 8 bits |
Char | jchar | unsigned short | unsigned 16 bits |
Short | jshort | short | signed 16 bits |
int | jint | int | signed 32 bits |
Long | jlong | long long | signed 64 bits |
Float | jfloat | float | 32 bits |
Double | jdouble | double | 64 bits |
2、java引用类型
java type | Native Type |
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
other object | jobject |
java.lang.Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
float[] | jfloatArray |
double[] | jdoubleArray |
other arrays | jarray |
原始类型在c/c++里面是直接可以使用的, 因为他们对应着c/c++里面的类型, 但是引用类型c/c++不可以直接操作, 如果想操作的话必须使用JNI提供的接口去操作这些引用类型。
四、引用类型操作
1. 字符串操作
创建String
jstring javastring = env->NewStringUTF("Hello world!");
如果内存不够用了, 这个方法将会返回NULL, 同事虚拟机会抛出一个异常, 所以我们的方法应该返回而不应该继续处理;
2. java字符串转C 字符串
const jbyte* str;
jboolean iscopy;
str = env->GetStringUTFChars(javastring, &iscopy);
if (NULL != str){
printf("java string:%s", str);
if ( JNI_TRUE == iscopy){
printf("this c string is copy from java string.");
}else{
printf("c string is one width java string.");
}
}
注意GetStringChars 和GetStringUTFChars 方法需要调用ReleaseStringChars和ReleaseStringUTFChars 释放内存,有一个设计规则,谁申请的内存,那么谁就赋值释放, 这里调用env获得字符串的过程中,env申请了内存,所以我们要调用env的方法去释放它。
3. 数组操作(注意数组是引用类型)
新建一个数组可以调用本地方法,类似于New<Type>Array 方法的形式构建, <Type>可以使int, char, boolean等等,比如NewIntArray;
jintArray javaArray;
javaArray = env->NewIntArray(10);
if (NULL != javaArray){
...
}
和NewString 方法类似, 如果内存不够用了, 那么New<Type>Array 方法将会返回NULL, 虚拟机将会抛出异常, 所以本地方法应该要立刻返回,而不应该继续执行了。
--操作数组元素
调用Get<Type>ArrayRegion方法可以复制一个java的原始类型数组成为对应的C数组. 可能有人会想,原始类型数组肿么操作要这么麻烦, 还要转成jni对应数组才行啊, 如果这么想的话,那么可能你忘了java数组是引用类型的事情, 引用类型我们是不能再c里面操作的, 但是窝们可以操作原始类型, 所以将java原始类型数组转化成jni 类型数组, 我们就可以做对应操作了。
jint nativeArray[10];
env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);
当然, get到了数据做完修改我们也会需要set回去咯, 这时候调用Set<Type>ArrayRegion方法就可以了,嘛, 这里设计的还是很对称的啦。
注意一点, 当数组很大的时候, 复制数组会造成性能问题, 所以我们应该get我们需要修改的范围,然后设置回去, 当然Jni提供了一系列不同的方法,可以直接通过指针的方式操作数组, 而不用复制他们。
---直接通过指针操作数组
Get<Type>ArrayElements 方法 允许本地代码直接通过指针操作数组元素, isCopy允许调用者声明是否返回一个c数组指针指向复制或者在堆空间上的固定数组。
jint *nativeDirectArray;
jboolean iscopy;
nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
同样的,我们需要调用Release<Type>ArrayElements方法去释放内存, 否则会造成内存泄漏。
比如不用的时候应该调用env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);
第三个参数可以是下面的值:
Release Mode | Action |
0 | Copy back the content and free the native array |
JNI_COMMIT |
Copy back the content but do not freee the array. This can be used for periodically updating a Java array |
JNI_ABORT | free the native array without copyting its content. |
---直接新建一个字节缓冲区
本地代码可以直接创建一个字节缓冲区, 这个缓冲区可以给java应用直接使用, 缓冲区的内容直接使用c/c++层字节数组。
unsigned char * buffer = (unsigned char *) (unsigned char *) malloc(1024);
....
jobject directBuffer;
directBuffer = env->NewDirectByteBuffer(buffer, 1024);
注意:
当然这里的内存不是由java虚拟机申请的了, 所以本地代码需要自己管理这些分配的内存。比如我们可以写个recycle的本地方法,在java层调用这个方法释放内存。
同理我们也可以获得java应用创建的字节缓冲区。调用GetDirectBufferAddress方法会返回一个c字符指针。
访问属性:
java有两种类型的属性, 实例的属性和静态属性, 每种属性都有对应的方法获取。
其实步骤都是获取对应的属性的id, 然后获取属性值。
JNI提供了方法去获得者两种属性例:
public class JavaClass{
private String instanceField = "instance Field";
private static String staticField = "static Field";
}
1) 获取非静态属性id
jfieldID instanceFieldId;
instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String");
2) 获取静态属性id
jfieldID staticFieldId;
staticFieldId = env->GetStaticFieldId(clazz, "staticField", "Ljava/lang/String;");
最后一个参数是属性的描述, 这个是java虚拟机规范里面的, 可以看下我前面的博客查查肿么写。
获取属性通过Get<Type>Field, 或者GetStatic<Type>Field方法得到, type是属性的类型。 如果内存满了, 者两个会返回NULL。
小提示:
获取一个属性需要调用2个或者3个JNI方法的调用, 建议尽量在本地方法里面获取参数,然后返回到java层, 尽量少的直接用java层的类的属性来获取参数。
调用方法:
跟获取属性一样, 也要先获取id, 然后才能执行方法, 我们有两种获取方法id的方式, 一种是对class的,也就是静态方法的id,一种是实例的,也就是非静态方法的id.
public class JavaClass{
private String instanceMethod(){
return "Instance Method";
}
private static String staticMethod(){
rerturn "static Method";
}
}
jmethodID instanceMethodId;
instanceMethodId = env->GetMethod(clazz, "instanceMethod", "()Ljava/lang/String;");
jmethodID staticMethodId;
staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");
和方法id一样, 最后一个参数是方法的描述, 也就是方法签名, 同样的是java虚拟机规范。
接下来就是根据方法id调用方法了,同样使用 Call<Type>Method,或者CallStatic<Type>Method去执行对应的非静态和静态方法。
捕获异常:
java里面是有异常机制的,如果我本地执行java代码, java代码里面抛出了异常, 本地方法这么处理呢? java JNIEnv接口提供了一系列方法来处理异常, 现在来总结下:
public class JavaClass{
private void throwingMethod() throws NullPointerException{
throw new NullPointerException("Null pointer");
}
private nativve void accessMethods();
}
如果我们在accessMethods的本地方法里面调用了throwingMethod方法, 那么我们本地代码里面就要精确的处理throwingMethod方法可能产生的异常。
首先我们肿么会想到, 本地代码里面肿么抛出异常呢, 比如我们定义了一个可以抛出异常的本地方法, 辣么我们实现本地方法的时候肿么抛出异常呢?
jthrowable ex;
..
env->CallVoidMethod(instance, throwingMethodId);
ex = env->ExceptionOccurred();
if (NULL != ex){
env->ExceptionClear();
}
JNI提供了ExceptionOccurred方法去查询虚拟机是否有异常抛出, 本地异常处理需要精确使用ExceptionClear方法来清除异常
问题来啦, 我们肿么在本地代码里面抛出异常呢?
jclass clazz;
...
clazz = env->FindClass("java/lang/NullPointerException"); //这里的参数是java类的内部名, 不要和签名弄混哦
if(NULL != clazz){
env->ThrowNew(clazz, "Exception message.");
}
由于本地代码不归虚拟机控制, 所以啊, 抛出异常后, 我们的方法不应该继续有其他操作了,而是应该返回同时释放本地引用和资源。
后面的只是提一下了:
java里面的关键字Synchronized,肿么 在本地代码实现呢?
例:
if(JNI_OK == env->MonitorEnter(obj)){
//错误处理
}
//同步代码
if (JNI_OK == env->MonitorExit()){
//错误处理
}
五、本地线程:
本地代码产生的线程java虚拟机是不知道的, 所以JNIEnv是不能跨线程使用的, 如果要使用的话我们需要将本地线程贴到java虚拟机上,去重新获得JNIEnv指针。不过java虚拟机是是可以跨线程的, 所以JavaVM指针是可以全局共享的。
JavaVM* cachedJvm;
..
JNIEnv* env;
//Attach
cachedJvm->AttachCurrentThread(cachedJvm, &env, NULL);
//现在线程可以通过JNIEnv和Java应用交互了
//Detach
cachedJvm->DetachCurrentThread();
话说JavaVm肿么获得呢?
其实只有在本地代码中注册一个回调就可以了, 本地代码在加载的时候会自动执行这个方法。
JavaVM *cachedJvm;
jint JNI_OnLoad(JavaVM *vm, void *reserved){
g_jvm = vm;
if (JNI_OK != vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_4)){
return JNI_ERR;
}
return JNI_VERSION_1_4;
}
JNI引用:
引用知识前面的博客总结过了,这里就不写了