NDK开发总结
前言
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字符指针。
4. 访问属性
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 层的类的属性来获取参数。
5. 调用方法
跟获取属性一样, 也要先获取 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 去执行对应的非静态和静态方法。
6. 捕获异常
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引用
引用知识前面的博客总结过了,这里就不写了