转自:http://zhiwei.neatooo.com/blog/detail?blog=513835c2bb0201464b000004
Android NDK开发简介
其实NDK的开发并不复杂,就入门而言甚至可以说是easy job,觉得它难是难于C/C++代码的编写与调试。这个是我最近从事NDK开发的一点感受!
首先,我们要弄懂几个概念,何为NDK,它和SDK以及JNI有什么关系?请前看下图:
JNI (Java Native Interface),Java的本地接口
JNI是Java众多开发技术中的一门,意在利用本地代码,为Java程序提供更高效,更灵活的拓展。应用场景包括:对运行效率敏感的算法实现、跨平台应用移植、调用系统的底层驱动、调用硬件等。尽管Java一贯以其良好的跨平台性而著称,但真正的跨平台之王,应该是C/C++,因为当前世上90%的系统都是基于C/C++编写的。Java的跨平台,是以牺牲效率换来对多种平台的兼容性,因而JNI可以说是Java短板的补充!举一例子说明,当前流行的移动操作系统Android,一直被说系统操作的流畅性不如IOS,原因在于Android的App是基于Java开发的,IOS的是基于Object-C开发的,区别在于同样的操作,在IOS上一条指令完成,在Android上则需要多大三条指令才能完成(数据来自于网络,不一定准确)!于是在AndroidJellyBean版本中,Google为其引入ProjectButter(黄油计划),在应用层大量使用了本地库,并优化了系统的架构,以提升Android系统整体的操作反应!
咔咔,JNI的介绍就先说到这里,总之,JNI是一门技术,是Java Code和C/C++ Code联系的桥梁!
JNI开发的流程
1、编写Java Code,如下面的例子:
publicclassMainActivityextendsActivity {
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(getApplicationContext(),sayHellow(),Toast.LENGTH_LONG).show();
}
publicnativeString sayHellow(); //调用本地方法
static {
System.loadLibrary("Scgps_Client"); //加载本地共享库
}
}
2、编写C/C++ Code,如下面的例子:
#include"string.h"
#include"jni.h"
JNIEXPORT jstringJNICALL Java_com_scgps_client_MainActivity_sayHellow(JNIEnv* env, jobject thiz)
{
constchar * ret ="Hellow Form Ndk";
return (*env)->NewStringUTF(env, ret);
}
3、编译C/C++ Code,成功并得到本地共享库
本地共享库是Linux下的叫法,文件扩展名是.so,windows下叫动态链接库,文件扩展名是.dll。前面说到C/C++才是跨平台之王,这就是其中的道理,面对不同的平台,编译不同的结果。相对于Java的一次编译到处运行的跨平台性牺牲运行效率,C/C++的跨平台性则是牺牲编译时间以及编译的难度。这里的编译难度是指为适应不同平台而做的编译过程的调整,这个活的难度可大可小,还不一定成功,视乎平台的兼容性以及支持。说到这里,难免会有人喷了:说什么跨平台性,这么复杂还不稳定!的确C/C++的跨平台性是有局限性的,但是纵观当前的各种平台和系统,有哪家是不支持C/C++本地开发的?只是各自提供的底层API和编译条件不同而已,只需要调整一下C/C++的编译代码,通过编译即可运行,难道也不是一件美事?
4、编译并打包Java
把本地共享库放置到Java项目的指定目录,一般是libs文件件,Android的项目是libs/armeabi(armeabi是对应的平台,后面会详讲),然后编译Java的代码即可运行!
NDK,(Native develop kit),本地开发工具包
NDK是Google为Android进行本地开发而放出的一个本地开发工具,包括Android的Native API、公共库以及编译工具,注意,NDK需要Android 1.5版本以上的支持哦。
按照上图的解说,NDK处在开发流程的编译环节,对,简单来说,NDK是JNI开发的一个扩展工具包!针对Android平台,其支持的设备型号繁多,单单就设备的核心CPU而言,都有三大类:ARM、x86和MIPS,况且ARM又分为ARMv5和ARMv7等等,为何Android又能适配如此之多的设备?接着JNI开发流程的话,利用NDK,我们可以针对不同的手机设备,编译出对应可运行的本地共享库了,至于如何使用NDK进行编译、开发,我们留作下次再进行探讨。
SDK,(Standard Develop Kit),标准开发包
SDK是Google提供的Android标准开发工具包,里面包含了完整的API文档,各Android版本的开发库,Android的虚拟机以及Android的打包工具等。众所周知,Android的应用开发语言是Java,App的运行时是Delvik Runtime,属于JVM的改良版本,官方说Delvik VM更适用于移动设备。一般而言,由于Google的SDK提供了强大又完善的API,开发一般需求的应用,SDK足矣。然而前面已经说过,Java的运行效率引发了不少问题,因而才有了JNI技术的存在,那SDK和NDK的关系是怎样的呢?见下图解说,可以说,NDK是SDK的一个补充。
SDK,JNI,NDK的开发流程
这个开发流程大致与JNI的开发流程差不多,下面我再详细说明一下每个环节:
SDK开发,编写Java代码,调用各种Android的API实现功能,编写含有native关键字的代码开始JNI;
JNI开发,按照 JNI编码规范,编写与Java交互的本地代码,一般就是数据类型的转换,把C/C++的数据类转换成Java能识别的,或反过来。也因为这样子,我认为JNI其实就是Adapter,作为数据转换层而存在,具体JNI的一般操作,我之后再分享;
C/C++开发,编码实现业务逻辑,或调用NDK提供的本地API或库,完成Android平台上特定功能的开发、封装;
NDK编译,编写.mk文件,编译调试,最后修改.mk文件,针对特定的平台(ARM/x86)做编译结果的优化;
最后就是SDK编译、打包,上真机调试了...
http://zhiwei.neatooo.com/blog/detail?blog=514294b275363a521d000003
Android NDK开发之Jni的数据类型
在前面的一篇博客《Android NDK开发简介》,我简单地说明了Android NDK开发的流程,以及其重要的一环:JNI层得开发。今天我再详细说明一下自己的学习经验。
JNI是Java代码和C/C++代码通信的桥梁,其角色在某种意义上就是一个翻译员,从设计模式来看叫适配器。
两者的沟通,首要的一定要对嘴型,对channel,沟通才能到位。计算机程序的基本组成,从狭义来讲,就是数据结构+算法。由于Java和C/C++是两种不同的编程语言,它们各自拥有自家定义的数据类型和结构。JNI的第一步就是统一转换其中一方的数据类型,这就好比我们跟外国友人沟通,我们得说英语一样子。下表是Java的8大基本类型,在Jni层对应的数据描述:
Java | Native(jni.h) |
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
复杂一点的对象类型,其对应的数据描述如下图:
这里补充说明一下:
1. Java中的返回值void和JNI中的void是完全对应的
2. Java中的基本数据类型(boolean, byte, char, short, int, long, float, double),在JNI中对应的数据类型只要在前面加上 j 就对应了(jboolean, jbyte, jchar, jshort, jint, jlong, jfloat,jdouble)
3. Java中的对象,包括类库中定义的类、接口以及自定义的类接口,都对应于JNI中的 jobject
4. Java中基本数据类型的数组对应与JNI中的 jarray 类型。(type就是上面说的8种基本数据类型)
5. Java中对象的数组对应于JNI中的 jobjectArray 类型。(在Java中一切对象、接口以及数组都是对象)
关于数据类型的转换,JNI还提供的强悍的函数库来支持。对于基本的类型的转换,我们先来复习一下,先关注一下Java基本类型的精度。
类型 | 字节数 | 范围/精度 |
float | 4 | 32位IEEE754单精度 |
double | 8 | 64位IEEE754双精度 |
byte | 1 | -128到127 |
short | 2 | -32,768到32,767 |
int | 4 | -2,147,483,648到2,147,483,647 |
long | 8 | -9,223,372,036,854,775,808到9,223,372,036,854,775,807 |
char | 2 | 整个Unicode字符集 |
boolean | 1 | True或者false |
Java 的基本数据类型是不存在有符号和无符号这种概念的,它们的取值范围是固定的,不会随着机器硬件环境或者操作系统的改变而改变。
像 byte的范围是 -128 到 127,你想要变为 0到255怎么办,,跟 0XFF做与运算就可以了:b &0XFF
如 byte b,如果你想赋值它值 255,那是不行的,就算赋值了,b的值也是 255 对 256 求模后的值 -1,即 b = -1, 然后 b & 0XFF结果即为 255,这个与运算后的结果会隐式转换为int类型的,因为 byte放不下了,与运算还是很快的,,比加减法还快的。
所以Jni层使用Java的基本类型数据,对于上面八种基本的数据类型,jni层的c/c++代码可以用强制直接转换成对应长度的c/c++类型数据。
如:unsigned chartmp = (unsigned char) m_jboolean;
unsigned short tmp =(unsigned short) m_jchar;
或者同长度类型的数据,可以直接赋值,int tmp =m_jint;
http://zhiwei.neatooo.com/blog/detail?blog=5142c98375363a521d000005
Android NDK开发之数组类型的操作
Jni 可以通过JNIEnv提供的方法,对传过来的Java数组进行相应的操作。它提供了两种函数:一种是操作Java的简单型数组的,另一种是操作对象类型数组的。
操作Java的简单型数组
因为速度的原因,简单类型的Java数组,会作为指向本地类型的指针暴露给本地代码调用。因此,它们能作为常规的数组存取。这个指针是指向实际的Java数组或者Java数组的拷贝的指针。另外,数组的布置保证匹配本地类型。
在C/C++中,jintArray不能用下标对其进行直接存取,必须用到JNI中提供的接口函数进行操作。为了存取Java简单类型的数组,就要要使用GetXXXArrayElements函数(见表),XXX代表了数组的类型。这个函数把Java数组看成参数,返回一个指向对应的本地类型的数组的指针。
函数 | Java 数组类型 | 本地类型 |
GetBooleanArrayElements | jbooleanArray | jboolean |
GetByteArrayElements | jbyteArray | jbyte |
GetCharArrayElements | jcharArray | jchar |
GetShortArrayElements | jshortArray | jshort |
GetIntArrayElements | jintArray | jint |
GetLongArrayElements | jlongArray | jlong |
GetFloatArrayElements | jfloatArray | jfloat |
GetDoubleArrayElements | jdoubleArray | jdouble |
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv*env, jobject obj, jintArray arr)
{
jint *carr;
carr =(*env)->GetIntArrayElements(env, arr,false); //获得Java数组arr的引用的指针
if(carr == NULL){
return0;/* exception occurred */
}
jint sum =0;
for(int i=0; i<10; i++){
sum += carr[i];
}
(*env)->ReleaseIntArrayElements(env, arr, carr,0);
return sum;
}
操作对象类型数组
在C/C++代码中,int类型的数组对应JNI中的jintArray,而类似字符串数组这种类型的,在Jni里对应的使用 jobjectArray 来声明,下面是存取访问 jobjectArray的方法简介:
GetObjectArrayElement(JNIEnv*env, jobjectArray array, jsize index)
array: a reference to the java.lang.Object array from which the element will be accessed.
index: the array index
功能:返回对应索引值的object.返回的是一个数组元素的值。
SetObjectArrayElement(JNIEnv* env, jobjectArray array, jsize index, jobject value)
array: a reference to an array whose element will be accessed.
index: index of the array element to be accessed.
value: the new value of the array element.
功能:用来设置对应索引元素的值。
Get/SetXXXArrayRegion函数说明
GetIntArrayRegion(array,jsize start,jsize len,*buf)
array:a reference to an array whose elements are to be copied.
start:the starting index of the array elements to be copied.
len: the number of elements to be copied.
buf: the destination buffer.
功能:把jintArray中的元素复制到buffer中。
SetIntArrayRegion(array,jsize start,jsize len,*buf)
array:a reference to a primitive array to which the elements to be copied.
start:the starting index in the primitive array.
len:the number of elements to be copied.
buf:the source buffer.
功能:把buf中的元素copy到jintArray中去。
使用SetXXXArrayRegion与GetXXXArrayRegion就是以复制的方式设置与取出Array数组中的某个值。
关于二维数组和String数组
在Jni中,二维数组和String数组都被视为object数组,因为Array和String被视为object。下面例子实现了构造并返回一个二维int数组:
JNIEXPORT jobjectArray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv*env, jclass cls,int size)
{
jobjectArray result;
jclass intArrCls=(*env)->FindClass(env,"[I"); //int数组的class
result =(*env)->NewObjectArray(env, size,intArrCls, NULL); //二维int数组的实例
for(int i =0; i < size; i++){ //初始化
jint tmp[256]; /* make sure it is large enough! */
for(int j =0; j < size; j++){
tmp[j]= i + j;
}
jintArray iarr =(*env)->NewIntArray(env, size);
(*env)->SetIntArrayRegion(env, iarr,0, size, tmp); //将tmp复制到iarr中
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}
最后得特别说明一下,当你使用对数组进行访问后,要确保调用相应的ReleaseXXXArrayElements函数,参数是对应Java数组和GetXXXArrayElements返回的指针,如果必要的话,这个释放函数会复制你做的任何变化(这样它们就反射到java数组),然后释放所有相关的资源,避免发生内存泄漏。
http://zhiwei.neatooo.com/blog/detail?blog=5142d8da75363a521d000006
Android NDK开发之Jni调用Java对象
本地代码中使用Java对象
通过使用合适的JNI函数,你可以创建Java对象,get、set静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。
下表列出了用以得到静态(static)和实例(instance)的域与方法的JNI函数。每个函数接受(作为参数)域或方法的类,它们的名称,符号和它们对应返回的jfieldID或jmethodID。
函数 | 描述 |
GetFieldID | 得到一个实例的域的ID |
GetStaticFieldID | 得到一个静态的域的ID |
GetMethodID | 得到一个实例的方法的ID |
GetStaticMethodID | 得到一个静态方法的ID |
构造一个Java对象的实例
jclass cls =(*env)->FindClass(env,"Lpackagename/classname;"); //创建一个class的引用
jmethodID id =(*env)->GetMethodID(env, cls,"","(D)V"); //注意这里方法的名称是"",它表示这是一个构造函数,而且构造参数是double型的
jobject obj =(*env)->NewObjectA(env, cls, id, args); //获得一实例,args是构造函数的参数,它是一个jvalue*类型。
首先是获得一个Java类的class引用 (*env)->FindClass(env,"Lpackagename/classname;"); 请注意参数:Lpackagename/classname; ,L代表这是在描述一个对象类型,packagename/classname是该对象耳朵class路径,请注意一定要以分号(;)结束!
然后是获取函数的id,jmethodID id = env->GetMethodID(cls,"", "(D)V"); 第一个是刚刚获得的class引用,第二个是方法的名称,最后一个就是方法的签名了
还是不懂?我曾经如此,请接着看...
难理解的函数签名
JNINativeMethod的定义如下:
typedefstruct{
constchar* name;
constchar* signature;
void* fnPtr;
}JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
"()" 中的字符表示参数,后面的则代表返回值。例如"()V"就表示void Func();
"(II)V" 表示 void Func(int, int);
那其他情况呢?请查看下表:
类型 | 符号 |
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | L |
float | F |
double | D |
void | V |
object对象 | LClassName; L类名; |
Arrays | [array-type [数组类型 |
methods方法 | (argument-types)return-type (参数类型)返回类型 |
稍稍补充一下:
1、方法参数或者返回值为java中的对象时,签名中必须以“L”加上其路径,不过此路径必须以“/”分开,自定义的对象也使用本规则
比如说 java.lang.String为“java/lang/String”,com.nedu.jni.helloword.Student为"Lcom/nedu/jni/helloword/Student;"
2、方法参数或者返回值为数组类型时,请前加上[
例如[I表示 int[],[[[D表示 double[][][],即几维数组就加几个[
在本地方法中调用Java对象的方法
1、获取你需要访问的Java对象的类:
jclass cls =(*env)->GetObjectClass(env, obj); // 使用GetObjectClass方法获取obj对应的jclass。
jclass cls =(*env)->FindClass(“android/util/log”)// 直接搜索类名,需要是static修饰的类。
2、获取MethodID:
jmethodID mid =(*env)->GetMethodID(env, cls,"callback","(I)V");//GetStaticMethodID(…),获取静态方法的ID使用GetMethdoID方法获取你要使用的方法的MethdoID
其参数的意义:
env-->JNIEnv
cls-->第一步获取的jclass
"callback"-->要调用的方法名
"(I)V"-->方法的Signature, 签名同前面的JNI规则。
3、调用方法:
(*env)->CallVoidMethod(env, obj, mid, depth);// CallStaticIntMethod(….) ,调用静态方法
使用CallVoidMethod方法调用方法。参数的意义:
env-->JNIEnv
obj-->通过本地方法穿过来的jobject
mid-->要调用的MethodID(即第二步获得的MethodID)
depth-->方法需要的参数(对应方法的需求,添加相应的参数)
注:这里使用的是CallVoidMethod方法调用,因为没有返回值,如果有返回值的话使用对应的方法,在后面会提到。
CallVoidMethod CallStaticVoidMethod
CallIntMethod CallStaticVoidMethod
CallBooleanMethod CallStaticVoidMethod
CallByteMethod CallStaticVoidMethod
现在稍稍明白文章开始构造Java对象那个实例了吧?让我们继续深入一下:
Jni操作Java的String对象
从java程序中传过去的String对象在本地方法中对应的是jstring类型,jstring类型和c中的char*不同,所以如果你直接当做char*使用的话,就会出错。因此在使用之前需要将jstring转换成为c/c++中的char*,这里使用JNIEnv提供的方法转换。
constchar*str =(*env)->GetStringUTFChars(env, jstr,0);
(*env)->ReleaseStringUTFChars(env, jstr, str);
这里使用GetStringUTFChars方法将传进来的prompt(jstring类型)转换成为UTF-8的格式,就能够在本地方法中使用了。
注意:在使用完你所转换之后的对象之后,需要显示调用ReleaseStringUTFChars方法,让JVM释放转换成UTF-8的string的对象的空间,如果不显示的调用的话,JVM中会一直保存该对象,不会被垃圾回收器回收,因此就会导致内存溢出。
下面是Jni访问String对象的一些方法:
· GetStringUTFChars 将jstring转换成为UTF-8格式的char*
· GetStringChars 将jstring转换成为Unicode格式的char*
· ReleaseStringUTFChars 释放指向UTF-8格式的char*的指针
· ReleaseStringChars 释放指向Unicode格式的char*的指针
· NewStringUTF 创建一个UTF-8格式的String对象
· NewString 创建一个Unicode格式的String对象
· GetStringUTFLength 获取UTF-8格式的char*的长度
· GetStringLength 获取Unicode格式的char*的长度
下面提供两个String对象和char*互转的方法:
/* c/c++ string turn to java jstring */
jstring charToJstring(JNIEnv* env,constchar* pat)
{
jclass strClass =(*env)->FindClass(env,"java/lang/String");
jmethodID ctorID =(*env)->GetMethodID(env, strClass,"","([BLjava/lang/String;)V");
jbyteArray bytes =(*env)->NewByteArray(env, strlen(pat));
(*env)->SetByteArrayRegion(env, bytes,0, strlen(pat),(jbyte*)pat);
jstring encoding =(*env)->NewStringUTF(env,"UTF-8");
return(jstring)(*env)->NewObject(env, strClass, ctorID, bytes, encoding);
}
/* java jstring turn to c/c++ char* */
char* jstringToChar(JNIEnv* env, jstring jstr)
{
char* pStr = NULL;
jclass jstrObj =(*env)->FindClass(env,"java/lang/String");
jstring encode =(*env)->NewStringUTF(env,"utf-8");
jmethodID methodId =(*env)->GetMethodID(env, jstrObj,"getBytes","(Ljava/lang/String;)[B");
jbyteArray byteArray =(jbyteArray)(*env)->CallObjectMethod(env, jstr, methodId, encode);
jsize strLen =(*env)->GetArrayLength(env, byteArray);
jbyte *jBuf =(*env)->GetByteArrayElements(env, byteArray, JNI_FALSE);
if(jBuf >0)
{
pStr =(char*)malloc(strLen +1);
if(!pStr)
{
return NULL;
}
memcpy(pStr, jBuf, strLen);
pStr[strLen]=0;
}
env->ReleaseByteArrayElements(byteArray, jBuf,0);
return pStr;
}