Android NDK开发之从Java与C互调中详解JNI使用(一)

生活

这一个礼拜过得真的是苦不堪言,上周因为打球脚踝直接扭伤,肿的想猪蹄一样,然后休息几天消肿了,可以缓慢龟速的行走了,然而五一回来上班第一天,上班鞋子还能穿上,下班脚已插不进鞋子里面了,好吧,又肿回来了,苦逼。

正文

回到正文,上篇我们已学习到了Android NDK开发之从环境搭建到Demo级十步流,主题是DNK环境搭建和Demo示例开发步骤,而今天我们要学习的是通过JNI实现Java和C之间的交互。

对于JNI的理解,上一节也已讲过,这里在回顾下:

JNI:Java Native Interface 也就是java本地接口,它是一个协议,这个协议用来沟通java代码和本地代码(c/c++)。通过这个协议,Java类的某些方法可以使用原生实现,同时让它们可以像普通的Java方法一样被调用和使用,而原生方法也可以使用Java对象,调用和使用Java方法。也就是说,使用JNI这种协议可以实现:java代码调用c/c++代码,而c/c++代码也可以调用java代码。

接下来就以一个登录实例来详细的讲解使用JNI来完成Java与C代码之间的交互。

Java 调用 C 本地方法

JNI使用演示

一、首先先构造登录界面

登录界面中有三个文本输入框,分别对应用户名,密码,验证码。详情如下

二、在Java2CJNI类中添加我们登录的login本地方法,且方法中带有三个不同类型的参数

三、使用javah生成头文件,并存放在jni文件夹下,且在本地方法中实现我们的登录逻辑(具体的逻辑将会在下面的JNI使用中详细讲解)

四、在我们的MainActivity中调用本地方法

五、来看执行结果

上述的执行过程及结果,逻辑是在C代码中完成的,主要逻辑非常的简单,就是判断验证码和用户名是否相同,不同返回错误信息,相同则是登录成功。

上面所完成的是我们接下来要讲解JNI的基础前提,所以并没有详细的讲解,如果伙伴们有疑问或是不明白的地方请移步到Android NDK开发之从环境搭建到Demo级十步流 来先学习了先基础的知识,因为本次讲解是在上一节的基础上进行的。

OK,现在我们来详细的看下在Java2C.c中我们是怎么使用JNI来完成调用Java调用C的。

JNIEXPORT jstring JNICALL Java_com_sanhui_ndkdemo_Java2CJNI_login
(JNIEnv *env, jobject jobj, jstring jUserName, jstring jPSW, jint jcode){
    const char* resultMessage;
    if(jcode == 1234){
        const char* cStr;
        jboolean isCopy;
        cStr = (*env)->GetStringUTFChars(env, jUserName,&isCopy);
        int result = strcmp(cStr, "guanmanman");
        if(result == 0){
            //同样的道理psw也可以做一样的验证
            //TODO
            resultMessage = "success login !";
        }else{
            resultMessage = "error username";
        }
    }else{
        resultMessage = "error code";
    }
    return (*env)->NewStringUTF(env, resultMessage);
};

JNI方法声明

我们知道通过javah会为我们生成这样的头文件方法(补全后):

JNIEXPORT jstring JNICALL Java_com_sanhui_ndkdemo_Java2CJNI_login
(JNIEnv *env, jobject jobj, jstring jUserName, jstring jPSW, jint jcode){}

这里做下解释:

①:我们知道在调用本地方法时会返回一个String类型的返回值, jstring 就是JNI对应Java的一个返回类型。
②:通过Java_ + 包名(com.sanhui.ndkdemo) + 类名(Java2CJNI) + 方法名(login)的形式生成在原生代码中所识别的方法,当java虚拟机调用com.sanhui.ndkdemo.Java2CJNI.login的方法时会自动查找到这个C实现的Native函数并调用。
③:参数列表中包含五个参数,其中三个是通过java调用传递的,这个不多说,JNIEnv 是指向可用JNI函数表的接口指针,也就是说通过它可以调用JNI所封装的函数进行处理逻辑业务,jobject 则是Java类中的对象引用,这里指的是Java2CJNI类的实例,也就是说,当调用本地方法时,JNI将会自动的获取当前类的实例,以方便原生代码使用。

JNI 数据类型

原生数据类型同Java一样,都包含两类,基本数据类型与引用类型。

JNI 基础数据类型

在JNI当中,基本数据类型可以直接与C/C++的相对应的基本数据类型映射,所以我们可以直接拿来使用并不需要转换。

我们先来看看java、JNI、C之间的基本类型的映射关系:

java类型 JNI类型 C类型
Boolean jboolean unsigned char
Byte jbyte char
Char jchar unsigned short
Short jshort short
Int jint int
Long jlong long long
Float jfloat float
Double jdouble double

通过上面讲解和表格我们现在可以明白为什么我们通过本地方法传递进来的整形验证码,使用JNI技术转化为jint类型时可以直接的进行如下判断:

if(jcode == 1234){
}

JNI 引用类型-字符串

引用类型我们并不能直接的使用,它不是以原生数据类型的形式展现,而是需要通过JNI提供的一组相关的API把引用类型提供给原生代码使用。

java类型 JNI类型
java.lang.Class jclass
java.lang.Throwable jthrowale
java.lang.String jstring
byte[] jbyteArray
boolean[] jbooleanArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

上面也说过引用类型不能够直接的被原生代码使用,它需要通过JNI提供的API来提供给原生使用,例如:

		const char* cStr;
        jboolean isCopy;
        cStr = (*env)->GetStringUTFChars(env, jUserName,&isCopy);
        int result = strcmp(cStr, "guanmanman");

上面的代码中,GetStringUTFChars就是JNI所提供的API,通过该方法可以把java中所传递的Stirng字符串转化为原生代码所能识别的字符串,然后在进行下面的比较操作。strcmp方法是C语言中所提供的的比较字符串的方法。

说到这里,我们在来详细的看下JNI针对字符串的操作:

①:创建字符串

可以在原生代码中使用NewString函数构建Unicode编码风格的字符串实例,假如你使用的是utf-8的编码格式,可以使用NewStringUTF函数来构建,如:

	jstring jString = (*env)->NewString(env,"I am from C");
	jstring jString = (*env)->NewStringUTF(env,"I am from C");

②:把Java字符串转换成C字符串

Java字符串String是属于引用类型的,它不能够直接的被原生代码使用,为了在原生代码中使用Java传递过来的String串,我们需要借用JNI API,根据编码的不同,分别可以是用GetStringChars和GetStringUTFChars函数进行转换,如我们上面的实例:

const char* cStr = (*env)->GetStringUTFChars(env, jUserName,&isCopy);

isCopy参数是可选的,它是jboolean类型的,它的调用者明确返回的C字符串地址是指向副本黑市指向堆中的对象。

JNI 引用类型-数组

在引用类型中还有一种比较重要的类型,那就是数组,JNI中也提供了相应的API来处理和使用数组。

①:创建数组

JNI通过New<?>Array函数在原生代码中创建数组,<?>类型可以是byte,short,int等类型,如:

	jintArray intArray = (*env)->NewIntArray(env,5);

NewIntArray数组中应该给出明确的数组长度,例如,5就是intArray的数组长度。

②:对数组的操作
如以上介绍,数组属于引用类型,通过Java传递过来的数组我们是无法直接进行操作的,但是根据JNI提供的API我们可以很顺利的进行数组的操作,如:

	//获取数组长度
    //jsize GetArrayLength(this, array);
	 //获取数组元素
    //jint* GetIntArrayElements(this, array, isCopy);

不仅仅可以对数组进行直接的操作,而且还可以对数组的副本进行操作,如

通过Get<?>ArrayRegion函数将给定的java数组复制成C数组,然后我们就可以在原生代码中针对C数组进行操作。

操作完成之前我们还可以通过以下函数把数组的副本给还原到原来的Java数组中,

通过以上的函数我们就完成了在原生代码中对Java数组的操作。

③:数组操作实例

首先,在Java2CJNI类中添加一个本地方法modifyValue,接受一个int类型的数组,完成后重新通过javah生成.h头文件,如:

然后在Java2C.c中完成对它的逻辑操作:

代码比较简单,就是通过JNI API获取数组的长度、元素,然后遍历数组针对每个元素进行加上100,完成后把C数组还原给Java数组,代码注释的已很清楚的体现。

最后在MainActivity中调用modifyValue方法,来看下执行过程和结果。

OK,看了下篇幅,没想到这么长了,原本打算把Java调C和C调Java一起讲解下的,看来C调Java只能放到下一篇进行讲解了。

下篇主题预告:原生C访问Java成员、调用Java方法的JNI使用,以及使用gradle-experimental来调试原生代码。

那么,今天就先讲解到这里吧。请关注微信公众号。谢谢

更多资讯请关注微信平台,有博客更新会及时通知。爱学习爱技术。

posted @ 2017-05-05 09:20  管满满  阅读(7191)  评论(0编辑  收藏  举报