有关ndk、jni开发流程、数据类型、数组操作

来源:互联网 发布:ubuntu换源163 编辑:程序博客网 时间:2024/05/20 14:26


转自: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;

第一个变量nameJava中函数的名字。
第二个变量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;
}

 




0 0
原创粉丝点击