前言
目前 Android 工程可以通过 .mk、或者 .cmake 的形式构建 NDK 工程,较新的 Android 工程一般采用的是 .cmake 构建 NDK 源码,而相对创建时间久的工程则大多数采用的是 .mk 的形式构建。下文主要是通过解析 Android 源码里的 docs 文档来深入了解 .mk 语法,其中会对原文里面一些描述通过实际工程加以描述。(原文以及演示的工程的链接将会在文章末尾给出)
MK 语法概述
一个 Android.mk 文件是用来描述 Android 的工程源码如何被构建系统所构建。进一步来说:
-
Android.mk 文件是一种会被构建系统解析一次或多次以上的
GUN Makefile片段。.mk语法允许构建工程的module源文件,每个module可以在Android.mk中被声明为下面的其中一种库:- 静态库(a static library)
- 动态库(a shared library)
只有动态库会被拷贝进项目工程,而静态库则是产生动态库的中间产物。在项目工程中,可以定义一个或多个
module在Android.mk文件中,或者可以使用同一份源码(.c/.cpp)在多个module内。 -
构建系统会自动地为
.mk工程处理细节问题。例如,我们不需要在Android.mk文件中列出源码的头文件,或者定义生成库的需要使用到的中间文件,NDK 构建工程会自动地为我们完成这些细节任务。同时新版本的 NDKtoolchain/platform支持向下兼容Android.mk语法。
NDK工程
对上图的解析说明:
-
src文件中包含着Java源文件、jni目录。jni目录下包含着.cpp/.mk; -
jni/Android.mk描述如何把hello_mk.cpp等文件构建成一个动态库,其内容如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello_mk
LOCAL_SRC_FILES := hello_mk.cpp
include $(BUILD_SHARED_LIBRARY)
在工程 app 下的 build.gradle 文件下对 Android.mk / Appplication.mk 文件位置的声明一般有两种方式:
方式一:
android {
...
task ndkBuild(type:Exec,description:'NDK Project'){
commandLine "C:\\Users\\iroot\\AppData\\Local\\Android\\Sdk\\ndk\\16.1.4479499\\ndk-build.cmd",//配置ndk的路径
'NDK_PROJECT_PATH=build/intermediates/ndk', // ndk默认的生成so的文件路径
'NDK_LIBS_OUT=src/main/jniLibs', // 配置的我们想要生成的so文件所在的位置
'APP_BUILD_SCRIPT=src/main/jni/Android.mk', // 指定项目的 Android.mk 所在位置
'NDK_APPLOCATION_MK=src/main/jni/Application.mk' // 指定项目的 Applicaiton.mk 所在位置
}
tasks.withType(JavaCompile){ //使用ndkBuild
compileTask ->compileTask.dependsOn ndkBuild
}
}
方式二:(方式二需要工程的 setting.gradle 文件配合声明 NDK 的所在位置)
android {
...
defaultConfig {
...
externalNativeBuild {
ndkBuild {
arguments 'NDK_APPLICATION_MK:=src/main/jni/Application.mk'
cFlags ''
cppFlags ''
abiFilters 'arm64-v8a, armeabi-v7a, x86, x86_64'
}
}
}
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
}
MK 语法详解(一)
LOCAL_PATH := $(call my-dir)
解析:每个 Android.mk 文件必须在文件头部最开始处定义 LOCAL_PATH 变量,该变量用来获取工程中的文件节点。在上述工程图中,通过构建系统提供的函数 my-dir 获取 Android.mk 当前的所在的目录节点。
include $(CLEAR_VARS)
解析:CLEAR_VARS 变量是构建系统提供,同时指向一个特殊的 GNU Makefile,主要是用来清除如 LOCAL_XXX 所定义的变量(e.g. LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES, etc...)、以及 LOCAL_PATH 环境中的异常(CLEAR_VARS 可简单理解为初始化环境)。include $(CLEAR_VARS) 声明是必须的,因为 Android.mk 在第一次被解析的时候,变量被初始化为为知的值(这里个人理解为类似 C 中的数据定义未初始化,被其值被系统赋值为垃圾值)。
LOCAL_MODULE := hello_mk
解析:LOCAL_MODULE 变量是用来声明需要被生成的 module 名称。该定义的名称在整个工程中必须是唯一的,同时在构建的时候,系统会自动为该 module 名称补全前缀、以及后缀。也就是说,上文定义的动态库名称 hello_mk 最后被补全为 libhello_mk.so。当然,如果我们使用 LOCAL_MODULE := libhello_mk 声明的时候,系统则不会为其添加前缀。
LOCAL_SRC_FILES := hello-jni.c
解析:变量 LOCAL_SRC_FILES 是用来定义将要生成的目标动态库所需要的源码文件列表,如 c 或者 c++ 文件。但我们不要把头文件或者被 include 的文件也定义到该变量列表中,因为这些构建系统已经自动地帮我们完成这些任务。
include $(BUILD_SHARED_LIBRARY)
解析:变量 BUILD_SHARED_LIBRARY 被定义后,GNU Makefile 脚本就会负责把 include $(BUILD_SHARED_LIBRARY) 往上定义的如 LOCAL_XXX 变量都收集起来,直到离 include $(BUILD_SHARED_LIBRARY) 最近的定义的 include $(CLEAR_VARS) 为止。可简单理解为 [include $(CLEAR_VARS) ... include $(BUILD_SHARED_LIBRARY)] 之间的 LOCAL_XXX 的变量将决定如何生成一个动态库。当然,BUILD_STATIC_LIBRARY 变量是用来声明定义生成静态库。
自定义变量
构建系统会提供一系列的 .mk 变量供我们使用,当然我们也可以在我们工程需要的时候自定义某些变量。但需要注意不能与构建系统保留的变量名发生冲突:
- 以
LOCAL_开头的变量名;(如LOCAL_MODULE) - 以
PRIVATE_、NDK_、APP_开头的变量名;(这些开头的变量名称被用以系统内部) lower-case名称;(用以系统内部,如my-dir)
如果我们需要自定义变量,官方推荐我们使用 MY_ 前缀开头的变量,避免与系统变量发生冲突。例子如下:
MY_SOURCES := foo.c
ifneq ($(MY_CONFIG_BAR),)
MY_SOURCES += bar.c
endif
LOCAL_SRC_FILES += $(MY_SOURCES)
MK 语法详解(二)
在上文我们了解了 CLEAR_VARS、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY 这几个变量。构建系统还提供了其他种类的变量供我们在 mk 文件中使用,下面我们来一一认识了解它们吧。
NDK提供的变量
PREBUILT_SHARED_LIBRARY
该变量用来指定一个需要被依赖进工程的动态库。与 BUILD_SHARED_LIBRARY 、BUILD_STATIC_LIBRARY 不同的是,该变量对应的 LOCAL_SRC_FILES 必须被初始化为一个需要被纳入到工程的动态库路径,而不是源码文件。NDK Prebuilt library support 参考资料
使用方式 (这里不做解析,感兴趣可以查看提供的 NDK Prebuilt library support 参考资料):
include $(CLEAR_VARS)
LOCAL_MODULE := foo-prebuilt
LOCAL_SRC_FILES := libfoo.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include #导出libfoo.so的头文件
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := foo-user
LOCAL_SRC_FILES := foo-user.c
LOCAL_SHARED_LIBRARY := foo-prebuilt
include $(BUILD_SHARED_LIBRARY)
PREBUILT_STATIC_LIBRARY
作用和 PREBUILT_SHARED_LIBRARY 类似,只是该初始化的为静态库路径。
TARGET_ARCH
指定程序运行的目标 CPU 架构指令集的名称;
TARGET_PLATFORM
指定 Android.mk 文件将被哪一个 Android 版本解析。例如,'android-3' 对应于 Android 1.5 系统镜像。
TARGET_ARCH_ABI
目标 CPU+ABI 被 Android.mk 解析。
部分举例:
armeabi => when targetting ARMv5TE or higher CPUs
armeabi-v7a => when targetting ARMv7 or higher CPUs
x86 => when targetting x86 CPUs
同时 armeabi-v7a 系统可以兼容 armeabi 二进制文件。
代码例子:
include $(CLEAR_VARS)
LOCAL_MODULE := foo-prebuilt
LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libfoo.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
同时假设上述代码目录结构如下:
Android.mk --> the file above
armeabi/libfoo.so --> the armeabi prebuilt shared library
armeabi-v7a/libfoo.so --> the armeabi-v7a prebuilt shared library
include/foo.h --> the exported header file
TARGET_ABI
与 TARGET_PLATFORM 、TARGET_ARCH_ABI 变量类似,变量 TARGET_ABI 在我们需要测试某一块真机的系统镜像的时候会非常有用。默认值为 android-3-armeabi。(升级 Android NDK 1.6_r1,将会默认使用 android-3-arm)
函数宏
接下来将要描述的是 GUN Make 函数宏(function macros),函数宏返回的是文本信息(可以理解为字符串),使用的格式如:$(call <function>)。
my-dir
在上文我们提到构建系统提供的函数 my-dir :返回最近导入的 Makefile 的路径,在 NDK 中一般为 Android.mk 当前的目录节点路径。这里我们再展开解析该函数在使用中需要注意的点。
$(call my-dir) 变量获取的是最新导入的 Makefile 路径,也就意味着,当 include 新的文件路径进来以后,$(call my-dir) 返回的是该新的文件路径。如下源码一:
源码一
LOCAL_PATH := $(call my-dir)
... declare one module
include $(LOCAL_PATH)/foo/Android.mk
LOCAL_PATH := $(call my-dir)
... declare another module
例子一存在二次调用 $(call my-dir) ,第二次调用返回的将是 $(LOCAL_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
但源码二给出形式还不是最好的,如当最后需要 include 多个 Android.mk 呢?最好的写法是把第一次获取到的 $(call 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
该变量返回当前包含 Android.mk 的 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) ,则将会自动地 include 文件 sources/foo/lib1/Android.mk 和 sources/foo/lib2/Android.mk 。默认地,NDK 只会遍历查找 sources/*/Android.mk 格式地目录下的 Android.mk。
this-makefile
返回当前的 Makefile 文件所在文件夹的路径。
parent-makefile
返回当前 Makefile 文件位于相对文件树节点的父 Makefile 路径。
grand-parent-makefile
...
import-module
该函数允许我们通过 module 的名字导入另一个 module,同时自动地导入该 module 的 Android.mk 文件。但该 module 需要在 NDK_MODULE_PATH 该变量中声明。代码如下例子:
$(call import-module,<name>)
Module描述变量
接下来解析的变量是用来描述我们的 module 将要如何被构建系统所构建。
LOCAL_PATH
该变量被赋值为当前文件的路径。我们必须把它定义在 Android.mk 文件中开头的地方。同时该变量是不会被 $(CLEAR_VARS) 函数所清除,所以我们需要为每个需要的 Android.mk 定义路径(如在单一个 Android.mk 文件中导入多个 module)。写法如下:
LOCAL_PATH := $(call my-dir)
LOCAL_MODULE
用来声明我们 module 的名称。声明的 module 的名称必须在该工程内是唯一的,该名称不可以包含任何的空格,同时需要声明在任何的 $(BUILD_XXXX) 变量前。该 module 的名称默认是生成文件的名称,如 module 的名称为 foo,则生成的静态库的文件名为 libfoo.a,或者生成静态库的文件名为 linfoo.so。
LOCAL_MODULE_FILENAME
这是个可选的变量,用来重载 LOCAL_MODULE 定义的名称。写法如下:
LOCAL_MODULE := foo-version-1
LOCAL_MODULE_FILENAME := libfoo
需要注意的是,我们不可以把路径名或者文件的后缀名称定义在 LOCAL_MODULE_FILENAME 。
LOCAL_SRC_FILES
该变量用来声明构建生成目标文件(静态 / 动态 / 可执行文件)所需要的源文件(C / C++),只有定义在该变量的源文件才会被编译进目标文件,同时构建系统会自动地为源码文件处理头文件导入这些细节操作。
因为源码文件的路径已经在声明 LOCAL_PATH 的时候已经导入进环境,因此我们只需要补充源码文件具体的目录位置即可。写法如下:
LOCAL_SRC_FILES := foo.c \
toto/bar.c
LOCAL_CPP_EXTENSION
该变量用来声明 C++ 的文件扩展后缀名称。我们可以更改默认的后缀(.cpp)名称声明。例子如下:
LOCAL_CPP_EXTENSION := .cxx
LOCAL_C_INCLUDES
相对于 NDK 的根目录路径,该变量定义编译源码时会被追加到导入搜索路径。如下例子:
LOCAL_C_INCLUDES := sources/foo
或者:
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../foo
LOCAL_C_INCLUDES 生效的时机是在 LOCAL_CFLAGS / LOCAL_CPPFLAGS 变量之前。同时,LOCAL_C_INCLUDES 路径在进行 native 层的调试的时候会被使用到。
LOCAL_CFLAGS
该变量声明的值会在构建 C 或 C++ 源码文件的时候给编译器设置编译参数,是一个可选的变量。这对于指定宏定义或者编译选项是非常有帮助的。
同时官方推荐不要尝试去调整优化选项、或者调试等级在我们的 Android.mk 文件,这些会通过 Application.mk 指定相关的信息为我们自动地处理,同时让 NDK 在调试的时候为我们生成有用的调试信息。
在 android-ndk-1.5_r1 的 NDK 版本,相对应的 LOCAL_CFLAGS 定义只针对 C 文件起效,而 C++ 则需要通过设置 LOCAL_CPPFLAGS 变量指定。
通过 LOCAL_CFLAGS 也可以像 LOCAL_C_INCLUDES 指定导入的源码文件路径,但推荐使用 LOCAL_C_INCLUDES 变量,因为后者会在 native 层调试的时候也需要使用到。
LOCAL_CXXFLAGS
LOCAL_CXXFLAGS 变量是 LOCAL_CPPFLAGS 的别名。但该变量可能在新的 NDK 版本中不再被支持。
LOCAL_CPPFLAGS
该变量会被拼接在 LOCAL_CFLAGS 变量之后,同时只对 C++ 源文件生效。
LOCAL_STATIC_LIBRARIES
LOCAL_STATIC_LIBRARIES 变量将被被链接进 module 里面的静态库列表,而这些静态库是通过 BUILD_STATIC_LIBRARY 定义构建的。
LOCAL_SHARED_LIBRARIES
作用与 LOCAL_STATIC_LIBRARIES 相同,只是定义的是动态库列表。
LOCAL_LDLIBS
LOCAL_LDLIBS 用于指定系统库通过 -l 前缀,指定的系统库将在编译的时候加载进我们目标 module。例如:
LOCAL_LDLIBS := -lz
通过指定 -lz 参数,编译器会加载 /system/lib/libz.so 进我们目标生成的 module。(参考 docs/STABLE-APIS.html)
LOCAL_ALLOW_UNDEFINED_SYMBOLS
在编译生成动态库的时候,构建系统回去检测源码中是否存在 undefined symbol 的错误,这一行为有利于帮助我们提前发现代码中的 Bug。但实际中,我们总会遇到某些原因让我们不得不关闭该检测行为,当 LOCAL_ALLOW_UNDEFINED_SYMBOLS := true 会关闭对 undefined symbol 的检查。但带来的风险是在加载动态库时会使得程序发生崩溃。
LOCAL_ARM_MODE
默认条件下,ARM 目标二进制文件将会在 thumb 模式下生成,同时在该模式下生成的每条指令都是 16 位的。我们可以通过该变量指定在 arm 模式下生成我们的目标文件,也就是说此时的目标文件的指令宽是 32 位的。写法如下:
LOCAL_ARM_MODE := arm
在生成指定的目标文件的过程中,我们还可以通过指定某个源码文件按照我们需要的方式构建,如:
LOCAL_SRC_FILES := foo.c bar.c.arm
通过在源码文件名称后面后添加 .arm 可以指定在编译的时候,该文件以 arm 模式构建。
同时,在 Application.mk 文件通过 APP_OPTIM := debug 定义的方式同样可以生成 ARM 目标二进制文件。在官方文档中指出这是因为由于工具链调试器在处理 thumb 指令有 bug。
LOCAL_ARM_NEON
当该变量被设为 true 的时允许我们使用 ARM Advanced SIMD (又名 NEON)、以及 GCC 内敛函数在我们的 C、C++ 代码中、同时允许 NEON 汇编出现在汇编文件中。
我们只允许指定 ARMv7 汇编指令集在对应架构 armeabi-v7a ABI 中。但并非所有的基于 ARMv7 架构的 CPU 都支持 NEON 扩展指令集,这需要我们执行运行检测才可以发现是否 NEON 指令可以安全运行。如 LOCAL_ARM_MODE 可以在运行时指定特定文件可以支持 arm 模式进行编译,LOCAL_ARM_NEON 指令同样也可以通过指定某特定的文件追加 .neon 后缀,来支持 NEON 扩展指令集。写法如下:
LOCAL_SRC_FILES = foo.c.neon \
bar.c \
zoo.c.arm.neon
在上面的例子中,foo.c 文件将以 thumb+neon 模式编译、bar.c 文件将以 thumb 模式编译、zoo.c 将以 arm + neon 模式编译。需要注意的是,如果某个文件同时需要以 arm、neon 模式编译,那么 .neon 后缀必须跟在 .arm 后面。
LOCAL_DISABLE_NO_EXECUTE
Android NDK r4 版本新增对 NX bit 的安全特性的支持。该特性被默认支持,我们可以通过该变量设置 LOCAL_DISABLE_NO_EXECUTE := true 来关闭该特性。
关于 NX 特性可以参考:
LOCAL_EXPORT_CFLAGS
LOCAL_EXPORT_CFLAGS 定义了一组 C / C++ 编译器参数,当其他模块以LOCAL_STATIC_LIBRARIES / LOCAL_SHARED_LIBRARIES 方式引用该模块时,就会将该组值加入到 LOCAL_CFLAGS,从而传递给编译器。如下例子:
代码片段一:
include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo/foo.c
LOCAL_EXPORT_CFLAGS := -DFOO=1
include $(BUILD_STATIC_LIBRARY)
代码片段二:
include $(CLEAR_VARS)
LOCAL_MODULE := bar
LOCAL_SRC_FILES := bar.c
LOCAL_CFLAGS := -DBAR=2
LOCAL_STATIC_LIBRARIES := foo
include $(BUILD_SHARED_LIBRARY)
在代码片段一种,我们通过 foo/foo.c 构建目标静态库 foo,同时 LOCAL_EXPORT_CFLAGS := -DFOO=1。在代码片段二中,我们通过 bar.c 构建动态库 bar,这时候 -DFOO=1 -DBAR=2 会传递给编译器以用来构建动态库 bar。
LOCAL_EXPORT_CFLAGS 定义的 flags 是可以被继承的。假设 zoo 依赖 bar,而 bar 依赖 foo,那么 zoo 也会继承来自 foo 中导出的 flags。在上述代码片段中,定义的 LOCAL_EXPORT_CFLAGS 对于构建本模块是不生效的,如上述例子中,构建 foo 时声明的 LOCAL_EXPORT_CFLAGS := -DFOO=1 不会传给编译器。
LOCAL_EXPORT_CPPFLAGS
作用和 LOCAL_EXPORT_CFLAGS 相同,但作用于 C++ 文件。
LOCAL_EXPORT_C_INCLUDES
作用和 LOCAL_EXPORT_CFLAGS 相同,但只是针对 C 导入的路径。当 bar.c 需要 foo 模块提供的头文件的时候,该定义会很有帮助。
LOCAL_EXPORT_LDLIBS
和 LOCAL_EXPORT_CFLAGS 相同,但针对的是链接器 flags。由于 Unix 的链接器工作的方式,导入的链接器 flags 将会被追加到我们 module 的 LOCAL_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)
在上述例子中,foo 是静态库,并且依赖系统库。同时 LOCAL_EXPORT_LDLIBS 定义用于导出依赖。当在编译器构建 bar 的时候,将会把 -llog (表示依赖系统的 日志库)构建进动态库中。