不同于<<跨平台编译的经验之谈--cmake编译文件的一般方法>>一文中总结的一般方法, Android由于运行文件的生成必须是java+so的形式,所以我们用cmake进行编译的部分即native部分只能是一个so,就是我们所说的库文件.而早期的Android工程(Eclipse时代)以及Gradle2.2之前,Android默认只能通过ndk-build命令结合.mk文件进行编译.在cmake 2.6.0版本支持了cmake toolchain之后,我们还能通过编写android toolchain来用cmake对Android进行编译.本文中,我们尝试用一种不同的思路,如何在cmake中用ndk-build对Android的native(c/c++code)进行编译.
Android NDK工程
Android应用开发语言是Java,但Goolge也保留了用c/c++进行开发的权利.Android工程一共可以分为两个部分,一是Java部分,用Android Sdk的 android, ant 命令进行编译(目前,最新版本的Android studio已经不支持了,改用gradle进行编译了); 二是c/c++部分, 可以用ndk的ndk-build命令即可.
Android ndk的编译是可以独立于Android工程的,编译出的目标文件是一个共享库文件so, Java部分通过jni技术,加载该so,就可以实现java和c/c++部分的交互.因此,从这点上来说,我们可以单独把Android ndk部分抽离出来,用cmake进行编译.
Android工程目录
my_project
|--Android.mk
|--Application.mk
|--main.cc
|--thirdparty
|--|--include
|--|--|-- *.h
|--|--src
|--|--|-- my_lib.so
my_project就是我们Android 的 ndk部分.你可以把它放在任何一个路径下,只要最后编译时能找到它们即可. .mk文件不一定非要放在my_project目录下,它也可以是任何一个路径下,ndk-build命令在.mk目录下 执行即可.
Android.mk文件
真正编译c/c++的东西应该是gcc/g++/clang等这种编译器命令.工程里每个源码文件的编译其实都是在执行gcc/g++/clang命令.但当工程开始庞大起来时,直接用这些命令一点点写就显得比较复杂,这个时候,我们需要有一套很好的规则来简化我们的工作.而cmake是生成这样规则并能够简化我们工作的跨平台工具集.
一个Android.mk文件应该有如下形式:
LOCAL_PATH := $(call my-dir)
// 包含第三方库my_lib
include $(CLEAR_VARS)
LOCAL_MODULE := my_lib
LOCAL_SRC_FILES := /path_to_my_project/thirdparty/src/libmy_lib.so
include $(PREBUILT_SHARED_LIBRARY)
// 设置工程编译
include $(CLEAR_VARS)
LOCAL_MODULE := my_project
LOCAL_LDLIBS := -landroid -lm -lz ..
LOCAL_SHARED_LIBRARIES : = my_lib
LOCAL_CFLAGS := -fexceptions -frtti ..
LOCAL_C_INCLUDES : = /path_to_my_project/thirdparty/include /path_to_project/. /path_to_system/*/include
LOCAL_CPPFLAGS += -std=c++11
LOCAL_PATH : = /path_to_my_project/.
LOCAL_SRC_FILES := /path_to_my_project/main.cc
include $(BUILD_SHARED_LIBRARY)
// 导入其他.mk工程
$(call import-module, ...)
以上是编译ndk部分的Android.mk文件内容
在include $(CLEAR_VARS) 和 include $(type)之间是编译的一个目标, 其中type编译目标的类型,有:
PREBUILT_SHARED_LIBRARY --已编译的共享库so
PREBUILT_SHARED_LIBRARY -- 已编译静态库.a
包含它们表示我们的.mk文件已经能够找到它,为编译链接所用
BUILD_SHARED_LIBRARY -- 编译生成共享库即so
BUILD_STATIC_LIBRARY -- 编译生成静态哭.a
包含它们,就告诉Android开始进行编译了.那么,怎么找到编译所需的源文件?
LOCAL_SRC_FILES-- 指定源码所在路径
$(call import-module, ...) -- ...表示一个包含Android.mk的路径,它的意思是,当我们需要用到这个Android.mk生成的.so或.a时,我们需要先编译该Android.mk所对应的工程,让它生成我们想要的库.
Android.mk在生成最后的目标文件时,又是如何链接第三方库,及寻找其头文件的呢?
LOCAL_LDLIBS -- 告诉链接器,我们要链接的系统库
LOCAL_SHARED_LIBRARIES, LOCAL_STATIC_LIBRARIES -- 告诉连接器,我们需要链接的第三方库
LOCAL_C_INCLUDES -- 指定链接的库以及源码的头文件的路径
CMake中用ndk-build编译的流程
知道了Android.mk以及知道其用法以后,我们应该想办法用CMake来生成一个这样的Android.mk文件,并且能够调用ndk-build命令进行编译.
为什么需要android.toolchain.cmake
当我们需要跨平台编译时, 我们需要有一个toolchain文件.所谓跨平台编译是指我们编译源码的系统不同于编译目标文件所运行的系统.这个时候,toolchain文件帮助我们指定编译目标的平台,系统,版本,处理器等,以便我们能够正确的编译.
android.toolchain.cmake就是Android目标平台所需要的toolchain. 当我们在执行cmake命令时, 指定CMAKE_TOOLCHAIN_FILE的值为android.toolchain.cmake的全路径,就可以用cmake进行Android编译了.
android.toolchain.cmake可以参考这个 , 它源自于Opencv项目.
认识configure_file
configure_file是cmake中一个很有用的命令,它能够帮助我们将一个文件Copy到我们想要的位置,并可以更改其内容.这对于我们将项目最终生产一个完整的工程非常有用.
利用双@ "@@"
configure_file修改内容的部分,需要用@VARIABLE@进行包裹起来, 其中VARIABLE是我们在cmake编译文件里指定的名字
比如我们在上面创建的那个Android.mk里,我们将包含第三方库的那一部分,写成@MY_LIBRARIY@
其内容我们假设存放在my_project/cmake/templates/Android.mk.in里
我们在CMakeLists.txt文件里,在最终编译之前写:
set(MY_VARIABLE "include $(CLEAR_VARS)\n\tLOCAL_MODULE := my_lib\n\tLOCAL_SRC_FILES := /path_to_my_project/thirdparty/src/libmy_lib.so\n\tinclude $(PREBUILT_SHARED_LIBRARY)")
configure_file("/path_to_my_project/cmake/templates/Android.mk.in" "/path_to_my_project/Android.mk" "ONLY")
如果,Android.mk文件中其它如头文件,库文件,编译器选项都需要通过cmake来指定的话,都可以通过这种方式来配置.
这样我们就能生成一个如上所述的Android.mk文件了.
添加一个cmake编译目标
通过生成Android.mk的方式,我们发现cmake并没有对源码进行编译,而生成最后的编译目标是通过android的ndk-build进行完成的. 但是,我们需要cmake至少执行configure_file来构建Android.mk, 执行 ndk-build命令来完成Android ndk的编译.
如果使得cmake能够执行,我们需要添加至少一个目标即Target.这个Target的作用只是为了cmake能够正常工作,因此可以很简单.
因为Android平台生成的目标文件是库,所以添加Android目标的方式不同与其它平台的add_executable,它是add_library.
set(SRC dummy.cc) //dummy.cc里就一行代码x = 5.0;即可
add_library(Dummy SRC)
利用add_custom_command
add_custom_command 能在我们生成编译目标文件后执行命令, 多用来做拷贝文件到安装目录的工作.
add_custom_command(OUTPUT output1 [output2...] COMMAND command1 [ARGS] [args1...] [COMMAND command2 [ARGS][args2...]...] [MAIN_DEPENDENCYdepend] [DEPENDS[depends...]][IMPLICIT_DEPENDSdepend1[depend2]...][WORKING_DIRECTORYdir] [COMMENTcomment] [VERBATIM][APPEND])
如上是add_custom_command的定义,其中[]扩起来的都是可选的选项.OUTPUT是我们要编译输出的目标文件, COMMAND是我们要执行的命令,这些命令大多是对文件或者文件夹的操作.
add_custom_command(
TARGET Dummy
POST_BUILD
COMMAND ndk-build all -j2 V=1 NDK_DEBUG=1
WORKING_DIRECTORY "/path_to_my_project/")
POST_BUILD --表示在我们编译生成Dummy文件后,即add_library执行后,再去执行COMMAND ndk-build命令.
WORKING_DIRECTORY-- 是我们执行ndk-build命令的目录,即Android.mk文件存放的目录.
总结
以上,我们说了如何用cmake执行ndk-build来编译Android c/c++部分.它主要还是通过cmake来生成Android.mk文件,并自动调用nkd-build命令来执行Android.mk的编译规则.而生成Android.mk的关键在于,我们将我们需要的信息如头文件路径,库路径,编译选项等通过cmake设置变量,并用configure_file命令来生成.
它与编译其他平台项目有着较大的区别,即cmake不参与生成编译规则工作,也有一个较小的区别,即添加编译目标如果是可执行文件,前者都是add_executable, 而Android只能是add_library.
以后,我们会探讨如何用cmake及android.toolchain.cmake来对Android ndk部分进行直接编译.