就在两天前,我对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(<ime); return ctime(<ime); }
对应生成的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背后的底层原理。
树伟大神,赞一个
优神你是第一评论呀!感动ing