作者:郑萌,华清远见嵌入式培训中心讲师。
Android NDK(Android Native Development Kit)是一系列的开发工具,允许程序开发人员在Android应用程序中嵌入C/C++语言编写的非托管代码。
Android NDK的版本是1.5,集成了交叉编译器,支持ARMv5TE处理器指令集、JNI接口和一些稳定的库文件。由于Android NDK仅支持Android SDK 1.5版本,因此1.0和1.1版本的应用程序不能够使用Android NDK。Android NDK提供一系列的说明文档、示例代码和开发工具,指导程序开发人员使用C/C++语言进行库文件开发,并提供便捷工具将库文件打包到apk文件中。
Android虚拟机允许应用程序源代码通过JNI调用在本地实现的源代码,简单的说,这就意味着:
● 应用程序将声明一个或多个用‘native’关键字的方法用来指明它们是通过本地代码实现的。例如:
代码清单11-1. native用法
native byte[] loadFile(String filePath)
● 必须提供包含实现这些方法的共享库(就是.so),将共享库打包到你的应用程序包apk中,这些库文件必须根据标准的Unix约定来命名为 lib<something>.so,并且是需要包含一个标准的JNI的接口,例如,libFileLoader.so。
● 应用程序必须明确的装载这些库文件(.so文件),比如,在程序的开始装载它,只需要简单的添加几句源代码:
代码清单11-2. 装载库文件
static {
System.loadLibrary(“FileLoader”);
}
Android NDK对于Android SDK只是个组件,它可以帮我们生成的JNI兼容的共享库可以在大于Android1.5平台的ARM CPU上运行,将生成的共享库拷贝到合适的程序工程路径的位置上,以保证它们自动的添加到你的apk包中(并且签名的)。
而且,Android NDK还提供了
● 一组交叉编译链(编译器、链接器等)来生成可以在Linux,OS X和Windows(用Cygwin)运行的二进制文件;
● 一组与由Android平台提供的稳定的本地API列表的头文件,它们在docs/STABLE-APIS.html中有说明;
● 一个编译系统(build system)可以允许开发者写一个非常短的编译文件(build files)去描述哪个源代码需要编译,并且怎样编译。编译系统可以解决所有的toolchain/platform/CPU/ABI细节的问题。并且,较晚的NDK版本中还添加了更多的可以不用改变开发者的编译文件的情况下的toolchains、platforms、系统接口。
通过以上的叙述,我们知道Android NDK解决了核心模块使用托管语言开发执行效率低下的问题;允许程序开发人员直接使用C/C++源代码,极大的提高了Android应用程序开发的灵活性。
但同时Android NDK也存在着一些不足。
NDK并不是一个可以编写通用的源代码并且可以在Android设备上运行的方法,你的应用程序还是需要使用JAVA程序,适当的处理系统事件来避免“应用程序没有反应”的对话框或者处理Android应用程序的生命周期。注意:可以适当的在源代码中写一个复杂的应用程序,用于启动/停止一个小型的“应用程序包”。
NDK在Android平台仅仅提供了有限的本地API和库文件的支持的系统头文件,然而一个标准的Android系统镜像包括许多本地共享库,这些都应该被考虑在更新和发行版本的可以彻底改变的实现细节。如果Android系统库没有明确的被NDK明确的支持,然后应用程序不应该依赖于它提供的,或者打破了将来在各种设备上的无线系统更新,选定的系统库将逐渐被添加到稳定的NDK API中。
NDK编译环境
Android NDK编译环境支持Windows XP、Linux和MacOS,本章节仅介绍Windows系统的编译环境配置方法
Windows系统的编译环境配置方法:
1 下载Android NDK的安装包
在Google的官方网站下载Android NDK的安装包,下载地址是http://developer.android.com/sdk/ndk/index.html,打开下载页面后选择的下载文件为android-ndk-r6-windows.zip。
图11-1 android-ndk-r6-windows.zip
将下载的ZIP文件解压缩到用户的Android开发目录中,作者将Android NDK解压到E:\Android目录中,ZIP文件中包含一层目录,因此Android NDK的最终路径为E:\Android\android-ndk。
2 下载并安装Cygwin
Android NDK目前还不支持在Windows系统下直接进行交叉编译,因此需要在Windows系统下安装一个Linux的模拟器环境Cygwin,完成C/C++代码的交叉编译工作。
Cygwin 是 Windows 上类似于 Linux 的环境。它包括一个提供 UNIX 功能性基本子集的 DLL 以及在这之上的一组工具(所以在linux下不需要使用)。
Android NDK要求GNU Make的版本高于或等于3.18,之前的版本并没有经过测试,因此需要安装较新版本的Cygwin。Cygwin的最新版本可以到官方网站http://www.cygwin.com下载,也可以到中文的映像网站http://www.cygwin.cn下载。
在Cygwin的安装过程中,需要将Devel下的gcc和make的相关选项选上,否则Cygwin将无法编译C/C++代码文件。
3 配置Cygwin的NDK开发环境
在缺省情况下,Cygwin安装在C盘的根目录下,修改C:\cygwin\home\username\.bash_profile文件,username会根据用户使用的用户名称而变化。
在.bash_profile文件的结尾处添加如下代码
代码清单11-3. .bash_profile
ANDROID_NDK_ROOT=/cygdrive/e/android/android-ndk
export ANDROID_NDK_ROOT
上面的代码说明了Android NDK所在的目录,目录是e盘android/android-ndk。
如果Android NDK安装在c盘的TestAndroid/android-ndk中,则上面的代码则应该为
代码清单11-4. .bash_profile
ANDROID_NDK_ROOT=/cygdrive/c/TestAndroid/android-ndk
export ANDROID_NDK_ROOT
4 测试开发环境是否可以正常工作
首先启动Cygwin,然后切换到<Android NDK>/build目录中,运行host-setup.sh文件。
如果运行结果如下图,说明Android NDK的开发环境已经可以正常工作了。
至此,Android NDK的编译环境已经安装配置完毕。下面我们来了解一下Android NDK的目录结构。
在android-ndk目录中,包含5个子目录和2个文件:其中,
● apps目录是Android工程的保存目录,子目录hello-jni和tow-libs是NDK自带的两个示例目录;
● build目录保存了交叉编译工具、编译脚本和配置文件;
● docs目录是帮助文档的保存目录;
docs目录中的帮助文件说明:
表11-1. docs目录中的帮助文件说明表
● out目录是交叉编译的输出目录,保存输出的so文件;
● sources目录是C/C++源代码文件的保存目录,其下的hello-jni和tow-libs子目录,分别保存了NDK自带示例所需要的C/C++源代码文件;
● GNUmakfile文件和README.TXT文件分别是make工具的配置文件和NDK的说明文件。
Android NDK自带两个示例hello-jni和tow-libs。hello-jni是一个非常简单的例子,非托管代码实现了一个可以返回字符串的共享库,Android工程调用这个共享库获取字符串,然后显示在用户界面上;tow-libs是稍微复杂一些的例子,使用非托管代码实现了一个数学运算的共享库,Android工程动态加载这个共享库,并调用其中的函数,函数功能是通过使用静态库实现的。
NDK开发示例
在进行NDK开发时,一般需要同时建立Android工程和C/C++工程,然后使用NDK编译C/C++工程,形成可以被调用的共享库,最后共享库文件会被拷贝到Android工程中,并被直接打包到apk文件中。
下面我们通过AndroidNdkDemo示例说明如何进行Android NDK开发。
AndroidNdkDemo是一个进行加法运算的示例,程序会随机产生两个整数,然后调用C语言开发的共享库对这两个整数进行加法运算,最后将运算结果显示在用户界面上。AndroidNdkDemo示例的界面如下图所示:
进行Android NDK开发一般要经过如下的步骤:
1 建立Application.mk文件
在apps目录中建立应用程序目录,AndroidNdkDemo示例的应用程序目录为ndk-demo;在ndk-demo目录中建立一个空目录project,这个目录以后会用来存放Android工程;在ndk-demo目录中建立一个名为Application.mk的文件,用来描述Android工程将调用的共享库。
在进行NDK开发时,在应用程序目录中一定要有Application.mk文件,用来声明Android工程需要调用的非托管模块(如静态库或共享库)。AndroidNdkDemo示例的Application.mk的代码如下:
代码清单11-5. Application.mk
APP_PROJECT_PATH := $(call my-dir)/project
APP_MODULES:= add-module
第1行的变量APP_PROJECT_PATH表示Android工程所在的目录,在生产共享库文件后,APK将自动将共享库文件拷贝到<app>\libs\armeabi目录中,本示例将共享库文件拷贝到apps\ndk-demo\project\libs\armeabi目录中;第2行代码中的变量APP_MODULES表示Android工程需要调用的非托管模块,如果存在多个非托管模块,使用空格进行分隔。本示例调用的非托管模块为add-module,对应在后面涉及的Android.mk文件。
Application.mk的变量说明:
表11-2. Application.mk的变量表
2 建立Android工程
在project目录中建立Android工程时,需要取消复选框“Use default location”,并指定预先建立的project文件夹作为工程文件夹。
在建立AndroidNdkDemo工程后,修改main.xml文件,添加一个id为display的TextView和一个id为add_btn的Button按钮。
程序中的生产随机数和调用的代码在AndroidNdkDemo.java文件中,下面是AndroidNdkDemo.java文件的核心代码:
代码清单11-6. AndroidNdkDemo.java核心代码:
public class AndroidNdkDemo extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
final TextView displayLable = (TextView)findViewById(R.id.display);
Button btn = (Button)findViewById(R.id.add_btn);
btn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
double randomDouble = Math.random();
long x = Math.round(randomDouble*100);
randomDouble = Math.random();
long y = Math.round(randomDouble*100);
//System.loadLibrary("add-module");
long z = add(x, y);
String msg = x+" + " +y+" = "+z;
displayLable.setText(msg);
}
});
}
//public native long add(long x, long y);
public long add(long x, long y){
return x+y;
}
}
上面的代码有一个NDK开发的小技巧,在开发C/C++的共享库前,可以使用具有相同和相近功能的Java函数进行替代。
在代码第17行本应该调用共享库的add()函数,但为了便于开发和调试,在代码第25行到第27行,使用Java代码开发了一个功能相同的add()函数,这样即使在没有完成C/C++的共享库开发前,也可以对这个Android工程进行界面部分的调试。
第16行和第23行注释掉的代码,就是在C/C++的共享库开发完毕后需要使用的代码,其中第16行是动态加载共享库的代码,加载的共享库名称为add-module;第23行用来声明共享库的函数,使用C/C++开发的共享库必须有同名的函数。在共享库开发完毕后,取消第16行和第23行代码的注释,并注释掉第25行到第27行代码,这样程序就可以正常调用共享库内的函数进行加法运算。
3 建立Android.mk文件
建立C/C++源代码文件前,首先需要在sources目录中建立模块目录,AndroidNdkDemo示例的模块目录为add-module,这个模块目录的名称与Application.mk文件中声明的模块名称相同。add-module目录中包含两个文件,Android.mk和add-module.c。
Android.mk是为NKD编译系统准备的脚本文件,用来描述模块需要编译C/C++文件的信息。通常NKD编译系统会搜寻$NDK/sources/*/目录中的所有Android.mk文件,但如果程序开发人员将Android.mk文件放置在下一级目录中,则需要在上一级目录中的Android.mk文件添加如下代码:
代码清单11-7. Android.mk
include $(call all-subdir-makefiles)
下面来分析AndroidNdkDemo示例的add-module模块的Android.mk文件,Android.mk文件的代码如下:
代码清单11-8. Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := add-module
LOCAL_SRC_FILES := add-module.c
include $(BUILD_SHARED_LIBRARY)
每个Android.mk文件都必须以第1行代码开始,变量LOCAL_PATH用来定义需要编译的C/C++源代码的位置,my-dir由NKD编译系统提供,表示当前目录的位置。代码第3行的include $(CLEAR_VARS)表示清空所有以LOCAL_开始的变量,例如LOCAL_MODULE、LOCAL_SRC_FILES、LOCAL_STATIC_LIBRARIES等,但第1行定义的LOCAL_PATH不在清空的范围内。因为所有的脚本都将粘贴到同一个GNU Make的执行上下文中,而且所有变量都是全局变量,因此必须在每次使用前清空所有以前用过的变量。
第5行代码变量LOCAL_MODULE用来声明模块名称,模块名称必须唯一,而且中间不能够存在空格。NKD编译系统将会在模块名称前自动添加lib前缀,然后生产so文件。这里的模块名称为add-module,生产的共享库文件名为libadd-module.so。但需要注意的是,如果程序开发人员使用具有lib前缀的模块名称,NKD编译系统将不再添加前缀,例如模块名称为libsub,生产的共享库文件名为libsub.so。
第6行代码中的变量LOCAL_SRC_FILES表示编译模块所需要使用的C/C++文件列表,但不需要给出头文件的列表,因为NKD编译系统会自动计算依赖关系。add-module模块仅需要一个C文件,文件名为add-module.c。缺省情况下,结尾名为.c的文件是C语言源文件,结尾名为.cpp的文件是C++语言源文件。
第8行代码include $(BUILD_SHARED_LIBRARY)表示NKD编译系统构建共享库,如果变量BUILD_SHARED_LIBRARY更改为BUILD_STATIC_LIBRARY,则表示需要NKD编译系统构建静态库。
4 建立C源代码文件
根据Android.mk文件的声明,add-module模块仅包含一个C源代码文件add-module.c。add-module.c文件的作用是实现两个整数加法运算功能,全部代码如下:
代码清单11-9. add-module.c
#include <jni.h>
jlong Java_edu_hrbeu_AndroidNdkDemo_AndroidNdkDemo_add( JNIEnv* env,jobject this, jlong x, jlong y )
{
return x+y;
}
第1行代码引入的是JNI(Java Native Interface)的头文件;第3行代码是函数名称,jlong表示Java长型整数,Java_edu_hrbeu_AndroidNdkDemo_AndroidNdkDemo_add的构成为Java_<包名称>_<类>_<函数>,其中<函数>的名称和参数要与AndroidNdkDemo.java文件定义的函数一致,AndroidNdkDemo.java文件定义的函数为public native long add(long x, long y);第5行代码用来返回加法运算结果。
5 编译共享库模块
首先启动cygwin,然后切换到Androd NDK的主目录下,键入如下的编译命令
代码清单11-10. 命令行
make APP=ndk-demo
ndk-demo是apps目录下的应用程序目录名称。在指定应用程序(目录)名称后,NKD编译系统会首先找到目录中的Application.mk文件,根据Application.mk文件的信息,确定该Android共享需要使用add-module模块;然后在sources目录中搜索所有Android.mk文件,在找到与add-module模块匹配的Android.mk文件后,根据Android.mk文件提供的信息编译指定的C/C++源代码文件,形成共享库文件;最后将生产的共享库文件拷贝到Android工程的指定目录中。
下图是编译成功的提示信息
提示信息包括编译add-module模块所使用到的文件,生产so文件的文件名和so文件的安装位置。为了确认是否成功编译了模块,用户可以打开apps/ndk-demo/project/libs/armeabi目录,如果目录中存在libadd-module.so文件,则表示编译成功。
6 运行Android程序
在运行AndroidNdkDemo示例程序前,务必将AndroidNdkDemo.java文件中第16行和第23行的注释取消,并注释掉第25行到第27行代码。
NDK开发技巧
1. 在JNI中打印Logcat
首先我们需要在cpp文件中加入 #include <android/log.h> 这个头文件,NDK有关android自己的就给我们这个唯一的文件log.h,其他的需要我们自己hack diy来解决。
代码清单11-11. 在JNI打印Logcat
jstring jlog; //从Java传来需要打印的字符
jboolean isCopy;
const char * szLog = (*env)->GetStringUTFChars(env, jlog, &isCopy); //将java的unicode字符转化为utf8字符
__android_log_print(ANDROID_LOG_WARN, “android123-cwj”, "from ndk = %s", szLog); //打印logcat
(*env)->ReleaseStringUTFChars(env, jlog, szLog); // 释放内存
上面这段比较简单,其中使用__android_log_print函数打印Logcat,第一个参数为log的level,在log.h头文件中定义了 ANDROID_LOG_UNKNOWN = 0、 ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ ANDROID_LOG_VERBOSE, ANDROID_LOG_DEBUG, ANDROID_LOG_INFO, ANDROID_LOG_WARN, ANDROID_LOG_ERROR, ANDROID_LOG_FATAL, ANDROID_LOG_SILENT等类型,第二个参数为tag标签,第三个为需要打印的字符。整个例子比较简单,但方便了很多调试。
2. Android NDK给我们提供了zlib库的支持,可以通过本地的方法解压缩zip文件。
3. 操作字符串
有关C语言运行库的一些方法,在string.h文件中描述的比较清楚,可以方便的操作字符串 ,比如
代码清单11-12. 操作字符串
extern void* memccpy(void *, const void *, int, size_t);
extern void* memchr(const void *, int, size_t);
extern void* memrchr(const void *, int, size_t);
extern int memcmp(const void *, const void *, size_t);
extern void* memcpy(void *, const void *, size_t);
extern void* memmove(void *, const void *, size_t);
extern void* memset(void *, int, size_t);
extern void* memmem(const void *, size_t, const void *, size_t);
extern void memswap(void *, void *, size_t);
extern char* strchr(const char *, int);
extern char* strrchr(const char *, int);
extern size_t strlen(const char *);
extern int strcmp(const char *, const char *);
extern char* strcpy(char *, const char *);
extern char* strcat(char *, const char *);
extern int strcasecmp(const char *, const char *);
extern int strncasecmp(const char *, const char *, size_t);
extern char* strdup(const char *);
extern char* strstr(const char *, const char *);
extern char* strcasestr(const char *haystack, const char *needle);
extern char* strtok(char *, const char *);
extern char* strtok_r(char *, const char *, char**);
extern char* strerror(int);
extern int strerror_r(int errnum, char *buf, size_t n);
extern size_t strnlen(const char *, size_t);
extern char* strncat(char *, const char *, size_t);
extern char* strndup(const char *, size_t);
extern int strncmp(const char *, const char *, size_t);
extern char* strncpy(char *, const char *, size_t);
相信这些肯定比Java效率快上不少,至少有指针用,在处理字符串等方面效率可能是几百倍几千倍的提升。
4. NDK处理I/O
NDK在I/O处理上会更有效率,比如提供了Socket和File的本地读写,在socket.h文件中包含了标准Socket的各种方法,可以处理 TCP和UDP报文,这样和C++服务器的互通,通过NDK解决,不用再为Java的类型字节对齐以及编码而烦恼。
代码清单11-13. 处理I/O
extern int memcmp(const void *, const void
__socketcall int socket(int, int, int);
__socketcall int bind(int, const struct sockaddr *, int);
__socketcall int connect(int, const struct sockaddr *, socklen_t);
__socketcall int listen(int, int);
__socketcall int accept(int, struct sockaddr *, socklen_t *);
__socketcall int getsockname(int, struct sockaddr *, socklen_t *);
__socketcall int getpeername(int, struct sockaddr *, socklen_t *);
__socketcall int socketpair(int, int, int, int *);
__socketcall int shutdown(int, int);
__socketcall int setsockopt(int, int, int, const void *, socklen_t);
__socketcall int getsockopt(int, int, int, void *, socklen_t *);
__socketcall int sendmsg(int, const struct msghdr *, unsigned int);
__socketcall int recvmsg(int, struct msghdr *, unsigned int);
extern ssize_t send(int, const void *, size_t, unsigned int);
extern ssize_t recv(int, void *, size_t, unsigned int);
__socketcall ssize_t sendto(int, const void *, size_t, int, const struct sockaddr *, socklen_t);
__socketcall ssize_t recvfrom(int, void *, size_t, unsigned int, const struct sockaddr *, socklen_t *);
5. 最新版本NDK支持最新的OpenGL ES版本
对于我们开发最方便的还要属OpenGL ES了,在NDK中所有GL的函数,可以在gl.h和glext.h中查找到,最新版本NDK支持最新的OpenGL ES版本,可以方便移植iPhone上的3D游戏了。Android123已经成功将Cube例子用NDK改造运行,确实比Java来的更方便和亲切。
嵌入式及3G相关资源及学习请点击:嵌入式开发视频 android开发视频 android培训 3G培训 QT培训 QT开发视频 物联网培训 物联网技术视频 嵌入式学习