分类:
2012-01-18 12:46:46
无论是否有性能上的提升,本机代码起码在重用现在的C/C++代码的时是不二之选,我们并不总是那么关心Java的垃圾收集是多么的优秀,在可移植性的需求面前,C/C++往往比一次编译到处运行的Java更保险 — 你并不总能期望在iOS或者一些轻量级嵌入式系统中都装有合适的JRE。同理在现实上也适用于iOS的Objective-C。
使用 Android NDK为Android开发本机代码 ,NDK在android 的官方网站可以下载(http://developer.android.com/sdk/ndk/index.html),目前支持在 linux、windows 和 Mac OS X 下开发。下载好NDK后,解压到磁盘上随便你喜欢的位置。
NDK中脚本基本都是基于 unix bash 的shell脚本,因些windows下使用需要安装 Cygwin 1.7 或更高版本。此外还需要安装 GNU Make 3.81或以上版本,Nawk 或 GNU Awk (windows下需要通过Cygwin安装这些工具)。
以上工具准备好后,你便可以通过NDK中的ndk-build来编译生成android本机代码了,或者在eclipse中安装 “Sequoyah Android 本机代码支持” 让eclipse自动编译本机代码。
下边来编写一个简单的调用本机代码的小应用,并简单探讨下 Android.mk 的内容,包括定义模块、动态、静态链接、还有关于C++的使用(异常、RTTI、标准库)。
首先在eclipse中创建一个基于Android SDK的Android Project,项目名称比如叫JniDemo,package name 为com.leftcode.android,入口 Activity 为 JniDemoActivity。Minimum SDK 设置为 3 (Android 1.5)。
确保已经安装了 “Sequoyah Android 本机代码支持” ,并且在eclipse的Preferences中 Android -> 本机开发 中设置了正确的 NDK 位置。
在JniDemo项目节点上点右键,选择 Android Tools -> Add Natvie Support。完成后,项目中会多出两个文件夹 jni 和 libs。其中我们的C++代码将放在 jni 这个文件夹中,而生成的本机代码库文件将被自动放到 libs 文件夹中 (实际上我们可以在一个没有添加本地支持的项目中手动添加一个libs子文件夹,然后把编译好的库文件复制到对应的架构子文件夹中,比如 libs/armeabi/libJniDemo.so,就可以在项目的java代码中调用libJniDemo.so库中的函数了。)
展开jni文件,里面自动添加了两个文件,Android.mk和JniDemo.cpp,后者是C++代码文件,本机函数将在这里定义实现(当然也可以添加其它的.cpp/.c/.h文件)。Android.mk 基本上是一个 GNU Makefile 文件,用来描述NDK应该如何编译我们的代码文件。在 Android.mk 文件中,可以定义两种类型的模块:静态链接库和动态链接库。只有动态库可以在java代码中加载并调用,静态库不能被Android应用直接使用,但可以在其它的动态库中使用。默认的 Android.mk 只定义了一个动态库模块。让我们看看这个文件
第一行的 LOCAL_PATH := $(call my-dir) ,所有Android.mk文件第一行必须是这一行。它告诉生成系统我们的源文件在哪里。
接下来的 include $(CLEAR_VARS) 是每一个模块定义开始前应该调用的,它将清除上一个模块定义过程中使用过的LOCAL_XXX形式的变量(LOCAL_PATH除外)
接下来的 LOCAL_MODULE := JniDemo 开了一个新的模块定义,名称为JniDemo。
LOCAL_SRC_FILES := JniDemo.cpp指定当前正在定义的模块包含的源文件的列表,多个文件用空格分隔。这里只需要添加要使用的.cpp或.c文件就可以了,不要添加.h文文件。
最后是include $(BUILD_SHARED_LIBRARY) ,当看到这一行时,生成系统将综合之前给出的模块信息(名称、源文件等等等等)执行真正的生成操作。其中的BUILD_SHARED_LIBRARY变更所示要生成一个动态链接库,相应的 BUILD_STATIC_LIBRARY 将生成一个静态链接库。
更多关于 Android.mk 文件信息一会再说,先来看看怎么编写一个可供Java代码调用的C/C++函数。
打开 JniDemo 录入以下内容
1
2
3
4
5
6
7
8
9
10
11 |
#include
extern "C" {
JNIEXPORT
jstring JNICALL Java_com_leftcode_android_JniDemoActivity_sayHello(
JNIEnv * env, jobject thisz) {
return env->NewStringUTF("Hello JNI!");
}
} |
不用说 extern “C” { } 用于告诉C++编译器函数名称使用C风格编译(C和C++都会对函数名进行修饰以生成最终的链接符号名,但方法不一样,JNI仅识别C风格的符号名)。如果我们的代码采用C语法编译(一般情况下使用.c扩展名可以导致使用C语法编译)则 extern “C” 是不需要的。还可以使用 __cplusplus 宏控制代码同时兼容C和C++,这里不多说。此外这个方法的实现也是C++风格的, C不支持这种this调用。
函数声明中的JNIEXPORT 和 JNICALL 两个宏展开为空,目前NDK编译环境可以去掉,但考虑到与JNI标准规范的一致性,还是最好加上。
jstring返回值类型表示返回一个java字符串。实际上jni.h头文件告诉C++ jstring是个指向_jstring类实例的指针类型,这完全是个谎言,用来忽悠编译器进行一行编译时类型检查而已。
丑陋的 Java_com_leftcode_android_JniDemoActivity_sayHello 大长函数名是JNI规范要求,用来指明这个函数应被链接到哪个包(com.leftcode.android)中的哪个类(JniDemoActivity)下的哪个方法(sayHello)。显然,函数名包括前辍Java、包名称、类名称、方法名称4部分,以下划线连接,包名称中的点也要替换成下划线(相当丑陋的命名规则,如果我们的java方法名或类名或包名有下划线可怎么办啊?当然没什么比Java要求类名与文件名相同,包名与路径名相同更糟糕的了)
再来看函数的两个参数,这两个参数时JNI函数固定的前两个参数定义,一个是JNIEnv指针类型,用于提供JNI接口的很多有用功能,基本相当于Java和C/C++沟通的桥梁;另一个是jobject,指代用于调用这个函数的Java对象(即this引用)。
最后函数的实现中只是简单的返回了一个 jstring 字符串,这个字符串对象的创建用到了JNIEnv 的NewStringUTF函数,基本上在这里所有的java类型的对象都需要直接或间接的通过 JNIEnv 来创建,在C++中是不能通过new操作来创建 Java对象的。
接下来在Java代码中调用下这个sayHello吧。打开 JniDemoActivity.java,在JniDemoActivity类中添加以下代码
1
2
3
4
5
6 |
public native String sayHello();
static {
// 调用本机代码库之前必须先加载库
System.loadLibrary("JniDemo");
} |
然后修改 onCreate 方法:
1
2
3
4
5
6
7 |
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView view = new TextView(this);
view.setText(sayHello());
setContentView(view);
} |
运行应该可以显示Hello JNI!了。
回过头来再说点关于 Android.mk 文件定义的内容。除了上边说到的那几个最常用的东西外,还有两个变量是比较有用的。一个是 LOCAL_STATIC_LIBRARIES, 另一个是 LOCAL_SHARED_LIBRARIES ,与 LOCAL_SRC_FILES有些类似,但用来在当前模块中引入其它的静态库和动态库而不是源文件。比如:
1
2
3
4
5
6
7
8
9
10
11
12 |
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo/foo.cpp
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := bar
LOCAL_SRC_FILES := bar/bar.cpp
LOCAL_STATIC_LIBRARIES := foo
include $(BUILD_SHARED_LIBRARY) |
将把foo静态库链接到bar库中。
另外还有PREBUILT_SHARED_LIBRARY和PREBUILT_STATIC_LIBRARY用于指定已经编译好的库文件的路径而不是其它库的模块定义名称。
如果要控制编译器的行为,可能会用到 LOCAL_CFLAGS 和 LOCAL_CPPFLAGS ,比如
1
2
3
4
5 |
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo.c
LOCAL_CFLAGS += -DBAR=1
include $(BUILD_SHARED_LIBRARY) |
在编译时将添加 -DBAR=1 的标志,最终导致在foo.c中宏BAR展开为1。
最后关于使用C++的几点事项:
C++异常默认被NDK关闭,要在C++中使用异常,应该在 Android.mk 中添加 LOCAL_CPPFLAGS += -fexceptions。注意你不能将C++异常抛给JVM,它不理解,基本上会立即中止运行你的应用。关于本机代码处理Java异常及向JVM抛出异常的方法我计划有时间也整理一篇,这里不多写了。
C++中的RTTI默认也是关闭的, 在 Android.mk 中通过 LOCAL_CPPFLAGS += -frtti 启用。
以上两点还可以通过 Application.mk (相关说明请参考文档吧)中添加 APP_CPPFLAGS += -frtti 或 APP_CPPFLAGS += -fexceptions 为所有的模块启用相关功能。
默认情况下,NDK使用一个C++标准库最小实现,提供包括 new / delete 及一些功能性函数(大部分都是直接调用 C 实现),要使用更丰富的C++标准库功能,可以通过 Application.mk 文件中 APP_STL := stlport_static 指令来使用STLport 静态库,或者 stlport_shared 使用 STLport 动态库。 但STLport 不支持C++异常和RTTI,要使用这些功能,只剩下最后一个选项,APP_STL := gnustl_static 静态链接使用 GNU libstdc++ 库。