前言
目前 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
特性可以参考:
- http://en.wikipedia.org/wiki/NX_bit
- http://www.gentoo.org/proj/en/hardened/gnu-stack.xml
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
(表示依赖系统的 日志库)构建进动态库中。
参考资料
后言
Android 原生开发随着 Flutter 的到来,前景开始变得不那么明朗。Android 原生可以实现一般功能,在 Flutter 同样也可以实现,同时 Flutter 一份代码可以实现跨平台运行。可能 Flutter 并不是最终跨平台的最终实现方案,但在一定程度上也给 Android 原生开发带来不小的冲击。
但随着目前 5G 时代的来临、以及人们普遍开始越来越重视应用安全,如应用隐私安全等。音视频领域、移动安全领域方兴未艾,尤其移动安全领域比较难找到相关的参考学习资料、学习路线,而笔者正深耘 Android 移动安全领域,希望在这学习 Android 移动安全的路上有你一起长风破浪。