提到NDK,相信有很多想进阶Android的朋友一听到这个名字都感觉有点头痛,但是你放心,这仅仅是噩梦的开始。
要说NDK的配置,主要是为了配合android开发的cpu等硬件问题而配置的,其实这些不是重点,要记住这些都是属性配置而已。真正有问题的地方就是要编写这些JNI的C,C++部分。
JNI是Java NativeInterface,就是java跟C,C++的接口,在jdk里面就已经有存在的两个头文件。在jdk安装路径的include文件下有一个jni.h,还有一个jni_md.h。在android中,主要用jni.h就够了,也就是加入C++ support的时候,默认的.cpp文件会自动加载了jni.h。
jni.h中定义了很多java和C的基本数据类型对照表,还有定义了一些相互调用的方法。
在java中要定义C/C++的方法要加关键字native,例如:
public native void sayHello();
在C/C++中定义这个函数名字要遵循这样的格式:
JNIEXPORT JNI类型返回值 JNICALL
Java_包名_类名_方法名(JNIEnv*env,各种参数)
就上面的例子而言,就要写成
JNIEXPORT void JNICALL
Java_com_arjinmc_ndkdemo_JNIUtils_sayHello(JNIEnv*env)
觉得很麻烦,没事,java提供了一个命令可以让把这些了名生成出来,只要用端口打开到编译好的classes目录下,找到你定义native方法的这个java类,然后输入javah 类名就可以看到在jni/cpp目录下自动生成了该类的头文件.h,打开就能看到这些方法名。但是,也可以不用这么麻烦,因为Android Studio 2.2开始已经自动识别到native关键字,在这个native java方法按下alter+enter就会自动生成该方法对应的C/C++方法。但是要注意,在cpp文件中,每个方法名字前面都要加上extern “C”,这样保证兼容C的方法。
不管是Java调用C/C++,还是C/C++调用java,上面两个内容都必须包含。
JNIEnv *env 是一级指针,在C++中可以直接使用,但是要在C中使用,env在C中是二级指针,所以要写成(*env)。
以下这个表是java,jni,c/c++对应的基本数据类型。jni就充当着两个派系语言的翻译官。
Java 类型 | jni 类型 | 实际表示的 C类型 (Win32) | 说明 |
boolean | jboolean | unsigned char | 无符号,8 位 |
byte | jbyte | signed char | 有符号,8 位 |
char | jchar | unsigned short | 无符号,16 位 |
short | jshort | short | 有符号,16 位 |
int | jint | long | 有符号,32 位 |
long | jlong | __int64 | 有符号,64 位 |
float | jfloat | float | 32 位 |
double | jdouble | double | 64 位 |
void | void | N/A | N/A |
java调用C/C++主要步骤:
C/C++从java所对应过来的函数中对应了的参数都是jni的类型。因此,需要jni定义的一些方法去做java和C/C++数据类型的切换。
GetStringChars和GetStringUTFChars
jboolean: JNI_TRUE,JNI_FALSE
const jchar* GetStringChars(jstringstr,jboolean* copied)
会把java的string复制到新的内存,获取到该指针。
const char* GetStringUTFChars(jstringstr,jboolean* copied)
直接获取java的string的内存指针。
不用的时候释放内存调用ReleaseStringChars,ReleaseStringUTFChars。
GetStringCritical/ReleaseStringCritical
得到字符串的指针,禁止GC。如果禁止GC时被GC了,会造成阻塞,死锁。
jchar* GetStringCritical(jstring str,jboolean*copied)
GetStringRegion/GetStringUTFRegion
先在内存创建空间,再复制。
//utf-16
GetStringRegion(jstring str,jsizestart,jsize len,char* buffer)
//utf-8
GetStringUTFRegion(jstring str,jsizestart,jsize len,char* buffer)
其他字符串相关用函数
新建
jstring NewString(const jchar* str,jsizelen);
jstring NewStringUTF(const char* str);
获取字符串大小
jszie GetStringLength(jstring str);
jsize GetStringUTFLength(jstring str);
处理数组
基本类型
Get<Type>ArrayElements(<Type>Arrayarr,jboolean* isCopied)
Release<Type>ArrayElements(<Type>Arrayarr,<Type>* array,jint mode)
mode:
0进行更新并释放
JNI_COMMIT 进行更但不释放
JNI_ABORT 不更新,释放
GetPrimitiveArrayCritical(jarryarr,jboolean* isCopied)
ReleasePrimitiveArrayCritical(jarrayarr,void* array,jint mode)
得到数组的指针,禁止GC。如果禁止GC时被GC了,会造成阻塞,死锁。
Get<Type>ArrayRegion(<Type>Arrayarr,jsize start,jsize len,<Type>* buffer);
先在内存申请空间,在复制过去.
对象类型
Get/SetObjectArrayElement
C/C++调用Java的步骤:
1.获取到java的类名;
2.获取到java类中的方法名id;
3.通过env->Callxxxx调用java的方法。
获取java类名
jclass FindClass(const char* clsName);
jclass GetObjectClass(jobject obj);
jclass GetSuperClass(jclass obj); //获取父类名
获取java类名中的方法id
jmethodID GetMethodID(jclass clazz,const char* name,const cahr*sign);
jmethodID GetStaticMethodID(jclass clazz,const char* name,constcahr* sign);
例:获取构构造方法
jmethodID env->GetMethodID(clazz,”<init”>,”()V”);
构造方法比较特殊,必须是<init>,其他方法直接写方法名称。最后一个参数是方法的签名,签名表如下:
类型 | 签名 |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | L |
float | F |
double | D |
void | V |
object | L开头,完整类名用/切割,最后加上; |
Array | [签名 |
Method | (参数1签名…参数签名n)返回值类型签名 |
调用java方法
通过env->CallXXXXMethod(jclass/jobject,jmethodID,param1,param2….)
要注意的是,调用java的方法,不能调用过于复杂的,比如返回值是数组类型,自定义类型的java方法是无法call出来。所以,可以定义成void方法,这样C/C++可以直接通过内存去修改java的参数的值。
还有其他可以让C/C++去修改java类的属性
获取java类属性的id
jfieldID GetFieldID(jclass clazz,const char* name,const cahr* sign);
jfieldID GetStaticFieldID(jclass clazz,const char* name,const cahr*sign);
get/set java类属性
Set<Type>Field
Get<Type>Field
SetStatic<Type>Field
GetStatic<Type>Field
JNI基本内容已经说完,剩下都是C/C++的问题,自己琢磨吧。
我写了一个Demo,有兴趣下载看看: