JNI系列教程三 —— NDK入门

3.1 背景

谈到JNI的使用场景,最常用的就是android NDK的编写了。首先从http://developer.android.com/ndk/downloads/index.html#download 把最新版的NDK下载下来。下载完之后得到一个exe文件,这其实是一个自解压文件,运行后自动解压,解压完成后的文件夹有3GB,所以你的磁盘空间起码得留足5GB左右的剩余空间。 最终我们得到这么一个目录结构:

ndk目录结构
图3.1.1 ndk目录结构
接着需要将ndk所在目录添加到环境变量PATH中,这样在以后运行的时候,只需要输出ndk-buld就可以了。文件夹plantforms存放着编译各个版本的android所需的头文件和动态库,举个例子platforms/android-3/arch-arm文件夹下存放的是android 1.5版本的arm平台的头文件和库文件,从android 2.3开始,开始支持x86mips两个平台,所以在platforms/android-9目录下会有arch-arm arch-mips arch-x86三个文件夹。

本文源地址:http://blog.whyun.com/posts/jni-ndk/ 转载请注明出处。

3.2 Android.mk

mk后缀的文件是makefile文件,mk文件一般通过include语法被引入到其它makefile中。在NDK中Android.mk里存储的都是编译相关的配置信息,我们先举一个例子:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := chapter3
LOCAL_CFLAGS := -DJNI_ANDROID
LOCAL_LDLIBS := -llog -lm
TARGET_ARCH := arm
TARGET_PLATFORM := android-7
LOCAL_SRC_FILES := chapter3.c 

$(info $(SYSROOT))
include $(BUILD_SHARED_LIBRARY)

文件第一行中my-dir是一个函数,通过调用它返回当前路径,CLEAR_VARS变量指向一个mk文件,它会清除所有除了LOCAL_PATH之外的LOCAL_开头的变量,下面是一些列的对于LOCAL_开头的变量的定义:

3.3 简单例子

这个例子就是NDKsamples目录中hello-jni项目,将这个项目随便拷贝到某一个目录,然后删除掉项目中的tests文件夹,这个是一个单元测试,我不知道怎么使用它,所以直接删除掉。然后打开eclipse,选择File->Project...->Android->Android Project From Existing Code,选择刚才拷贝后的路径,点击完成。 在命令行中进入项目的jni文件夹,然后运行ndk-build,你会发现程序生成了好几个so文件夹,放置于项目的libs文件夹中,这是由于在文件Application.mk(位于文件夹jni中)文件中这一句造成的:
APP_ABI := all
ABI这个参数(可以参见百度百科词条ABI)比之前讲到的ARCH要更加细化,可以理解为在同一体系结构下CPU的不同版本,支持的指令集有所差异,android中支持的ABI可以参见谷歌官方ABI解释。最终在模拟器上运行程序成功:

运行hello-jni项目成功
图3.3 运行hello-jni项目成功

Comments

JNI系列教程二——数据结构

JNI和java相互调用,一个不可避免的问题就是两者的数据结构要相互转换。这一节正是要讲这个重头戏。

本文源地址http://blog.whyun.com/posts/jni/2-struct-transform/ 转载请注明出处。

2.1 基本类型

基本数据类型大都是数字类型表2.1.1中给出了java和jni的对应关系。

Java 类型 本地类型 说明
boolean jboolean 无符号,8 位
byte jbyte 无符号,8 位
char jchar 无符号,16 位
short jshort 有符号,16 位
int jint 有符号,32 位
long jlong 有符号,64 位
float jfloat 32 位
double jdouble 64 位

表2.1.1 基本数据类型对照表

2.2 对象类型

对象类型的对照关系牵扯到的知识点比较多,所以下面决定通过具体例子来让大家更好的学习它。 首先在java代码有这么两行行声明

public native int getSum(int a, int b);
public native int getSum(byte [] array);

第一个函数是一个基本数据类型,第二个函数是一个数组,虽然两个函数在java中是同名的,但是生成的c文件却不能使用同名文件,

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getSum
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter2_StructDemo_getSum__II
    (JNIEnv *env, jobject obj, jint a, jint b) {
        return a+b;
};

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getSum
 * Signature: ([B)I
 */
JNIEXPORT jint JNICALL Java_com_whyun_jni_chapter2_StructDemo_getSum___3B
  (JNIEnv *env, jobject obj, jbyteArray arr) {
      int sum = 0;
      char *data = (char*)(*env)->GetByteArrayElements(env,arr,NULL);
      int dataLen = (int)(*env)->GetArrayLength(env,arr);
      int i = 0;
      for (i=0;i<dataLen;i++) {
          sum += data[i];
      }
      (*env)->ReleaseByteArrayElements(env,arr,data,0);
      return (jint)sum;
}

我们通过javah生成头文件时,就会发现生成的getSum的函数后缀是不一样的。这里重点看JNI中对数组的操作,在实际编程中我们常常要处理二进制数据,那么自己数组便是经常要使用的一个数据结构了。java中的byte[]最终在JNI中被转化为jbyteArray,但是jbyteArray要想在C语言中使用,还必须得到一个C语言中可识别的char类型指针的形式,这就是函数GetByteArrayElements的作用。要想知道当前数组的长度,可以使用函数GetArrayLength。我们可以推断出GetByteArrayElements内部申请了一块内存,也就是说变量data是通过类似于malloc之类的函数申请得到的,所以最终在使用完成之后是需要释放的,所以才有了函数最后调用ReleaseByteArrayElements函数的代码。

下面看一个复杂的例子通过JNI来调用java函数,直接上JNI的代码:

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    getUserList
 * Signature: (I)Ljava/util/ArrayList;
 */
JNIEXPORT jobject JNICALL Java_com_whyun_jni_chapter2_StructDemo_getUserList
  (JNIEnv *env, jobject obj, jint num) {
    int count = (int)num,i=0;
    jclass clsUserBean = (*env)->FindClass(env,"com/whyun/jni/bean/UserBean");
    jclass clsArrayList = (*env)->FindClass(env,"java/util/ArrayList");
    jmethodID userBeanConstructor = (*env)->GetMethodID(env,clsUserBean,"<init>","()V");
    jmethodID userBeanSetAge = (*env)->GetMethodID(env,clsUserBean,"setAge","(I)V");
    jmethodID userBeanSetName = (*env)->GetMethodID(env,clsUserBean,"setName","(Ljava/lang/String;)V");
    jmethodID arrayListContructor = (*env)->GetMethodID(env,clsArrayList,"<init>","(I)V");
    jmethodID arrayListAdd = (*env)->GetMethodID(env,clsArrayList,"add","(ILjava/lang/Object;)V");

    jobject arrayList = (*env)->NewObject(env,clsArrayList,arrayListContructor,num);

    char nameStr[5] = {0};
    int index = 0;
    jstring name;

    for(i=0;i<count;i++) {
        jobject userBean = (*env)->NewObject(env,clsUserBean,userBeanConstructor);

        for (index=0;index<4;index++) {
            nameStr[index] = randStr[(i+7)%5];
        }
        name = (*env)->NewStringUTF(env,(const char *)nameStr);
        (*env)->CallVoidMethod(env,userBean,userBeanSetAge,(jint)(20+i));
        (*env)->CallVoidMethod(env,userBean,userBeanSetName,name);

        (*env)->CallVoidMethod(env,arrayList,arrayListAdd,(jint)i,userBean);
    }
    return arrayList;
}

如果你稍加揣测的话,这段代码翻译成java代码应该是这么写的:

public ArrayList<UserBean> getUserList(int count) {
    ArrayList<UserBean> list = new ArrayList<UserBean>(count);
    for(int i=0;i<count;i++) {
        UserBean bean = new UserBean();
        bean.setAge(i+20);
        bean.setName("name"+i);
        list.add(i,bean);
    }
    return list;
}

看上去java代码要简单许多,在JNI中包括类、成员函数、对象之类的数据都需要先创建再使用。在java中创建对象用new UserBean()就够了,但是在JNI中,你首先要通过FindClass函数来找到类定义,然后通过类定义用函数GetMethodID来找到构造函数,然后根据类定义和构造函数通过函数NewObject来获取一个对象,下面分别对这三个函数进行讲解。
FindClass函数的第二个参数是要加载的类的类名全称,在java中我们应该写作com.whyun.jni.bean.UserBean,在JNI中就是把.换成了/而已。
GetMethodID函数的第二个参数是FindClass函数得到的类变量clsUserBean,第二个参数是函数名,一般来说函数名直接写函数名称就行了,比如说你再往下看一行代码获取UserBean的setAge函数的时候就直接写的函数名,但是构造函数就不同了,所有类的构造函数在JNI中统一叫<init>。最后一个参数很重要,它是java函数的签名,java中每个函数和属性都有一个它的标识,这个标识用来指出当前函数的参数、返回值类型或者属性的类名,可能有些人第一听说这个概念,其实获取这个标识有一个很简单的方法,就是命令javap,下面先做个小实验,运行javap -s java.lang.String,会输出如下内容:

Compiled from "String.java"
public final class java.lang.String implements java.io.Serializable, java.lang.Comparable<java.lang.String>, java.lang.CharSequence {
  public static final java.util.Comparator<java.lang.String> CASE_INSENSITIVE_ORDER;
    Signature: Ljava/util/Comparator;
  public java.lang.String();
    Signature: ()V

  public java.lang.String(java.lang.String);
    Signature: (Ljava/lang/String;)V

  public java.lang.String(char[]);
    Signature: ([C)V

  public java.lang.String(char[], int, int);
    Signature: ([CII)V

  public java.lang.String(int[], int, int);
    Signature: ([III)V
由于文件内容比较多,所以省略掉下面内容

由于java是支持重载的,一个函数可能会拥有多种实现方式,比如String类的构造函数就有N多个,那么你在调用其函数的时候,就必须得依靠参数和返回值类型来区分不同的函数了,而签名正提供了一种简介的方式来表示一个函数的参数和返回值。通过刚才javap命令的输出,我们可以得到对于没有参数的String构造函数,其签名为()V;对于参数为字符数组的构造函数签名为([C)V。 接着讲函数NewObject,前面经过FindClassGetMethodID一顿折腾,我们拿到了两个变量类变量clsUserBean和函数变量userBeanConstructor,将其传到NewObject中就能得到一个对象。在我们的代码中还有一个对于ArrayList类型的对象的构造,他调用NewObject的时候比UserBean多了一个参数,那是由于我们使用的构造函数为ArrayList(int i),故需要传递一个ArrayList的长度参数,这里需要声明的是NewObject函数的参数个数是可变的,调用的构造函数有参数,就依次追加到后面即可。 终于讲到真正调用java函数这一步了,就是代码中的CallVoidMethod,和NewObject一样,它也是可变参数,参数形式也一样。除了CallVoidMethod,JNI中还有各种Call[Type]Method,这个Type就代表了java函数的返回值,它可以是Object Boolean Byte Char Short Int Long Float Double

2.3 使用异常

c语言中没有异常这个概念,使用c代码的时候多是通过返回码来判断是否调用成功,然而对于java程序来说判断有没有成功,往往是看有没有异常抛出,所以说编写一个java友好的JNI程序,我们需要将错误码转成java异常。直接上例子:

/*
 * Class:     com_whyun_jni_chapter2_StructDemo
 * Method:    showException
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_whyun_jni_chapter2_StructDemo_showException
  (JNIEnv *env, jobject obj) {
      jclass exception = (*env)->FindClass(env,"java/lang/Exception");
      (*env)->ThrowNew(env,exception,"This is a exception.");
}

这里ThrowNew转成java的话,就是throw new Exception("This is a exception.");其它的我就不多说了。

Comments