Android.bp与Soong构建系统:Blueprint、Ninja、模块化编译与依赖管理
说实话,刚接触Android源码那会儿,我对着Android.mk和一堆Makefile看了好几天,心里直犯嘀咕:这玩意儿也太绕了吧?后来Google推出了Soong和Android.bp,我才算真正松了口气。今天咱们就来聊聊这套构建系统的核心——Blueprint、Ninja,以及模块化编译和依赖管理到底是怎么玩的。
从Make到Soong:为什么要有这么一套东西?
老一套的Android.mk基于GNU Make,说实话,它太慢了。你想想看,一个几千万行代码的项目,每次编译都要解析一堆Makefile,光解析时间就够喝杯咖啡的。而且Make的依赖管理是隐式的,很容易出现“改了头文件但没重新编译”这种坑。
Soong的出现,说白了就是要把构建这件事变得更快、更可控。它用Android.bp这种声明式的配置文件,取代了原来那些冗长的Make脚本。我个人习惯把Android.bp看作“构建清单”——你只需要告诉系统“我要编译什么、依赖什么”,剩下的交给Soong去处理。
核心思路: Soong将.bp文件解析成Blueprint的抽象语法树,然后生成Ninja文件,最后用Ninja来执行真正的编译任务。Ninja专注于“快”,它只做一件事:根据依赖图并行执行命令。
Blueprint:构建的“翻译官”
Blueprint是Soong的底层解析引擎。它把Android.bp里那些人类可读的配置,翻译成机器能理解的依赖图。我刚开始研究Blueprint时,觉得它就像个“翻译官”——一边是开发者写的简洁配置,另一边是Ninja需要的精确指令。
举个例子,你在Android.bp里写一个模块:
cc_library {
name: "libhello",
srcs: ["hello.cpp"],
shared_libs: ["liblog"],
}
Blueprint会把它解析成一个Module对象,然后分析出:libhello依赖liblog,源文件是hello.cpp。接着,它会生成对应的Ninja构建规则。这个过程我称之为“结构化解析”——每个模块的属性都被明确地提取出来,不像Make那样靠字符串拼接。
我的经验: 有一次我写了一个cc_binary模块,忘了加shared_libs,结果链接时报了一堆未定义引用。Blueprint的解析阶段不会报错,但Ninja执行链接时就会暴露问题。所以,写完.bp文件后,我建议先用m -j1 nothing跑一下,看看Blueprint解析有没有警告。
Ninja:真正的“搬砖工”
Ninja的设计哲学很简单:别做多余的事。它不像Make那样内置各种函数和规则,Ninja只有两个核心概念:rule(规则)和build(构建语句)。Soong负责生成这些规则,Ninja只管执行。
你看,一个典型的Ninja文件片段是这样的:
rule cc
command = clang -c $in -o $out
description = CC $out
build out/hello.o: cc hello.cpp
没有条件判断,没有变量展开,就是纯粹的“输入→命令→输出”。这种设计让Ninja能极快地解析文件,启动时间通常在毫秒级。我记得有一次在服务器上编译AOSP,Make的解析时间占了30秒,换成Soong+Ninja后,解析时间直接降到2秒以内。
注意: Ninja本身不支持增量编译的智能判断——它完全依赖Soong生成的依赖文件(.d文件)来跟踪头文件变化。如果你手动修改了.d文件,或者删除了中间文件,Ninja可能会漏编译。我曾经手贱删了out/目录下的某个.o文件,结果Ninja没检测到,导致最终链接了一个旧的对象文件……嗯,从那以后我再也不敢乱动out/目录了。
模块化编译:把大象装进冰箱
Android源码里有几千个模块:库、可执行文件、Java包、资源包……Soong把它们都看作“模块”,每个模块有自己的Android.bp文件。模块之间通过deps、shared_libs、static_libs等属性声明依赖。
这种模块化设计的好处是:你可以只编译你关心的部分。比如我只改了system/core/liblog里的代码,那么只需要运行m liblog,Soong会分析出所有依赖liblog的模块,然后只重新编译受影响的部分。这比全量编译快太多了。
下面这张图展示了模块化编译的核心流程:
你看,整个流程是单向的:从.bp文件到Blueprint解析,再到Soong生成Ninja文件,最后Ninja执行编译。左侧那条虚线表示增量编译的反馈——当你修改了某个源文件,Soong会重新解析受影响的模块,只生成必要的Ninja规则。
依赖管理:别让“依赖地狱”找上门
在大型项目里,依赖管理是最容易出问题的环节。Soong提供了几种依赖声明方式:
| 属性 | 用途 | 示例 |
|---|---|---|
shared_libs |
动态链接库依赖 | shared_libs: ["liblog", "libutils"] |
static_libs |
静态库依赖 | static_libs: ["libtinyxml2"] |
header_libs |
仅头文件依赖 | header_libs: ["libheaders"] |
whole_static_libs |
全量静态库(不裁剪未引用符号) | whole_static_libs: ["libfoo"] |
deps |
通用依赖(用于非库模块) | deps: [":my_generated_headers"] |
这里有个容易踩的坑:循环依赖。比如libA依赖libB,libB又依赖libA。Soong在解析时会检测到循环依赖并报错。我曾经在重构一个老模块时,不小心引入了循环依赖,结果Blueprint直接抛了个circular dependency detected错误。解决办法通常是拆分模块,或者把公共部分抽出来做成一个独立的库。
避坑指南: 如果你遇到undefined reference错误,先检查shared_libs和static_libs有没有写全。另外,注意whole_static_libs和static_libs的区别——前者会把整个静态库都链接进来,后者只会链接被引用的符号。我一般只在需要保留所有符号的场景(比如JNI库)才用whole_static_libs。
实战:写一个简单的Android.bp
说了这么多,咱们来点实际的。假设你要编译一个C++可执行文件,它依赖一个静态库和一个动态库:
cc_library_static {
name: "libmystatic",
srcs: ["mystatic.cpp"],
cflags: ["-Wall"],
}
cc_binary {
name: "myapp",
srcs: ["main.cpp"],
static_libs: ["libmystatic"],
shared_libs: ["liblog"],
ldflags: ["-Wl,--gc-sections"],
}
这个例子很简单,但包含了几个关键点:
cc_library_static生成静态库libmystatic.acc_binary生成可执行文件myappstatic_libs把静态库的内容直接复制到可执行文件中shared_libs声明运行时依赖的动态库
编译时,Soong会先编译libmystatic,再编译myapp,最后链接。如果你只改了mystatic.cpp,那么只有libmystatic和myapp会被重新编译——这就是模块化编译的威力。
核心总结: Soong + Blueprint + Ninja 这套组合拳,把Android的构建从“慢而全”变成了“快而准”。Android.bp让配置变得清晰,Blueprint负责解析和依赖分析,Ninja负责高效执行。你只需要把精力放在模块划分和依赖声明上,剩下的交给工具链。
嗯,这套系统我用了好几年,从最初的抗拒到现在的依赖,确实能感受到Google在构建效率上的用心。下次你编译AOSP时,不妨打开out/soong/build.ninja看看——里面密密麻麻的规则,就是Soong为你精心编排的“编译交响乐”。
公众号:蓝海资料掘金营,微信deep3321