浅谈独立使用NDK编译库文件(Android)
阅读前准备
这是一篇相对入门的文章。文中会涉及到少许NDK的知识,但个人认为对初学者来说都相对比较实用,因为都是在平时项目中遇到的(目前自己也是初学者)。一些其他高深的技术不再本文探讨范围之内(因为我不懂)。文章中可能会存在一些啰里八嗦的地方,抱歉,目前的行文风格如此,考虑以后变得牛逼点儿再改改文风,毕竟现在的阶段还是自嘲的情感因素占上风。
你要知道什么是NDK,我是用cocos2dx的,所以平日里还算经常有接触,如果不知道,那就skip这篇文章吧,否则你也看不懂,要不就自己google去。这里只会展开和Android相关的部分,iOS点到为止,详细不讨论。
我使用NDK的目的
cocos2dx之所以能跨平台,是因为那些被支持的设备的OS都能用某种方式运行C/C++生成的程序(或者库)。本身Android运行的cocos2dx的程序就是通过编译好一个大的动态库让Java加载实现程序启动的。一般情况下,cocos2dx的项目已经帮你屏蔽这些设置等的麻烦事情,至多增加一个你自己添加的源文件到mk文件中罢了,所以平时其实去接触这个的机会也不算太多,但是往往只要一有这种知识的涉及,就直接抓瞎了(这也是所谓框架带来的问题,虽然能让人从繁重的底层处理中解放出来,但是不知甚解往往遇到一些略微涉及到这些方面的问题就直接被拍死了,扯远了)。
特别是在接入其他平台SDK的时候尤为明显,好多三方的库都是带*.so文件的,这和平时用的代码隔了至少两层(cocos2dx的C++,Android的Java,Android的底层操作系统),因为*.so文件明显是给Android的OS去调用的,JAVA在这里知道个毛线。
就算不管三方的SDK接入,有很多情况还是会遇到一些棘手的问题。比如,你需要管理并使用socket,有好多解决方案, 比如在应用的C++层采用统一的接口,然后各自平台各自实现,Android的么就通过JNI去和JAVA交互,反正java的三方库是大而全的,基本找什么都有;iOS么也不差,类库也相当全(其他平台暂时省略,没弄过)。但是在cocos2dx中,首先要考虑的,我觉得是跨平台问题,如果各自平台各自实现,那就不要用cocos2dx了,直接按平台分几个项目组各自写就是了,难说执行效率还高好几个档次,但是开发成本就不好讲了。项目中,这个例子中我最终考虑使用的是libevent库,在Android和iOS上只要简单把对应的库编译出来就可以了,从调用到封装也都是C++的,和cocos2dx无缝连接,平滑过渡,个人感觉棒棒的。Android下面编译出动态库(也可以使用静态库)就靠NDK了,当然iOS上也有NDK的概念,反正说穿了,也就是个交叉编译的环境,XCode里面也带全了iOS对应的各种平台的交叉编译环境,原理是一样的,基于MAC是基于FreeBSD的前提,他们的编译过程其实也很类似(至少对我来讲),都是写个makefile文件,用各自平台对应的gcc折腾一下。
其实这次写这篇文章的起意是因为sqlite库的使用,下面文章中也会以这个做例子来阐述如何单独编译个动态库出来。
适用本篇文章的环境以及程序版本
操作系统:WIN7 (32bit/64/bit)(XP这种应该也问题不大)
NDK版本:r8、r9 ... (r8以下的么用过,不清楚)请猛击我到下载界面
cygwin:NULL(独立编译暂时用不到,虽然NDK的INSTALL文档里面说WINDOWS下还是需要装cygwin)
平时项目中NDK的使用
iconv库
我自己项目中第一次去看和用NDK是iconv库。Android和iOS的程序都能很好得支持UTF8编码,但是因为我是用win32做测试,并且用VS2010作为开发工具的,平时的测试也都是VS调的,但是它对UTF8编码的支持很不友好,这点应该做cocos2dx的人都折腾过,开始翻来覆去想解决这个问题,后来找到篇文章说是编译器的缘故,并且MSDN上回复的意思是编译器小组也不准备就此问题来做适应修改(尼玛,心中千万只草泥马在奔腾),要么索性用UNICODE。UNICODE说实话之前被坑过几次,也没搞太明白,因为项目时间的缘故(尼玛,在开发时间和产品质量中找平衡是永远的痛)还是用自己熟悉的本地编码吧,反正目前没有国际化的预期,且碰到的问题都能解决。不过在字符串转换上还是要费点周章的,于是就用到了iconv库。这个在win32、android、iOS上都有对应的库(好嘛,就你了)。
在使用上(这里只记录下流水帐,详细不展开),其实也是照样画葫芦。首先把iconv源代码下过来,放cocos2dx源代码根目录下(这个是为了让NDK_MODULE_PATH中能找到iconv模块,这部分可以参考之前的一篇文章:cocos2dx 中 Android NDK 加载动态库的问题),然后编写好iconv库的Android.mk文件(也就是编译所需要用到源代码和模块名称神马的,还有就是编译出来的是静态库还是动态库),打开项目的jni/Android.mk文件,添加上对应的库依赖即可。当你整个项目编译的时候就会看到iconv的源文件也一起编译了(不知道哪里看?尼玛,感觉不会再爱了……)。
libevent库
这个是用到的第二个三方库,其载入的方法和iconv库一样,不再赘述,官方都有源码下载。或者翻这篇文章:使用cygwin和NDK编译Android版本的libevent,不过里面有些概念当时没理解,比如即使在ubuntu下交叉编译,用的也是NDK,不存在说使用ubuntu版本的gcc编译的情况(莫非这就是之前编译出来不能用的缘故?应该不会,呵呵)。
带着项目编译的麻烦之处
之前用到的iconv和libevent的库都是采用静态库的方式(前面没说明,反正也不是重点,因为用动态库的方式也一样),并且是带着项目一起编译的。如果来个新项目,这些代码还是会重新被编译,因为这几个库的Android.mk是在cocos2dx项目中的jni/Android.mk中被加载的。如果库小,还能忍受,因为编译一次的代价也不算太大,稍微等等就行了,但是如果是以下的情况,估计大家就会喝咖啡喝到吐为止:
- 库本身就比较大,一次编译需要的时间比较长
- 随着新功能增加和开发的持续,会加入各种三方库,本身库的数量会变多
- 你要编译不同CPU架构对应的库(这个真是太苦逼了)
并且,修改了Application.mk后势必会重新编译,那个苦啊(连同2dx的源文件一起重编啊,尼玛坑爹啊)。本身,程序在发布的时候,会移除Application.mk中的一些宏定义,比如 -DCOCOS_DEBUG=1。如果,我是说如果,以上三个条件都被无情得满足了,那么,一旦编译启动的时候,你就可以打个电话约上两三好友,跑到市中心的星巴克,点上几杯卡布其诺(做得不好看让他们重做,味道不对也让他们重做,以上动作可以重复N次),然后装逼一个下午,顺带可以和好友吐槽下这坑爹的交叉编译耗时冗长令人发指。聊得累了,也差不多可以告别这悠长的下午茶时间,跟着节奏慢悠悠回到电脑前,一看,尼玛,还在编译mips的版本。当然,后面的各渠道打包又是一条漫长的不归路。这时候我只想说两个字:『呵呵』。
回到正题,其实这方面的的问题,cocos2dx里面已经有很好的方法去避免了(其实也不算是cocos2dx去避免,而是gcc),那就是prebuilt。在cocos2dx\platform\third_party\android\prebuilt目录下,有好多预编译好的库,如libcurl,libpng等等。这些库是不会重新编译的(你也编译不了,只有头文件,除非你吃饱了去自己整源代码弄一份),可以参考项目的jni/Android.mk中对于这些库的加载,然后自己写一个。正好,昨天在做一个消息中心的东西,因为要保存用户的离线消息,所以,再三考虑,还是使用sqlite来作为『存储介质』。下面就来讲下生成sqlite的预编译版。
用NDK编译独立的库文件
从这里开始,建议对NDK不太了解的人去看下NDK解压后根目录下的README.txt,里面讲了你大致需要至少的知识,以及如果不知道要去看哪个文档。
硬盘里面要有NDK,版本建议r8以上(包含r8),并且把NDK的目录加到环境变量中去,使得shell能在任意目录访问到NDK目录下的ndk-build(或者ndk-build.cmd)文件。
Sqlite库的使用
cocos2dx源码中有sqlite的库,但是很奇怪的是只有win32的版本,虽然Android和iOS的系统中都自带sqlite库,但是如果我们程序中要用到的话,要么通过转接层(如JNI)去调用系统原生的方法去操作,要么自己挂一个sqlite库上去。前一种方法作为C/C++程序员来说显然很不愿意,虽然我对JAVA也不排斥,但是这么调来调去的,自己很容易搞晕,并且两边都有对应的代码,代码耦合度明显上升个数量级,关键尼玛Android和iOS还要各自实现一份,即使各自的实现不麻烦,但只要想到要这么做我就觉得好麻烦,囧rz。所以我毫不犹豫选择挂库的方式。
当然,我对cocos2dx不带Android和iOS的sqlite库的方式也有点想法,不知道是不是本来就不用实现呢?于是我把真机上system/lib目录下的libsqlite.so文件拿出来,然后加载到项目中去,竟然也可以用,呵呵,无语。不过link的时候,报了一堆的warning,估计是Android系统带的sqlite库是有其他库依赖,这些库明显在Android系统中。iOS的没试过,估计也差不多。每次编译都要看那一堆warning,好蛋痛,并且我发现最终我的方法还是会让这个libsqlite.so装到apk中(尼玛不是系统自带么),那还不如我自己编译一个放上去安全,还不用考虑版本兼容性的问题。
Sqlite库的编译
首先要去下载sqlite的源代码,我下载到的版本是3.8.0.2(请猛击我下载),释放到任意目录,我是把它翻到NDK的samples里面了,反正编译一下还是要把那几个库文件拿出来放到cocos2dx的prebuilt目录下的。
建立一个文件夹叫sqlite3(呃,我还是带上大版本,以便于日后区分,目前看来是有点多此一举),然后建立jni文件夹,放上压缩包中的sqlite3.c和两个头文件(头文件目前可有可无),shell那个没用,不需要。然后在目录下建立两个mk文件,Android.mk和Application.mk。写完后,在win7下,可以在sqlite3目录下按住shift点右键,菜单中选择『从此处打开命令窗口(W)』,然后输入 ndk-build 来对项目进行编译。
Android.mk
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_ARM_MODE := arm LOCAL_MODULE := sqlite3 LOCAL_SRC_FILES := sqlite3.c include $(BUILD_SHARED_LIBRARY)
这个文件表明要生成一个libsqlite3.so的动态库,模块名称叫做sqlite3。如果要编译的是静态库的版本,则把后面的$(BUILD_SHARED_LIBRARY)修改为$(BUILD_STATIC_LIBRARY)即可。
Application.mk
APP_OPTIM := release APP_ABI = all
Application.mk的内容比较简单,首先表明是release版本(加不加对编译没有影响,就是编译器会对执行速度进行优化),然后APP_ABI中填入的是all,表明会进行所有支持的CPU架构的版本编译,针对的就是4个,armabi、armabi-v7a、x86、MIPS这四个。
编译速度,我觉得挺慢的。
然后再写一个可以作为库加载的Android.mk,连同这几个文件都放到prebuilt的下面去:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_ARM_MODE := arm LOCAL_MODULE := sqlite3 LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libsqlite3.so include $(PREBUILT_SHARED_LIBRARY)
之后就可以把它放到项目中使用了,当然,项目中要明确加载这个模块。这里只写了动态库的mk,静态库的自己照样画葫芦写一个吧,可以参考已有的实现。具体加载方法可以参考这篇文章:cocos2dx 中 Android NDK 加载动态库的问题,昨天试验了下貌似不需要在Java中显示loadlibrary这个sqlite库,还是各自试验下吧。
我把编译好的静态库和动态库都放在这里可以下载(意思意思,卖一个币,贪财贪财):请猛击我这里下载