首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android.mk详解:构建Android项目的编译控制文件

Android.mk详解:构建Android项目的编译控制文件

作者头像
byte轻骑兵
发布2026-01-20 17:29:25
发布2026-01-20 17:29:25
1140
举报

Android.mk 文件是 Android 平台上的 Makefile 文件,用于定义 Android 应用或库中模块的编译方式。它是 Android NDK(Native Development Kit)的一部分,允许开发者使用 C 或 C++ 等原生语言来编写应用或库的某些部分。本文将简要介绍如何入门学习 Android.mk 文件的编写。

一、基本结构

Android.mk 文件是 Android NDK(Native Development Kit)构建系统中的一个核心组成部分,用于定义如何在 Android 项目中编译和链接原生代码(主要是 C/C++)。一个基本的 Android.mk 文件结构通常包含以下几个部分:

  1. 模块定义:通过 LOCAL_MODULE 变量定义模块的名称。
  2. 模块类型:通过 include 指令引入不同的模板文件来指定模块的类型(如可执行文件、静态库、共享库等)。通常,这是通过包含如 (CLEAR_VARS)、(BUILD_SHARED_LIBRARY) 或
  3. 源文件列表:通过 LOCAL_SRC_FILES 变量列出所有要编译的源文件(C/C++ 源文件、头文件等,但通常只列出源文件)。
  4. 包含的库:如果模块依赖于其他库(系统库或自定义库),则需要通过 LOCAL_LDLIBS(链接系统库)或 LOCAL_SHARED_LIBRARIES/LOCAL_STATIC_LIBRARIES(链接自定义的共享库/静态库)来指定这些依赖。
  5. 其他编译选项:可以通过设置不同的变量(如 LOCAL_CFLAGS 用于 C/C++ 编译器标志,LOCAL_LDFLAGS 用于链接器标志)来定制编译过程。

下面是一个简单的 Android.mk 文件示例,它定义了一个名为 hello-jni 的共享库模块:

代码语言:javascript
复制
LOCAL_PATH := $(call my-dir)  
  
include $(CLEAR_VARS)  
  
# 定义模块名称  
LOCAL_MODULE    := hello-jni  
  
# 定义模块类型为共享库  
include $(BUILD_SHARED_LIBRARY)  
  
# 如果有源文件,需要这样指定  
# LOCAL_SRC_FILES := hello-jni.c  
  
# 如果需要链接系统库,可以这样指定(例如,如果使用了日志功能)  
# LOCAL_LDLIBS    += -llog  
  
# 如果依赖其他自定义的库,可以这样指定  
# LOCAL_SHARED_LIBRARIES := libdependency

请注意,上面的示例中并没有显式指定源文件列表(LOCAL_SRC_FILES),因为这是一个非常简单的示例,只展示了如何定义和指定模块类型。在实际的项目中,需要根据项目的需求来设置这些变量。

此外,LOCAL_PATH := $(call my-dir) 这行代码非常重要,它用于获取当前 Android.mk 文件所在的目录路径,并存储在 LOCAL_PATH 变量中,以便在后续的 include 指令中使用相对路径来引用其他文件或 Android.mk 文件。

二、指定模块类型

在 Android 的 NDK 构建系统中,Android.mk 文件通过包含不同的 GNU Makefile 片段(即预定义的变量和规则)来指定模块的编译类型。这些片段定义了如何将源文件编译成不同的输出类型,如共享库、静态库或可执行文件。

2.1. 常见的模块类型及其指定方式

  • 共享库(Shared Library)当想要将代码编译成一个可以在多个应用程序之间共享的库时,应该使用 BUILD_SHARED_LIBRARY。这会在你的 Android.mk 文件中创建一个 .so(Shared Object)文件。
代码语言:javascript
复制
include $(CLEAR_VARS)  
LOCAL_MODULE    := my_shared_library  
LOCAL_SRC_FILES := my_source_file.c  
include $(BUILD_SHARED_LIBRARY)
  • 静态库(Static Library):静态库与共享库不同,它会被直接链接到应用程序中,而不是在运行时被加载。静态库通常用于封装不希望暴露给最终用户的代码,或者当需要控制库如何与应用程序一起被打包时。
代码语言:javascript
复制
include $(CLEAR_VARS)  
LOCAL_MODULE    := my_static_library  
LOCAL_SRC_FILES := my_source_file.c  
include $(BUILD_STATIC_LIBRARY)

注意:静态库通常不会单独构建为目标,而是作为其他模块(如共享库或可执行文件)的依赖项来构建。

  • 可执行文件(Executable):如果想要编译一个可以在 Android 设备上直接运行的可执行文件(尽管这通常不是 Android 应用开发的标准做法),可以使用 BUILD_EXECUTABLE。然而,在 Android 应用开发中,更常见的是创建 Java/Kotlin 应用程序,并使用 JNI(Java Native Interface)来调用本地(C/C++)代码。
代码语言:javascript
复制
include $(CLEAR_VARS)  
LOCAL_MODULE    := my_executable  
LOCAL_SRC_FILES := my_main_function.c  
include $(BUILD_EXECUTABLE)

但是,请注意,直接在 Android 设备上运行可执行文件可能会受到安全限制和沙箱机制的约束。

2.2. 注意事项

  • 在每个模块定义之前,使用 include $(CLEAR_VARS) 来清除之前设置的所有局部变量(如 LOCAL_MODULELOCAL_SRC_FILES 等),这是必要的,因为这些变量是全局的,并且会影响后续的模块定义。
  • 如果模块依赖于其他模块(无论是静态库还是共享库),需要在 LOCAL_SHARED_LIBRARIESLOCAL_STATIC_LIBRARIES 中指定这些依赖项。
  • 当使用 ndk-build 命令来构建项目时,它会查找所有 Android.mk 文件,并根据这些文件中定义的规则来编译和链接你的模块。

三、变量赋值

Android.mk 文件中,变量用于存储配置信息,如源文件列表、模块名称、编译选项等。变量赋值使用 := 操作符,例如:

代码语言:javascript
复制
LOCAL_MODULE    := hello-jni  
LOCAL_SRC_FILES := hello-jni.c

这里,LOCAL_MODULE 被赋值为 hello-jniLOCAL_SRC_FILES 被赋值为 hello-jni.c

注意:使用 := 进行赋值时,如果右侧的值中引用了其他变量,这些变量会被立即展开。如果想要延迟展开(即,在变量实际被使用时才展开),可以使用 = 赋值符,但这在 Android.mk 文件中不太常见。

四、清除变量

在 Android NDK 的 Android.mk 文件中,include $(CLEAR_VARS) 是一个非常重要的步骤,用于在定义新的模块之前清除之前设置的所有局部变量。这是必要的,因为 Android.mk 文件中定义的变量(如 LOCAL_MODULELOCAL_SRC_FILES 等)是全局的,并且在同一个 Makefile 解析过程中持续存在。

如果没有清除这些变量,那么后续定义的模块可能会意外地继承之前模块的设置,导致构建错误或生成不正确的输出。

CLEAR_VARS 是一个由 NDK 提供的特殊变量,它指向一个 GNU Makefile 片段,该片段定义了一系列空值来重置之前可能设置的任何局部变量。因此,通过在定义新模块之前包含 $(CLEAR_VARS),可以确保每个模块都从一个干净的状态开始。

下面是一个典型的 Android.mk 文件片段,展示了如何使用 include $(CLEAR_VARS) 来定义多个模块:

代码语言:javascript
复制
LOCAL_PATH := $(call my-dir)  
  
# 清除之前的变量  
include $(CLEAR_VARS)  
  
# 定义第一个模块  
LOCAL_MODULE    := first_module  
LOCAL_SRC_FILES := first_module.c  
include $(BUILD_SHARED_LIBRARY)  
  
# 再次清除变量,为下一个模块做准备  
include $(CLEAR_VARS)  
  
# 定义第二个模块  
LOCAL_MODULE    := second_module  
LOCAL_SRC_FILES := second_module.c  
include $(BUILD_STATIC_LIBRARY)  
  
# ... 可以继续定义更多的模块

在这个例子中,first_modulesecond_module 是两个完全独立的模块,它们分别被编译为共享库和静态库。通过在每个模块定义之前使用 include $(CLEAR_VARS),我们确保了它们的配置不会相互干扰。

五、引入其他库

在 Android NDK 的 Android.mk 文件中,如果模块需要链接其他库(无论是静态库、共享库还是系统库),可以通过不同的方式来指定这些依赖。

5.1. 链接静态库或共享库

如果模块需要链接其他自定义的静态库或共享库,应该使用 LOCAL_STATIC_LIBRARIESLOCAL_SHARED_LIBRARIES 变量来指定这些库的名称。这些变量告诉构建系统在链接你的模块时需要包含哪些库。

  • LOCAL_STATIC_LIBRARIES:指定要链接的静态库(.a 文件)。
  • LOCAL_SHARED_LIBRARIES:指定要链接的共享库(.so 文件)。

例如:

代码语言:javascript
复制
include $(CLEAR_VARS)  
  
LOCAL_MODULE    := my_module  
LOCAL_SRC_FILES := my_source.c  
LOCAL_STATIC_LIBRARIES := libstatic  # 链接名为 libstatic 的静态库  
LOCAL_SHARED_LIBRARIES := libshared  # 链接名为 libshared 的共享库  
  
include $(BUILD_SHARED_LIBRARY)  
  
# 假设已经有 libstatic.a 和 libshared.so 的定义在其他 Android.mk 文件中  
# 或者它们是由你的项目中的其他模块生成的

注意:在链接自定义库时,确保这些库已经在项目中被正确定义,并且它们的构建顺序要早于模块,或者它们已经以某种方式被包含在构建路径中。

5.2. 链接系统库

如果需要链接 Android 系统提供的库(如 libc、libm、liblog 等),应该使用 LOCAL_LDLIBS 变量来指定这些库的名称。但是,请注意,对于大多数系统库,通常不需要直接指定它们,因为 NDK 的工具链已经默认链接了这些库。然而,对于某些不是默认链接的库(如 liblog),可以这样做:

代码语言:javascript
复制
include $(CLEAR_VARS)  
  
LOCAL_MODULE    := my_module  
LOCAL_SRC_FILES := my_source.c  
LOCAL_LDLIBS    += -llog  # 链接 Android 的日志库  
  
include $(BUILD_SHARED_LIBRARY)

在这个例子中,-llog 告诉链接器要链接 Android 的日志库(liblog.so)。注意,LOCAL_LDLIBS 中的条目前面通常会有 -l 前缀(表示链接库),并且如果库名不是标准的(即不以 lib 开头),则可能需要完整指定库名(但在这个例子中,log 是标准的 Android 系统库名)。

5.3. 注意事项

  • 确保链接的库在构建时是可用的,并且它们的路径被正确地包含在构建环境中。
  • 对于自定义库,可能需要在 Android.mk 文件中使用 include 语句来包含定义这些库的其他 Android.mk 文件,或者确保这些库是通过其他方式(如预构建库)被包含在你的项目中的。
  • 对于系统库,通常不需要手动指定它们的路径,因为 NDK 的工具链已经配置了这些库的路径。但是,如果需要链接非标准路径下的系统库或第三方库,可能需要使用 LOCAL_LDFLAGS 变量来指定额外的库搜索路径(使用 -L 选项)。

六、定义条件编译

在 Android NDK 的 Android.mk 文件中,实际上并不直接支持 GNU Make 的条件语句(如 ifeqifneqelseendif)来进行条件编译,因为 Android.mk 文件主要是被 Android 的构建系统(基于 soong 和 kati)用来解析和转换成 Ninja 构建文件的。然而,可以通过一些间接的方法来实现类似条件编译的效果。

6.1. 使用环境变量

一种常见的做法是使用环境变量来控制编译过程。可以在构建时设置环境变量,然后在 Android.mk 文件中检查这些变量来决定是否包含某些源文件或编译选项。虽然这不是直接在 Android.mk 文件中使用条件语句,但它可以达到类似的效果。

例如,可以在命令行中设置环境变量:

代码语言:javascript
复制
export MY_FEATURE_ENABLED=1  
ndk-build

然后在 Android.mk 文件中检查这个环境变量:

代码语言:javascript
复制
include $(CLEAR_VARS)  
  
LOCAL_MODULE := my_module  
  
# 检查环境变量  
ifneq ($(MY_FEATURE_ENABLED),)  
    LOCAL_SRC_FILES += feature_enabled.c  
endif  
  
LOCAL_SRC_FILES += main.c  
  
include $(BUILD_SHARED_LIBRARY)

6.2. 使用自定义变量和包含文件

另一种方法是使用自定义变量和包含额外的 .mk 文件。可以根据条件包含不同的 .mk 文件,这些文件定义了不同的源文件列表或编译选项。

例如,可以有两个 .mk 文件:config_feature_enabled.mkconfig_feature_disabled.mk,然后在 Android.mk 中根据条件包含其中一个:

代码语言:javascript
复制
include $(CLEAR_VARS)  
  
LOCAL_MODULE := my_module  
  
# 假设有一个变量或环境变量来控制功能是否启用  
ifeq ($(ENABLE_MY_FEATURE),1)  
    include $(LOCAL_PATH)/config_feature_enabled.mk  
else  
    include $(LOCAL_PATH)/config_feature_disabled.mk  
endif  
  
LOCAL_SRC_FILES += main.c  
# config_*.mk 文件将可能添加额外的源文件  
  
include $(BUILD_SHARED_LIBRARY)

6.3. 注意

  • 由于 Android.mk 文件的这些限制,如果需要更复杂的条件逻辑,可能需要考虑使用 Android 的其他构建机制,如 CMake 或直接在构建脚本中处理这些逻辑。
  • 需要注意的是,直接在 Android.mk 文件中使用 GNU Make 的条件语句(如 ifeq)通常不会按预期工作,因为 Android.mk 文件不是直接由 GNU Make 解析的。相反,它们被 Android 的构建系统解析并转换为 Ninja 构建文件或其他内部表示。

七、 编译选项

可以设置多种编译选项来定制模块构建过程。这些选项包括 C/C++ 的包含路径、编译器标志等

7.1. LOCAL_C_INCLUDES

LOCAL_C_INCLUDES 变量用于指定额外的 C/C++ 包含(头文件)路径。这些路径会被添加到 C/C++ 编译器的包含搜索路径中,使得编译器能够找到你的项目中使用的头文件。

代码语言:javascript
复制
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include  
LOCAL_C_INCLUDES += /path/to/another/include

在这个例子中,(LOCAL_PATH)/include 和 /path/to/another/include 将会被添加到包含路径中。(LOCAL_PATH) 是一个自动变量,它代表了当前 Android.mk 文件所在的目录的路径。

7.2. LOCAL_CFLAGS

LOCAL_CFLAGS 变量用于指定 C 编译器的标志。这些标志会在编译 C 源文件时传递给 C 编译器。可以使用这些标志来启用或禁用特定的编译器功能,如优化选项、警告级别等。

代码语言:javascript
复制
OCAL_CFLAGS += -Wall -Wextra -O2

在这个例子中,-Wall-Wextra 将会开启额外的警告信息,而 -O2 将会启用二级优化。

7.3. LOCAL_CPPFLAGS

LOCAL_CPPFLAGS 变量与 LOCAL_CFLAGS 类似,但它专门用于指定 C++ 编译器的标志。这些标志会在编译 C++ 源文件时传递给 C++ 编译器。如果需要为 C++ 源文件设置特定的编译器标志(而 C 源文件不需要这些标志),则应该使用 LOCAL_CPPFLAGS 而不是 LOCAL_CFLAGS

代码语言:javascript
复制
LOCAL_CPPFLAGS += -std=c++11 -Wall -Wextra -O2

在这个例子中,-std=c++11 指定了使用 C++11 标准,-Wall-Wextra-O2 的作用与前面相同。

7.4. 注意事项
  • 如果既需要为 C 文件又需要为 C++ 文件设置相同的编译标志,可以将它们同时放在 LOCAL_CFLAGSLOCAL_CPPFLAGS 中,但通常将它们放在 LOCAL_CFLAGS 中就足够了,因为 C++ 编译器通常也接受 C 编译器的标志。然而,对于特定于 C++ 的标志(如 -std=c++11),你应该将它们放在 LOCAL_CPPFLAGS 中。
  • 还可以设置 LOCAL_LDFLAGS 变量来指定链接器标志,这些标志会在链接阶段传递给链接器。
  • 记住,这些变量都是局部的,它们只影响当前正在构建的模块。如果需要在多个模块之间共享编译选项,可能需要将这些选项提取到一个单独的 .mk 文件中,并在需要时包含它。

八、链接选项

LOCAL_LDLIBS 变量用于指定链接器(linker)在链接模块时需要额外考虑的库。这些库可以是系统库,也可以是项目中其他模块生成的库(尽管对于后者,通常更推荐使用 LOCAL_SHARED_LIBRARIESLOCAL_STATIC_LIBRARIES)。

当需要链接系统库时,LOCAL_LDLIBS 变得非常有用。例如,如果想要链接 Android 的日志库(liblog),可以在 Android.mk 文件中这样设置:

代码语言:javascript
复制
LOCAL_LDLIBS += -llog

这里的 -llog 告诉链接器在链接模块时,需要链接到名为 liblog 的库。注意,在链接系统库时,通常不需要指定库文件的前缀(如 lib)和后缀(如 .so),只需要指定库的基本名称即可。

然而,需要注意的是,对于大多数 Android NDK 提供的系统库,通常不需要通过 LOCAL_LDLIBS 来手动链接它们,因为 NDK 的构建系统已经处理了这些依赖。LOCAL_LDLIBS 更多地用于链接那些不是由 NDK 直接管理的库,或者当有特殊的链接需求时。

另外,如果需要链接自己项目中其他模块生成的库(无论是静态库还是共享库),应该使用 LOCAL_STATIC_LIBRARIESLOCAL_SHARED_LIBRARIES 而不是 LOCAL_LDLIBS。这是因为 LOCAL_STATIC_LIBRARIESLOCAL_SHARED_LIBRARIES 提供了更明确的依赖关系管理,它们允许 NDK 构建系统正确地处理库之间的依赖关系,并确保在链接时库的顺序是正确的。

九、导出变量

在某些情况下,可能需要在一个 Android.mk 文件中定义的变量在另一个 Android.mk 文件中也可见(例如在包含的子目录中)。这可以通过使用 include (BUILD_XXX) 之前导出变量来实现,但更常见的是使用 include (CLEAR_VARS) 后立即设置 include

十、编译多个源文件

如果模块包含多个源文件,可以在 LOCAL_SRC_FILES 中一次性列出所有文件。

代码语言:javascript
复制
LOCAL_SRC_FILES := main.c \  
                   helper.c \  
                   another_helper.c

十一、调试

使用 NDK 编译的应用或库可以在 Android 设备或模拟器上调试。可以通过 ndk-build 命令配合 gdbserver 和 gdb 客户端进行调试。

十二、 示例

这是一个包含多个元素的 Android.mk 示例:

代码语言:javascript
复制
LOCAL_PATH := $(call my-dir)  
  
include $(CLEAR_VARS)  
  
LOCAL_MODULE    := hello-jni  
LOCAL_SRC_FILES := hello-jni.c  
LOCAL_SHARED_LIBRARIES := libfoo  
LOCAL_LDLIBS += -llog  
  
include $(BUILD_SHARED_LIBRARY)

在这个示例中,我们定义了一个名为 hello-jni 的共享库,它依赖于名为 libfoo 的另一个共享库,并且链接了 Android 的日志库。

十三、注意事项

  • 确保 LOCAL_PATH 正确设置,以指向包含 Android.mk 文件的目录。
  • 使用 include $(CLEAR_VARS) 在定义新模块之前清除变量。
  • 仔细选择模块类型和链接的库类型。
  • 查阅 NDK 文档以获取有关特定变量和选项的更多信息。

十四、资源和文档

  • 官方文档:Google 提供的 NDK 文档是学习和理解 Android.mk 的最佳资源。
  • 示例代码:查看 NDK 示例或开源项目中的 Android.mk 文件可以加深理解。
  • 社区和论坛:参与 Android NDK 相关的社区和论坛,获取帮助和分享经验。

通过以上介绍,你应该对 Android.mk 文件的基本结构和用法有了一定的了解。建议通过实践来加深理解,并查阅官方文档以获取更多高级特性和最佳实践。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-10-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、基本结构
  • 二、指定模块类型
    • 2.1. 常见的模块类型及其指定方式
    • 2.2. 注意事项
  • 三、变量赋值
  • 四、清除变量
  • 五、引入其他库
    • 5.1. 链接静态库或共享库
    • 5.2. 链接系统库
    • 5.3. 注意事项
  • 六、定义条件编译
    • 6.1. 使用环境变量
    • 6.2. 使用自定义变量和包含文件
    • 6.3. 注意
  • 七、 编译选项
    • 7.1. LOCAL_C_INCLUDES
    • 7.2. LOCAL_CFLAGS
    • 7.3. LOCAL_CPPFLAGS
    • 7.4. 注意事项
  • 八、链接选项
  • 九、导出变量
  • 十、编译多个源文件
  • 十一、调试
  • 十二、 示例
  • 十三、注意事项
  • 十四、资源和文档
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档