android jni学习

来源:互联网 发布:网络教育 入学考试 编辑:程序博客网 时间:2024/06/10 17:10

       现在很多app都不是仅仅的android sdk搭建而成的,都是混合式的居多,也就是说,一个android app,一方面包含有c或者c++的底层代码,一方面还有html5的网页,所以,拓展学习很有必要。

       这篇文章,我们就说一下android怎么通过jni来和本地代码(C/C++)通信的。

       这里,得先简单说一下NDK和JNI的区别;

         (1)NDK是android的特性,JNI是java的特性;

         (2)NDK是android一系列工具的集合,通过这些工具,一方面简化jni的编写,一方面简化我们把原生代码和java代码一起混合打包成为一个apk;JNI可以理解为一门语言,Java Native Interface java原生接口

       这里,我们主要聊一下JNI的机制问题,至于NDK中的种种工具的用法,这里暂且不谈。

       JNI原理

       JVM和JNIENV

       JVM是一个java虚拟机,JVM中可以有多个java线程,而一个线程对应与一个JNIENV,这些JNIENV保存在TLS中,而且是互相不干扰的。

       java访问本地代码

       1 通过loadLibrary导入本地代码的so包,这一个过程,把本地代码的函数映射到一个函数表里面;

       2 在java端通过声明native方法,告知jvm该方法从本地代码中找寻;

       3 jvm从步骤1中找寻相应的原生函数;

       本地代码访问java

        通过JVM,JVM把java代码隔绝到一个独立的环境中,造就了它的跨平台性,也导致了它和本地代码的通信更加困难,我们可以通过JNI提供的接口函数进行操作。

       JNI数据传递

       JNI本来就是做数据转化和传递的,可以把它理解为一个胶水,用来连接本地代码(c/c++)和java的接口,所以,我们本地代码工程师只需关注C/C++代码即可,然后通过JNI把C/C++编写好的函数进行转化,提供给android 的java层使用即可。

       基本数据类型

       我们知道,本地代码的基本数据类型和平台有关,如int,不一定为4个字节,而java的就恒定为4个字节,这对这种情况,JNI充当了个中介的角色,

         引用类型

          类,实例,所有的数组都是引用类型

   

          JNI访问java成员

         java中类成员和指静态属性和静态方法,它们属于类而不属于对象;而对象成员是指非静态属性和非静态方法,它们属于对象而非类,正因为如此,在JNI中访问类成员和对象成员是不一样的。

        获取类名

       如果java层传入JNI层对象,可以通过(*env)->GetObjectClass(env,obj)来获取该类,否则,通过(*env)->FindClass(env,".../.../...")获取,获取到jclass中。

        获取属性ID和方法ID

        JNI在jni.h中定义了jFielId和jMethodId用来表示java的属性和方法,其中方法如下:    

// 根据属性签名返回clazz类中的该属性ID

jfieldIDGetFieldID(JNIEnv*env,jclass clazz, const char*name, const char *sig);

//如果获得静态属性ID,则调用下面的函数

jfieldIDGetStaticFieldID(JNIEnv*env,jclass clazz, constchar *name, const char *sig);

//根据方法签名返回clazz类中该方法ID

jmethodIDGetMethodID(JNIEnv*env,jclass clazz, const char*name, const char *sig);

//如果是静态方法,则调用下面的函数实现

jmethodIDGetStaticMethodID(JNIEnv*env,jclass clazz, constchar *name, const char *sig);

         可见,属性和方法,静态和非静态JNI中都定义了不同的方法获取。其中参数说明,clazz表示该属性或方法所属的类,有前面获取类名得到(注意区分类和对象),name表示该属性或者方法名,sig表示该属性或者方法名的类型签名。

         JNI类型签名

        上面提到,sig参数需要用到类型签名,为啥需要用到类型签名呢,源于java的面向对象的思想,java支持重载,即一个名字完全相同的函数,只要参数或者返回值不同,那也是不同的函数,所以JNI中通过这种方式来区别。

类型签名

Java类型

类型签名

Java类型

Z

boolean

[

[]

B

byte

[I

int[]

C

char

[F

float[]

S

short

[B

byte[]

I

int

[C

char[]

J

long

[S

short[]

F

float

[D

double[]

D

double

[J

long[]

L

[Z

boolean[]

V

void

 

 

       这里需要注意以下几点:

         1 类名以为L开头,别想当然的觉得是Long的签名;

         2 类名以/分隔,(java的包名本来就是一层层的文件夹),以;结束,别忘了结束符号;

         3 括号内()是参数,中间没有分隔,如果没有参数,留空,括号外为返回值 如()V;

       JNI操作java属性或方法

        前面取得了methodid之后,就可以根据这个id来来获取或者设置属性或方法了。

取得Java属性的JNI方法定义如下:

j<类型>Get<类型>Field(JNIEnv* env,jobject obj,jfieldID fieldID);

j<类型>Get Static<类型>Field(JNIEnv* env,jobject obj,jfieldID fieldID);

设置Java属性的JNI方法定义如下:

voidSet<类型>Field(JNIEnv* env,jobject obj, jfieldID fieldID, j<类型>val);

void SetStatic<类型>Field(JNIEnv* env,jobject obj, jfieldID fieldID, j<类型>val);

JNI提供了下面的方法用来调用Java方法:

// 调用Java成员方法

Call<Type>Method(JNIEnv* env,jobject obj,jmethodID methodID,...);

Call<Type>MethodV(JNIEnv* env,jobject clazz, jmethodIDmethodID,va_listargs);

Call<Type>tMethodA(JNIEnv* env,jobject clazz,jmethodID methodID,constjvalue *args);

// 调用Java静态方法

CallStatic<Type>Method(JNIEnv* env,jclass clazz,jmethodID methodID,...);

CallStatic<Type>MethodV(JNIEnv* env,jclass clazz,jmethodID methodID,va_listargs);

CallStatic<Type>tMethodA(JNIEnv* env,jclass clazz,jmethodID methodID,constjvalue *args);

         传入env指针,类或者对象(非静态用类,静态用对象),方法id即可。这里简单说一下带V或A或不带的区别。

最常用的是不带的,里面是一个可变参数,带V的传入的是以向量表形式传入参数,带A的表示以jvalue数组提供的列表。

        在本地代码操作java对象

         

jobjectNewObject(JNIEnv* env,jclass clazz, jmethodIDmethodID,...);

jobjectNewObjectV(JNIEnv* env,jclass clazz,jmethodIDmethodID,va_list args);

jobjectNewObjectA(JNIEnv* env,jclassclazz, jmethodIDmethodID,const jvalue *args) ;

         这里的方法id,是构造函数的方法id,所以创建一个java对象的简单步骤可为:

           1 获取到该类;

           2 获取构造函数方法id,(*env)->GetMethodId(env,class,"<init>",()V),第三个参数为<init>或者类名

        创建java对象

         为了返回java环境的字符串,jni提供了相应的方法;

根据传入的宽字符串创建一个JavaString对象

jstringNewString(const jchar *unicode, jsizelen)

根据传入的UTF-8字符串创建一个JavaString对象

jstringNewStringUTF(const char *utf)

          为了处理java传入的字符串String,也有类似的方法;

将一个jstring对象,转换为(UTF-16)编码的宽字符串(jchar*:

const jchar*GetStringChars(jstring str,jboolean*isCopy) 

将一个jstring对象,转换为(UTF-8)编码的字符串(char*:

 const char*GetStringUTFChars(jstringstr,jboolean *isCopy)

            isCopy可为NULL,JNI_FALSE,JNI_TRUE,为true表示在本地开辟内存,把该java 的string拷贝到该内存中,然后返回指向该内存的指针,为false表示不另外开辟内存,而是直接指向java中string的内存地址。NULL表示不关心是否拷贝。不管怎么样,在用完之后,都得释放该内存。

RealeaseStringChars(jstringjstr,const jchar*str)

RealeaseStringUTFChars(jstringjstr,constchar* str)

          Java数组在本地代码中的处理

         数组其实和属性差不多,不过它比较灵活,这里说一下几个要点

           1  如果java未传入数组,我们可以先通过GetFieldId获取到该数组id,然后根据把该id传入GetObjectField中,返回值强转成为对应的array。

             2   获取数组长度,GetArrayLength;

             3  获取数组元素,GetObjectArrayElements,返回的是该数组的地址,可通过该地址操作数组;

             4  NewObjectArray新建数组,可用于返回给java;

             5 用完干数组元素记得释放 Realease<Type>ArrayElements;

       


       最后补充一下c和java之间怎么传递json格式的数据。有经验的朋友知道,在c中是有用于封装和解封装json格式的库,这里我直接把源码拷过来用,里面把函数已经做了很好的封装,直接调用即可;

         c把json格式传到java大概步骤如下:

       1 在c中创建json格式  cJSON_CreateObject();

       2  添加item  cJSON_AddStringToObject

       3  json格式转char*    cJSON_Print

       4  用完之后记得删掉该json  cJSON_Delete;

         java把json格式传到c大概步骤如下:

       1 cJSON_Parse把char*的字符转为cJSON格式;

       2 cJSON_GetObjectItem获取到java的json key,再转化即可。

     

           其实就是库的使用,这里不再赘述,可看我的demo。


          总结:把android jni的大概内容大概过了一遍,其中包括一些原理,类型转换等,知道这些简单的步骤,可以开始着手相关的工作,在工作中慢慢的磨合即可。当然,要做好android的本地代码部分,关键还是要有c/c++功底,可以找个教程简单复习一下。

        我自己写了个demo,里面包含了大部分的数据转化,包含了json格式的转换等,在android studio的环境下编写,大家可下载看看。地址点击打开链接


0 0
原创粉丝点击