使用 Boost 编译可执行文件及掌握 Makefile
1. 使用 Boost 单元测试库构建可执行文件
要使用 Boost 单元测试库构建自己的单元测试可执行文件,可按以下步骤操作:
1. 创建 Android.mk 文件 :仍在 boost 目录下,创建一个新的 Android.mk 文件,将新的预构建库声明为 Android 模块,使其可用于 NDK 应用程序。每个库需要一个模块声明。例如,定义一个模块 boost_unit_test_framework :
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= boost_unit_test_framework
LOCAL_SRC_FILES:= android-$(TARGET_ARCH_ABI)/lib/libboost_unit_test_framework.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
include $(PREBUILT_STATIC_LIBRARY)
可以在同一文件中使用相同的行声明更多模块(例如 boost_thread )。
2. 创建测试文件 :回到 DroidBlaster 项目,创建一个新目录 test ,其中包含单元测试文件 test/Test.cpp 。编写一个测试来检查 TimeManager 的行为,示例代码如下:
#include "Log.hpp"
#include "TimeManager.hpp"
#include <unistd.h>
#define BOOST_TEST_MODULE DroidBlaster_test_module
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_SUITE(suiteTimeManager)
BOOST_AUTO_TEST_CASE(testTimeManagerTest_elapsed)
{
TimeManager timeManager;
timeManager.reset();
sleep(1);
timeManager.update();
BOOST_REQUIRE(timeManager.elapsed() > 0.9f);
BOOST_REQUIRE(timeManager.elapsed() < 1.2f);
sleep(1);
timeManager.update();
BOOST_REQUIRE(timeManager.elapsed() > 0.9f);
BOOST_REQUIRE(timeManager.elapsed() < 1.2f);
}
BOOST_AUTO_TEST_SUITE_END()
- 启用异常和 RTTI :要在应用程序中包含 Boost,需要将其与支持异常和 RTTI 的 STL 实现链接。在
Application.mk文件中全局启用它们,代码如下:
APP_ABI := armeabi armeabi-v7a x86
APP_STL := gnustl_static
APP_CPPFLAGS := -fexceptions –frtti
- 创建新模块 :打开 DroidBlaster
jni/Android.mk,在import-module部分之前创建一个名为DroidBlaster_test的第二个模块。此模块编译额外的test/Test.cpp测试文件,并必须链接到 Boost 单元测试库。使用$(BUILD_EXECUTABLE)将此模块构建为可执行文件,而不是共享库。
...
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LS_CPP=$(subst $(1)/,,$(wildcard $(1)/*.cpp))
LS_CPP_TEST=$(subst $(1)/,,$(wildcard $(1)/../test/*.cpp))
LOCAL_MODULE := DroidBlaster_test
LOCAL_SRC_FILES := $(call LS_CPP,$(LOCAL_PATH)) \
$(call LS_CPP_TEST,$(LOCAL_PATH))
LOCAL_LDLIBS := -landroid -llog -lEGL -lGLESv2 -lOpenSLES
LOCAL_STATIC_LIBRARIES := android_native_app_glue png box2d_static \
libboost_unit_test_framework
include $(BUILD_EXECUTABLE)
$(call import-module,android/native_app_glue)
$(call import-module,libpng)
$(call import-module,box2d)
$(call import-module,boost)
- 构建并运行项目 :构建项目后,查看
libs文件夹,除了共享库外,应该会看到一个droidblaster_test文件。这是一个可执行文件,可以在模拟器或 rooted 设备上运行(前提是你有权限部署和更改文件权限)。部署并运行此文件(在 Arm V7 模拟器实例上):
adb push libs/armeabi-v7a/droidblaster_test /data/data/
adb shell /data/data/droidblaster_test
2. 构建原生库的方法
构建原生库主要有四种方法:
| 方法 | 描述 |
| — | — |
| BUILD_STATIC_LIBRARY | 构建静态库 |
| BUILD_SHARED_LIBRARY | 构建共享库 |
| PREBUILT_STATIC_LIBRARY | 使用现有的(即预构建的)二进制静态库 |
| PREBUILT_SHARED_LIBRARY | 使用现有的二进制共享库 |
这些指令表明库已准备好进行链接。在主模块文件中,链接的子模块需要在以下位置列出:
- LOCAL_SHARED_LIBRARIES :用于共享库
- LOCAL_STATIC_LIBRARIES :用于静态库
无论库是否为预构建,规则都是相同的。模块(无论是静态、共享、预构建还是按需构建)都必须使用 NDK import-module 指令导入到最终的主模块中。
3. 异常和 RTTI 的使用
为了与 Boost 正确链接,需要在整个项目中启用异常和 RTTI。可以通过在 Application.mk 文件的 APP_CPPFLAGS 指令或相关库的 LOCAL_CPPFLAGS 文件中添加 -fexceptions 和 -frtti 来轻松激活它们。默认情况下,Android 使用 -fno-exceptions 和 -fno-rtti 标志进行编译。
异常可能会使编译后的代码变大且效率降低,因为它们会阻止编译器进行一些巧妙的优化。然而,与手动错误检查和处理异常情况相比,这种代价在大多数情况下可能并不显著。C++ 中的异常处理并不容易,必须严格用于异常情况,并需要精心设计的代码。可以参考资源获取即初始化(RAII)惯用法来正确处理它们,更多信息可查看 RAII 介绍 。
4. Makefile 变量
编译设置通过一组预定义的 NDK 变量来定义。主要有四种类型的变量,每种变量有不同的前缀:
| 变量类型 | 描述 |
| — | — |
| LOCAL_ 变量 | 用于单个模块的编译,在 Android.mk 文件中定义 |
| APP_ 变量 | 指应用程序范围的选项,在 Application.mk 中设置 |
| NDK_ 变量 | 主要是内部变量,通常指环境变量(例如 NDK_ROOT 、 NDK_APP_CFLAGS 或 NDK_APP_CPPFLAGS )。有两个例外: NDK_TOOLCHAIN_VERSION 和 NDK_APPLICATION_MK |
| PRIVATE_ 前缀变量 | 仅用于 NDK 内部使用 |
以下是部分 LOCAL 变量的非详尽列表:
| 变量 | 描述 |
| — | — |
| LOCAL_PATH | 指定源文件的根位置,必须在 Android.mk 文件开头 include $(CLEAR_VARS) 之前定义 |
| LOCAL_MODULE | 定义模块名称,在所有模块中必须唯一 |
| LOCAL_MODULE_FILENAME | 覆盖编译模块的默认名称,共享库为 lib<模块名称>.so ,静态库为 lib<模块名称>.a |
| LOCAL_SRC_FILES | 定义要编译的源文件列表,每个文件用空格分隔,相对于 LOCAL_PATH |
| LOCAL_C_INCLUDES | 指定 C 和 C++ 语言的头文件目录 |
| LOCAL_CPP_EXTENSION | 更改默认的 C++ 文件扩展名,例如 .cc 或 .cxx |
| LOCAL_CFLAGS, LOCAL_CPPFLAGS, LOCAL_LDLIBS | 指定编译和链接的选项、标志或宏定义 |
| LOCAL_SHARED_LIBRARIES, LOCAL_STATIC_LIBRARIES | 分别声明与其他模块(非系统库)的依赖关系,共享和静态模块 |
| LOCAL_ARM_MODE, LOCAL_ARM_NEON, LOCAL_DISABLE_NO_EXECUTE, LOCAL_FILTER_ASM | 处理处理器和汇编/二进制代码生成的高级变量,大多数程序不需要 |
| LOCAL_EXPORT_C_INCLUDES, LOCAL_EXPORT_CFLAGS, LOCAL_EXPORT_CPPFLAGS, LOCAL_EXPORT_LDLIBS | 定义导入模块中应附加到客户端模块选项的额外选项或标志 |
以下是部分 APP 变量的非详尽列表(所有变量都是可选的):
| 变量 | 描述 |
| — | — |
| APP_PROJECT_PATH | 指定应用程序项目的根目录 |
| APP_MODULES | 要编译的模块列表及其标识符,依赖模块也会包含在内 |
| APP_OPTIM | 设置为 release 或 debug 以适应不同的编译设置 |
| APP_CFLAGS, APP_CPPFLAGS, APP_LDFLAGS | 全局指定编译和链接的选项、标志或宏定义 |
| APP_BUILD_SCRIPT | 重新定义 Android.mk 文件的位置(默认在项目的 jni 目录中) |
| APP_ABI | 应用程序支持的 ABI(即“CPU 架构”)列表,用空格分隔 |
| APP_PLATFORM | 目标 Android 平台的名称,默认在 project.properties 文件中找到 |
| APP_STL | 要使用的 C++ 运行时 |
5. 启用 C++ 11 支持和 Clang 编译器
可以在 Application.mk 文件中重新定义 NDK_TOOLCHAIN_VERSION 变量来显式选择编译工具链。对于 NDK R10,可能的值为 4.6(现已弃用)、4.8 和 4.9,分别对应 GCC 版本。要启用 C++ 11 支持,需要使用 GCC 4.8 工具链,并在 APP_CPPFLAGS 中添加 -std=c++11 标志,同时激活 GNU STL。示例代码如下:
...
NDK_TOOLCHAIN_VERSION := 4.8
APP_CPPFLAGS += -std=c++11
APP_STL := gnustl_shared
...
切换到 GCC4.8 和 C++11 可能会遇到一些问题,编译器可能会比以前更严格。如果在使用新工具链编译旧代码时遇到问题,可以尝试使用 -fpermissive 标志(或重写代码)。
要启用 Clang(基于 LLVM 的编译器),只需将 NDK_TOOLCHAIN_VERSION 设置为 clang ,也可以指定编译器版本,如 clang3.4 或 clang3.5 。可能的版本号在 NDK 的未来版本中可能会改变,可以查看 $ANDROID_NDK/toolchains 目录来查找。
6. Makefile 指令
Makefile 是一种具有编程指令和函数的真实语言。可以使用 include 指令将 Makefile 分解为多个子 Makefile。变量初始化有两种方式:
- 简单赋值( := 运算符) :在变量初始化时展开变量
- 递归赋值( = 运算符) :每次调用时重新计算赋值表达式
可用的条件和循环指令包括 ifdef/endif 、 ifeq/endif 、 ifndef/endif 和 for…in/do/done 。例如,仅当变量定义时显示消息:
ifdef my_var
# Do something...
endif
Makefile 还提供了一些有用的内置函数,如下表所示:
| 函数 | 描述 |
| — | — |
| $(info <message>) | 允许将消息打印到标准输出 |
| $(warning <message>), $(error <message>) | 允许打印警告或致命错误以停止编译 |
| $(foreach <variable>, <list>, <operation>) | 对变量列表执行操作 |
| $(shell <command>) | 执行 Make 外部的命令 |
| $(wildcard <pattern>) | 根据模式选择文件和目录名称 |
| $(call <function>) | 允许评估函数或宏 |
字符串和文件操作函数如下表所示:
| 函数 | 描述 |
| — | — |
| $(join <str1>, <str2>) | 连接两个字符串 |
| $(subst <from>, <replacement>,<string>), $(patsubst <pattern>, <replacement>,<string>) | 替换字符串中每个子字符串的出现 |
| $(filter <patterns>, <text>), $(filter-out <patterns>, <text>) | 根据模式过滤字符串 |
| $(strip <string>) | 去除不必要的空格 |
| $(addprefix <prefix>,<list>), $(addsuffix <suffix>, <list>) | 分别为列表中的每个元素添加前缀和后缀 |
| $(basename <path1>, <path2>, ...) | 返回去除文件扩展名的字符串 |
| $(dir <path1>, <path2>), $(notdir <path1>, <path2>) | 分别提取路径中的目录和文件名 |
| $(realpath <path1>, <path2>, ...), $(abspath <path1>, <path2>, ...) | 返回每个路径参数的规范路径 |
7. 实践 Makefile
可以通过以下方式实践 Makefile:
1. 尝试赋值运算符 :在 Android.mk 文件中使用 := 运算符,示例代码如下:
my_value := Android
my_message := I am an $(my_value)
$(info $(my_message))
my_value := Android eating an apple
$(info $(my_message))
观察编译时的结果,然后使用 = 进行相同的操作。
2. 打印当前优化模式 :使用 APP_OPTIM 和内部变量 NDK_APP_CFLAGS ,观察发布和调试模式之间的差异:
$(info Optimization level: $(APP_OPTIM) $(NDK_APP_CFLAGS))
- 检查变量是否正确定义 :
ifndef LOCAL_PATH
$(error What a terrible failure! LOCAL_PATH not defined...)
endif
- 使用
foreach指令打印文件和目录列表 :
ls = $(wildcard $(var_dir))
dir_list := . ./jni
files := $(foreach var_dir, $(dir_list), $(ls))
- 创建宏来记录消息和时间 :
log=$(info $(shell date +'%D %R'): $(1))
$(call log,My message)
- 测试
my-dir宏的行为 :
$(info MY_DIR =$(call my-dir))
include $(CLEAR_VARS)
$(info MY_DIR =$(call my-dir))
通过以上步骤和方法,可以更好地使用 Boost 编译可执行文件,并掌握 Android Makefile 的使用。
使用 Boost 编译可执行文件及掌握 Makefile
8. 流程总结
下面通过一个 mermaid 流程图来总结使用 Boost 构建可执行文件的整体流程:
graph TD
A[在 boost 目录创建 Android.mk 文件] --> B[在 DroidBlaster 项目创建 test 目录及测试文件]
B --> C[在 Application.mk 中启用异常和 RTTI]
C --> D[在 DroidBlaster jni/Android.mk 创建 DroidBlaster_test 模块]
D --> E[构建项目]
E --> F[部署并运行可执行文件]
9. 不同构建方法的选择
在实际开发中,选择合适的构建方法对于项目的性能和可维护性至关重要。下面是对几种构建原生库方法的选择建议:
- 静态库(BUILD_STATIC_LIBRARY) :
- 优点 :代码被完整地包含在最终可执行文件中,不依赖外部库文件,移植性好,运行时加载速度快。
- 缺点 :会增加可执行文件的大小,多个程序使用相同静态库时会造成磁盘空间的浪费。
- 适用场景 :对可移植性要求较高,且不介意文件大小的项目。
- 共享库(BUILD_SHARED_LIBRARY) :
- 优点 :多个程序可以共享同一个库文件,节省磁盘空间,更新库文件时不需要重新编译所有依赖该库的程序。
- 缺点 :运行时需要动态加载库文件,可能会影响启动速度,并且需要确保库文件在系统中正确安装。
- 适用场景 :多个项目共享相同功能库,且对磁盘空间和更新灵活性有要求的项目。
- 预构建静态库(PREBUILT_STATIC_LIBRARY) :
- 优点 :可以直接使用已经编译好的静态库,无需重新编译,节省时间。适合提供库给第三方而不公开源代码的情况。
- 缺点 :无法对库的编译过程进行调整,如果库的版本不兼容可能会出现问题。
- 适用场景 :使用第三方提供的静态库,或者需要快速集成现有库的项目。
- 预构建共享库(PREBUILT_SHARED_LIBRARY) :
- 优点 :与预构建静态库类似,节省编译时间,同时具有共享库的优点。
- 缺点 :同样存在版本兼容性问题,并且需要确保库文件在运行环境中正确部署。
- 适用场景 :使用第三方共享库,或者需要快速集成现有共享库的项目。
10. 异常处理的权衡
异常处理在 C++ 中是一个双刃剑,虽然它可以简化错误处理逻辑,但也会带来一些性能和代码复杂性的问题。下面是异常处理和手动错误检查的对比:
| 方法 | 优点 | 缺点 |
| — | — | — |
| 异常处理 | 代码简洁,将错误处理逻辑与正常业务逻辑分离,提高代码可读性;可以跨函数调用捕获异常,方便处理深层次的错误。 | 会增加编译后代码的大小,降低程序的执行效率;异常处理需要严格的代码设计,否则容易出现资源泄漏等问题。 |
| 手动错误检查 | 对性能影响较小,代码执行流程清晰,开发者可以精确控制错误处理的时机和方式。 | 代码冗余,需要在每个可能出错的地方添加检查代码,降低代码的可读性和可维护性。 |
在实际开发中,应该根据项目的具体需求来选择合适的错误处理方式。对于对性能要求极高的部分,可以采用手动错误检查;而对于业务逻辑复杂,需要统一处理错误的部分,可以考虑使用异常处理。
11. Makefile 高级应用示例
除了前面提到的基本操作,Makefile 还可以实现一些高级功能。例如,根据不同的编译模式(debug 或 release)设置不同的编译选项:
ifdef DEBUG
APP_CPPFLAGS += -g -O0
else
APP_CPPFLAGS += -O3
endif
上述代码根据 DEBUG 变量是否定义来设置不同的编译选项。如果 DEBUG 被定义,则使用 -g 生成调试信息, -O0 关闭优化;否则使用 -O3 进行最高级别的优化。
另外,还可以使用 Makefile 实现自动化的代码检查和测试:
check:
cppcheck $(LOCAL_PATH)/*.cpp
test:
adb push libs/armeabi-v7a/droidblaster_test /data/data/
adb shell /data/data/droidblaster_test
在这个示例中, check 目标使用 cppcheck 工具对源文件进行静态代码检查, test 目标则负责部署并运行测试可执行文件。
12. 总结
通过本文的介绍,我们学习了如何使用 Boost 单元测试库构建可执行文件,以及 Android Makefile 的相关知识。具体包括:
- 构建可执行文件的详细步骤,从创建 Android.mk 文件到部署运行。
- 四种构建原生库的方法及其使用场景。
- Makefile 变量的类型和作用,以及如何通过它们来控制编译设置。
- 启用 C++ 11 支持和 Clang 编译器的方法。
- Makefile 的编程指令和内置函数,以及如何进行实践操作。
掌握这些知识可以帮助我们更好地进行 Android 开发,提高项目的编译效率和可维护性。在实际应用中,我们可以根据项目的具体需求灵活运用这些技术,不断优化开发流程。
超级会员免费看
1万+

被折叠的 条评论
为什么被折叠?



