25、使用 Boost 编译可执行文件及掌握 Makefile

使用 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()
  1. 启用异常和 RTTI :要在应用程序中包含 Boost,需要将其与支持异常和 RTTI 的 STL 实现链接。在 Application.mk 文件中全局启用它们,代码如下:
APP_ABI := armeabi armeabi-v7a x86
APP_STL := gnustl_static
APP_CPPFLAGS := -fexceptions –frtti
  1. 创建新模块 :打开 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)
  1. 构建并运行项目 :构建项目后,查看 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))
  1. 检查变量是否正确定义
ifndef LOCAL_PATH
    $(error What a terrible failure! LOCAL_PATH not defined...)
endif
  1. 使用 foreach 指令打印文件和目录列表
ls = $(wildcard $(var_dir))
dir_list := . ./jni
files := $(foreach var_dir, $(dir_list), $(ls))
  1. 创建宏来记录消息和时间
log=$(info $(shell date +'%D %R'): $(1))
$(call log,My message)
  1. 测试 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 开发,提高项目的编译效率和可维护性。在实际应用中,我们可以根据项目的具体需求灵活运用这些技术,不断优化开发流程。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值