在 Android 系统开发中,频繁地修改和调试各个模块是必不可少的环节。传统的 make 命令在大型项目上的编译速度一直是痛点。随着 Android 系统的日益复杂,使用 make 进行全量编译耗时漫长,严重影响开发效率。为了解决这个问题,Android 构建系统引入了 Ninja。Ninja 是一个小而快的构建系统,它通过读取 .ninja 文件来执行构建任务。理解 Android 系统模块编译调试的流程,并熟练掌握 Ninja 的使用,可以显著提升开发效率,减少等待时间,将更多精力投入到核心业务逻辑的开发中,而不是被编译问题所困扰。
Ninja 构建系统原理剖析
Ninja 的核心概念
Ninja 的核心在于其简洁高效的构建描述语言。它不像 make 那样依赖复杂的 Shell 脚本和依赖关系推导,而是通过明确的规则(rules)和构建目标(build targets)来定义构建过程。简单来说,build 语句声明了如何从输入文件(inputs)生成输出文件(outputs),以及使用的 rule。
# 示例:编译一个 C++ 源文件
rule cc
command = clang++ -o $out $in
build main.o: cc main.cpp
在这个例子中,rule cc 定义了 C++ 编译器的调用方式,build main.o: cc main.cpp 声明了 main.o 这个目标文件由 main.cpp 通过 cc 规则生成。
Android 构建系统中的 Ninja
在 Android 构建系统中,.ninja 文件通常由 Soong 编译系统生成。Soong 负责解析 Android.bp 文件,分析模块间的依赖关系,然后生成对应的 .ninja 文件。Ninja 最终读取这些文件,按照其中描述的依赖关系和规则执行构建任务。可以使用 mma 或 mm 命令编译指定的模块,Soong 会增量生成或更新相应的 .ninja 文件,然后调用 Ninja 进行编译。这保证了只有修改过的模块及其依赖才会被重新编译,大大缩短了编译时间。
如何查看和理解 .ninja 文件
虽然 .ninja 文件通常由构建系统自动生成,但理解其内容对于调试编译问题至关重要。可以通过查看 .ninja 文件来了解模块的依赖关系、编译选项等信息。.ninja 文件通常位于 out/build-xxx/ninja 目录下(xxx 是 build variant,例如 debug 或 release)。
可以使用文本编辑器打开 .ninja 文件,查看其中的 build 语句和 rule 定义。例如,可以搜索某个模块的名称,找到与其相关的构建规则。
Android 系统模块编译调试实战
场景:修改系统服务代码并编译调试
假设我们需要修改 SystemServer 中的某个类。首先,找到对应的源码文件,例如 frameworks/base/services/java/com/android/server/SystemServer.java。修改代码后,我们需要重新编译 SystemServer 模块。
定位模块: 首先确定
SystemServer模块对应的Android.bp文件。通常位于frameworks/base/services/Android.bp。模块编译: 使用
mma命令编译该模块。在 Android 源码根目录下执行:
mma frameworks/base/servicesmma命令会自动分析依赖关系,生成或更新.ninja文件,并调用 Ninja 进行编译。编译完成后,会在out/target/product/<设备名称>/system/framework目录下生成services.jar文件。设备部署: 将新生成的
services.jar文件推送到设备上,替换原有的文件。可以使用adb push命令:adb root # 如果没有 root 权限,需要先获取 adb remount adb push out/target/product/<设备名称>/system/framework/services.jar /system/framework/services.jar adb reboot重启设备后,新的代码就会生效。
使用 Ninja 单独编译模块
除了使用 mma 命令外,还可以直接使用 Ninja 命令编译指定的模块。首先,需要找到该模块对应的 .ninja 文件。例如,SystemServer 模块的 .ninja 文件可能位于 out/build-xxx/ninja/frameworks/base/services/services.ninja。然后,可以使用以下命令编译该模块:
ninja -f out/build-xxx/ninja/frameworks/base/services/services.ninja services.jar
这会直接调用 Ninja 构建 services.jar 目标。这种方式更加灵活,可以更精确地控制编译过程。
调试技巧
查看编译日志: 编译过程中,Ninja 会输出详细的编译日志。可以通过查看日志来定位编译错误。通常,日志会显示编译命令、错误信息等。
修改编译选项: 有时,需要修改编译选项来解决特定的问题。可以通过修改
Android.bp文件中的cflags或ldflags属性来添加或修改编译选项。// Android.bp 示例 java_library { name: "services", srcs: ["java/com/android/server/SystemServer.java"], cflags: [ "-DDEBUG", // 添加 DEBUG 宏 ], }增量编译: 充分利用 Ninja 的增量编译特性,只编译修改过的模块及其依赖。避免全量编译,节省时间。

实战避坑经验
依赖关系错误: Android 构建系统依赖关系复杂,容易出现依赖关系错误。编译时,如果出现
ninja: error: unknown target错误,通常是因为依赖关系配置不正确。需要仔细检查Android.bp文件中的deps属性,确保所有依赖都已正确声明。编译环境问题: 编译环境问题也可能导致编译失败。例如,缺少必要的工具链、环境变量配置不正确等。需要确保编译环境已正确配置,并且所有必要的工具链都已安装。
缓存问题: 有时,即使代码没有修改,编译系统也可能重新编译模块。这可能是因为缓存问题导致的。可以尝试清理缓存,重新编译。
make cleanSoong 语法错误:
Android.bp文件使用 Soong 语法编写。如果Android.bp文件存在语法错误,会导致编译失败。需要仔细检查Android.bp文件,确保语法正确。SELinux 权限问题: 推送修改后的系统文件到设备后,可能会遇到 SELinux 权限问题,导致系统无法正常运行。需要根据具体情况,修改 SELinux 策略,授予相应的权限。可以使用
audit2allow工具生成 SELinux 策略。
总之,熟练掌握 Android 系统模块的编译调试流程,并灵活运用 Ninja 构建系统,可以显著提高开发效率,减少不必要的等待时间,将精力集中在解决实际问题上。合理利用 mma 命令和直接调用 ninja 命令,可以灵活选择最适合的编译方式。同时,也要注意避免常见的编译错误,并掌握相应的调试技巧,才能高效地进行 Android 系统开发。
冠军资讯
CoderPunk