NDK开发的入门级总结

= 1564

本文出自:【InTheWorld的博客】

就在两天前,我对JNI或者NDK开发的了解基本都还处于Hello World水平。这两天花时间学习了一下,算是对NDK开发有了一些入门级的了解。所以我准备写这一篇博客,算是一个入门级的学习总结。鉴于目前的水平,这篇blog不会讨论到一些复杂的内容,仅仅是一些流程的理解和分析。


  • JNI和NDK的开发思路

NDK听起来好像很神秘,但实质上就是Java调用C/C++代码的技术。考虑到JVM就是用C/C++写的,Java调用C/C++其实并没有本质上的难度。以前写过一篇关于Lua调用C扩展的博客,其实就和JNI的思路是差不多的,当然Java比Lua复杂的多。JNI本质上就是这种调用的支持接口,在展开这种支持接口的使用方法之前,先来探讨一下这种接口的实现思路。

虽然JNI已经明确的定义了接口函数的设计标准,但具体的实现还是有很多门道的。我准备从接口生成工具展开这个问题,虽然听起来很不专业,但结论却挺有意义的。就我目前了解,大概有两种接口生成方式——swig和javah。

SWIG 是一个非常优秀的开源工具,支持将 C/C++ 代码与任何主流脚本语言相集成。使用SWIG的方式也比较简单,你需要的仅仅是提供一个描述C/C++接口的文件。一个简单的C/C++接口函数文件如下所示。

 /* File : example.c */
 
 #include <time.h>
 double My_variable = 3.0;
 
 int fact(int n) {
     if (n <= 1) return 1;
     else return n*fact(n-1);
 }
 
 int my_mod(int x, int y) {
     return (x%y);
 }
    
 char *get_time()
 {
     time_t ltime;
     time(&ltime);
     return ctime(&ltime);
 }
 

对应生成的SWIG接口文件(一般以.i为后缀)如下所示:

 /* example.i */
 %module example
 %{
 /* Put header files here or function declarations like below */
 extern double My_variable;
 extern int fact(int n);
 extern int my_mod(int x, int y);
 extern char *get_time();
 %}
 
 extern double My_variable;
 extern int fact(int n);
 extern int my_mod(int x, int y);
 extern char *get_time();

随后,调用swig命令即可生成指定语言的接口,包括C/C++代码和指定语言代码。如果接口文件中的函数都是已经实现好的,那么JNI的开发就基本完成了,你只需要在Java中调用生成类对应的方法即可。听起来是不是很简单?是的,的确很简单,而且生成的代码中有很多错误处理,并且bug也应该很少。同时,这种方法也有缺点。如果对应的C/C++代码的接口定义的不是很“友好”,SWIG生成的代码会比较冗余。这里的“不友好”指以下几种情况,使用多种类型的指针、使用多种枚举类型、过多的类型宏定义等等。在这种情况下,SWIG会生成很多的冗余代码,本来很简单的接口反而被它弄得很复杂。

相比于SWIG,大家应该更熟悉javah方法,毕竟是标准的JNI实现方式。使用javah的步骤也和简单,定义native类,然后生成对应的头文件,然后剩下的工作就是实现头文件对应的函数。使用javah就不可避免的需要编写一部分的C/C++代码,相比之下,比SWIG的工作量大一些。但javah方式生成的接口会更加整洁优雅一些,因为JNI的C/C++部分会被编译成动态链接库,对于上层开发人员来说,java部分代码的可读性会非常重要。由于javah方法的java代码是由手动写出的,所以可读性和灵活性上都有很多优势。


  • NDK的开发概要

NDK开发其实本质上就是动态链接库的开发。和Unix/Linux系统中的开发一样,NDK开发也需要Makefile构建脚本。一般存在两种的构建方式,一种是Android.mk,另一种是CMakeLists.txt。其实两种方式本质上都是一样的,最终都是生成GNU Makefile,使用make工具完成构建任务,这里我就不再赘述了。

除去NDK的业务逻辑,通过JNI实现与原生代码通信算是NDK最重要的技术了。 对原生数据类型而言,它们可以直接与C/C++数据类型保持对应关系。对应关系如下

Java类型 JNI类型 C/C++类型 大小
boolean jboolean unsigned char 无符号8位
byte jbyte char 有符号8位
char jchar unsigned short 无符号16位
short jshort short 有符号16位
int jint int 有符号32位
long jlong long long 有符号64位
float jfloat float 32位
double jdouble double 64位

对于引用类型,也存在着一些映射关系,但是它们的内部数据结构没有直接暴露给原生代码,而是通过JNIEnv指针接口来访问内部数据结构或者方法。对于非基本数据的访问,主要的难点在于对象字段和方法的访问。两者的访问方式其实都差不太多,大概有以下几个步骤:

1. 获取对象对应类

 jclass clazz;
    clazz = (*env).GetObjectClass(instance);
    //clazz = (*env)->GetObjectClass(env, instance);

正如代码中所示,通过对象获得对应类有两种方法。其中第二种方法其实有点奇怪,但事实上这两种方法是等价的。看看代码就明白:

struct _JNIEnv {
    const struct JNINativeInterface* functions;

    jobject GetObjectClass(jobject obj) {
        return functions->GetObjectClass(this, obj);
    } 
}

其中(*env)->操作相当于是做了一次强制类型转换,因为functions其实就是_JNIEnv最前面的一个域。 最终都是调用了同一个函数。

2. 获得域或者方法的id

   jfieldID instanceFieldId;
    instanceFieldId = (*env).GetFieldID(clazz, "fieldname", "Ljava/lang/String");

    jmethodID instanceMethodId;
    instanceMethodId = (*env).GetMethodID(clazz, "methodName", "()Ljava/lang/String;");

注意,GetFieldID和GetMethodID方法的第二个参数分别是域的名称和方法的名,第三个参数是域或者方法的签名。比较特别的情况是构造函数,构造函数的名称统一为“<init>”。

3. 访问域或者调用方法

对于域的访问,其实就和类型访问一样了,而方法调用就非常简单了。不过取决于返回值的类型,可能需要进行一些简单处理。特别是方法返回对象时,需要对类型做强制转换。这里就不贴代码了。

先总结到这里吧。等以后深入了解了,准备结合openJDK研究一发JNI背后的底层原理。

已有2条评论 发表评论

  1. Wu you /

    树伟大神,赞一个

    1. lshw4320814 / 本文作者

      优神你是第一评论呀!感动ing

发表评论