大家在编写Android的Native代码时,经常会接触到一个叫做Android.mk的文件。
虽然编译的时候都用到的是make,但是这个Android.mk文件里的语法还跟一般的make文件语法不太一样。
本质上,Android.mk只是GNU MakeFile的一个片段,编译系统在编译的时候有可能会多次解释Android.mk文件,所以要尽量少在脚本里面申明变量,也不要假设任何没有在脚本中定义的条件。
Android.mk文件是用来让你把源码组织成各个“模块”。所谓模块,由以下三种构成:
- 静态库
- 共享库
- 独立的可执行文件
只有共享库可以被包含到apk应用程序包里,但是静态库可以被用来生成共享库。
可以在一个Android.mk文件中定义一个或者多个模块,并且可以多个模块复用同样的源代码。
编译系统已经替你处理了很多琐碎的事情。例如,你不需要在Android.mk文件中罗列.h头文件和显式声明生成文件之间的依赖关系。NDK编译系统会自动为你计算出来。
这也意味着,当升级到新版的NDK时,不需要更改Android.mk文件就可以相互兼容。
NDK中的Android.mk文件语法和Android源码中的Android.mk文件语法非常相近。但是其实编译系统实现是不一样的,这是有意这样设计的,为了让应用程序开发者可以更加方便的复用第三方库的源码。
简单的例子
在正式描述语法细节之前,让我们来看一个简单的例子“hello JNI”,这个例子包含在NDK里的以下目录中:
samples/hello-jni
在这个目录里,我们可以看到
- src目录,里面包含了例子用到的Java代码
- jni目录,里面包含了例子用到的Native代码(jni/hello-jni.c)
- jni/Android.mk文件,描述了要NDK编译系统编译出来的共享库。内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LBRARY)
下面来稍微解释一下:
LOCAL_PATH := $(call my-dir)
所有的Android.mk文件必须要以LOCAL_PATH变量定义开头。它用来定位要编译的源代码在代码树中的位置。在本例中,宏函数“my-dir”是由编译系统提供的,用来返回当前目录的路径(也就是包含此Android.mk文件的目录)。
include $(CLEAR_VARS)
CLEAR_VARS变量也是由编译系统提供的,它指向了一个特殊的GNU MakeFile文件,这个文件的用处是为你清理许多LOCAL_XXX变量(例如,LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARY),除了前面才定义的LOCAL_PATH变量。必须要这样做的目的是,所有的编译脚本都在同一个GNU Make执行环境中分析,所有这些变量都是全局的。如果不及时清理,前面编译脚本定义的变量会对当前编译脚本产生影响。
LOCAL_MODULE := hello-jni
LOCAL_MODULE变量必须定义,用来标识你在Android.mk文件中描述的每一个模块。名字必须唯一,并且不能包含空格。注意,编译系统会自动给生成的文件加上适当的前缀和后缀。也就是说,如果一个共享模块名是“foo”的话,那么生成的文件就是“libfoo.so”。
重要提示:如果你把你的模块命名成“libfoo”的话,编译系统是不会再加上“lib”前缀的,还是会生成“libfoo.so”文件。这样设计的目的是为了支持从AOSP那移植过来的代码。
LOCAL_SRC_FILES := hello-jni.c
变量LOCAL_SRC_FILES必须包含编译模块必须要的C或者C++代码源文件。注意,请不要在此列出头文件和其它的各种包含文件,因为编译系统会自动帮你算出依赖关系,请只列出需要编译器编译的代码源文件。
注意,缺省的C++代码源文件的扩展名是“.cpp”。但是,可以通过定义变量LOCAL_CPP_EXTENTION来指定成其它的名字。定义的时候不要忘记起始的点(例如“.cxx”可以,但是“cxx”就不行)。
include $(BUILD_SHARED_LIBRARY)
变量BUILD_SHARED_LIBRARY也是由编译系统提供的,指向了一个GNU MakeFile脚本文件。这个脚本文件是用来负责收集所有你从“include $(CLEAR_VARS)”开始定义的所有LOCAL_XXX变量中包含的信息,来决定如何编译,编译成什么。相应的还有BUILD_STATIC_LIBRARY变量,用来生成一个静态库。
在samples目录下,还有很多更加复杂的例子,每个Android.mk都包含注释。
自定义变量
NDK编译系统预留了如下的变量名:
- 所有以LOCAL_开头的变量(如LOCAL_MODULE)
- 所有以PRIVATE_、NDK_或者APP_开头的变量(供内部使用)
- 小写字母构成的变量(内部使用,例如my-dir)
只要符合以上三个规则,其它的你就可以自由定义了。如果你要定义的话,我们建议用MY_前缀,下面是一个简单的例子:
MY_SOURCES := foo.c
ifneq ($(MY_CONFIG_BAR),)
MY_SOURCES += bar.c
endif
LOCAL_SRC_FILES += $(MY_SOURCES)
NDK提供的变量
这些GNU Make变量是在解析你的Android.mk文件之前就有编译系统定义好的。注意,在某些特定的情况下,NDK可能会多次解析你的Android.mk文件,并且每次预先定义的变量值会不一样。
CLEAR_VARS
指向一个编译脚本。这个编译脚本的目的是清空所有接下来脚本中会用到的LOCAL_XXX变量。这个脚本必须要在定义一个新模块之前被包含进来
include $(CLEAR_VARS)
BUILD_SHARED_LIBRARY
指向一个编译脚本,这个编译脚本可以收集所有你定义的LOCAL_XXX变量中提供的信息,然后决定如何编译目标共享库。注意,最少你要在包含这个脚本之前定义好LOCAL_MODULE和LOCAL_SRC_FILES变量。用法如下:
include $(BUILD_SHARED_LIBRARY)
这将会生成一个名字为lib$(LOCAL_MODULE).so的目标文件。
BUILD_STATIC_LIBRARY
变量BUILD_STATIC_LIBRARY是专门用来编译静态库的。静态库是不能直接用在应用程序中的,但是可以用来构建共享库(参照下面的对LOCAL_STATIC_LIBRARIES和LOCAL_WHOLE_STATIC_LIBRARIES变量的说明)。示例用法如下:
include $(BUILD_STATIC_LIBRARY)
这将会生成一个名为lib$(LOCAL_MODULE).a的目标文件。
PREBUILT_SHARED_LIBRARY
指向一个编译脚本,该脚本用来指定一个预先编译好的共享库。这时候变量LOCAL_SRC_FILES值的含义,就和在BUILD_SHARED_LIBRARY和BUILD_STATIC_LIBRARY里面的不同。前者要设置成一个指向预编译好的共享库文件的路径,而后者是要编译的源文件。
PREBUILD_STATIC_LIBRARY
含义基本和PREBUILD_SHARED_LIBRARY相同,只不过这是指定一个静态库文件。
TARGET_ARCH
TARGET_PLATFORM
目标平台的名字。表示要编译的这个模块,将来要跑在哪个Android目标平台上,例如“android-18”代表Android 4.3系统。
TARGET_ARCH_ABI
TARGET_ABI
NDK提供的宏函数
本节将介绍编译系统预先定义好了的GNU Make宏函数,这些函数必须要像“$(call)”这样调用。它们会返回文本类型的信息。
my-dir
返回最近一次包含的MakeFile的目录位置,通常这就是当前Android.mk文件所在的目录。它可以用来在Android.mk文件的开头定义LOCAL_PATH变量,如下:
LOCAL_PATH := $(call my-dir)
特别要注意的是,该函数确切的说是返回最近一次包含的MakeFile文件的位置。请不要在包含了另外一个文件后调用my-dir宏函数。
例如,考虑以下这种情况:
LOCAL_PATH := $(call my-dir)
... declare one module
include $(LOCAL_PATH)/foo/`Android.mk`
LOCAL_PATH := $(call my-dir)
... declare another module
这里存在的问题是,由于在第二次调用my-dir之前包含了另一个Android.mk文件,就会将LOCAL_PATH设置成$PATH/foo,而不是$PATH。
由于这个原因,如果要包含另外的文件的话,最好将其放在Android.mk文件的最后面,如:
LOCAL_PATH := $(call my-dir)
... declare one module
LOCAL_PATH := $(call my-dir)
... declare another module
# extra includes at the end of the `Android.mk`
include $(LOCAL_PATH)/foo/`Android.mk`
如果这样做不方便的话,在第一次调用my-dir之后,将其值保存在另外一个变量中,例如:
MY_LOCAL_PATH := $(call my-dir)
LOCAL_PATH := $(MY_LOCAL_PATH)
... declare one module
include $(LOCAL_PATH)/foo/`Android.mk`
LOCAL_PATH := $(MY_LOCAL_PATH)
... declare another module
all-subdir-makefiles
返回在当前‘my-dir’目录下,所有子目录中包含的Android.mk文件列表。例如,考虑在以下目录层级中:
sources/foo/Android.mk
sources/foo/lib1/Android.mk
sources/foo/lib2/Android.mk
如果在sources/foo/Android.mk文件中包含下面这一行:
include $(call all-subdir-makefiles)
那么,这一句将自动包含sources/foo/lib1/Android.mk和sources/foo/lib2/Android.mk文件。
该函数可以在多级嵌套的目录结构中,帮助编译系统罗列出里面所有包含的Android.mk文件。而在默认情况下,NDK只会寻找sources/*/Android.mk文件,再下面就不会去查找了。
this-makefile
返回当前MakeFile的路径(这个函数是在哪个MakeFile中调用的)
parent-makefile
返回父MakeFile的路径,也就是包含当前调用这个函数的MakeFile的那个MakeFile。
grand-parent-makefile
不解释了,我想大家应该从名字就能猜到其意思了。
import-module
该函数用于按指定的名字,查找另一个模块的Android.mk文件,并包含到当前的Android.mk中来。用法如下:
$(call import-module,<name>)
上面的宏函数调用,会在 NDK_MODULE_PATH变量里所指定的目录列表的每一个目录中,寻找指定名字的模块,并且找到之后将其包含进来。
模块描述变量
下面介绍的这些变量,专门用来向编译系统描述你模块的一些特性。如果要使用的话,请确保将它们定义在“include $(CLEAR_VARS)”和“include $(BUILD_XXXXX)”之间。而除了一些下面说明的特例,“$(CLEAR_VARS)”会将这些变量都清理掉。
LOCAL_PATH
这个变量用来告诉编译系统当前编译路径是什么,必须要在Android.mk文件的一开头就定义,像这样:
LOCAL_PATH := $(call my-dir)
这个变量不会被“$(CLEAR_VARS)”给清理掉,所以一般情况下每个Android.mk文件只要定义一次就可以了(除非你在一个Android.mk文件中定义了多个模块)。
LOCAL_MODULE
LOCAL_MODULE_FILENAME
LOCAL_MODULE := foo-version-1
LOCAL_MODULE_FILENAME := libfoo
这样的话,最终编译生成的文件就不是libfoo-version-1.so了,而是libfoo.so。
LOCAL_SRC_FILES
LOCAL_SRC_FILES := foo.c \
toto/bar.c
除了上面例子中这种相对路径外,也可以使用绝对路径:
LOCAL_SRC_FILES := /home/user/mysources/foo.c
如必要,请尽量不要使用绝对路径,会导致兼容性问题,可移植性会变差。
LOCAL_CPP_EXTENSION
LOCAL_CPP_EXTENSION := .cxx
另外,从NDK r7开始,可以指定一串扩展文件列表给这个变量:
LOCAL_CPP_EXTENSION := .cxx .cpp .cc
LOCAL_CPP_FEATURES
LOCAL_CPP_FEATURES := rtti
LOCAL_CPP_FEATURES := exceptions
也可以同时指定多个特性(顺序无所谓):
LOCAL_CPP_FEATURES := rtti exceptions
通过设置这个变量,在编译的时候,可以传递相应的选项给编译器或链接器。
LOCAL_C_INCLUDES
LOCAL_C_INCLUDES := sources/foo
或者,也可以这样:
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../foo
如果要在LOCAL_CFLAGS或者LOCAL_CPPFLAGS中使用任何包含选项的话,请确保要包含文件所在的目录已经在LOCAL_C_INCLUDES变量中定义过了。
LOCAL_CFLAGS
LOCAL_CFLAGS += -I<path>
但是,请尽量不要这么做,还是使用前面介绍的LOCAL_C_INCLUDES变量来指定特殊的包含路径,以保证后面用ndk-gdb对程序进行调试的时候,这些特殊路径能自动被包含进来。
LOCAL_CPPFLAGS / LOCAL_CXXFLAGS
LOCAL_STATIC_LIBRARIES
LOCAL_SHARED_LIBRARIES
LOCAL_LDLIBS
LOCAL_LDLIBS := -lz
这样的话,会告诉链接器,在生成最终的二进制文件中包含运行时将动态链接/system/lib/libz.so模块的信息。
LOCAL_LDFLAGS
LOCAL_ALLOW_UNDEFINED_SYMBOLS
LOCAL_ARM_MODE
LOCAL_ARM_MODE := arm
这样的话,这个整个这个模块都会编译成ARM指令集的。
LOCAL_SRC_FILES := foo.c bar.c.arm
这将告诉编译系统,将“bar.c”编译成ARM指令集的。而对于“foo.c”来说,任然用LOCAL_ARM_MODE变量指定的方式(如果未指定则默认用Thumb指令集)编译。
LOCAL_ARM_NEON
LOCAL_SRC_FILES = foo.c.neon bar.c zoo.c.arm.neon
本例中,“foo.c”会被编译成Thumb指令集加上NEON指令集的形式(默认情况下,所有源代码会被编译成Thumb指令集的形式),“bar.c”会被编译成Thumb指令集的形式,而“zoo.c”会被编译成ARM指令集加上NEON指令集的形式。
LOCAL_DISABLE_NO_EXECUTE
LOCAL_DISABLE_RELRO
mprotect()
函数将GOT先改成可写的就可以了)。
LOCAL_EXPORT_CFLAGS
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo/foo.c
LOCAL_EXPORT_CFLAGS := -DFOO=1
include $(BUILD_STATIC_LIBRARY)
而同时还有另外一个模块,叫做“bar”,它依赖于这个“foo”模块,其定义如下:
include $(CLEAR_VARS)
LOCAL_MODULE := bar
LOCAL_SRC_FILES := bar.c
LOCAL_CFLAGS := -DBAR=2
LOCAL_STATIC_LIBRARIES := foo
include $(BUILD_SHARED_LIBRARY)
那么,对于“bar”来说,编译的时候传给编译器的选项就不光是“-DBAR=2”了。由于依赖的模块“foo”中定义了LOCAL_EXPORT_CFLAGS选项,所以编译选项要加上“-DFOO=1”。因此,最终传给编译器的选项是“-DFOO=1 -DBAR=2”。
另外,这个变量对编译包含这个变量的自己模块是没有作用的。例如,前例中,在编译“foo.c”时,并不会将参数“-DFOO=1”传给编译器。
LOCAL_EXPORT_CPPFLAGS
LOCAL_EXPORT_C_INCLUDES
LOCAL_EXPORT_LDFLAGS
LOCAL_EXPORT_LDLIBS
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo/foo.c
LOCAL_EXPORT_LDLIBS := -llog
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := bar
LOCAL_SRC_FILES := bar.c
LOCAL_STATIC_LIBRARIES := foo
include $(BUILD_SHARED_LIBRARY)
这时,在最后链接生成libbar.so时,传递给链接器的参数将包含-llog参数,并且这个参数是在所有其它参数的最后。也就是表示“bar”模块是依赖于系统所提供的日志动态库的,因为“bar”模块依赖于“foo”模块,而“foo”模块依赖于系统的日志模块。