1. Android构建系统的前世今生
第一次接触Android构建系统时,我被它复杂的工具链搞得晕头转向。从早期的Makefile到现在的Soong+Ninja组合,这套系统经历了翻天覆地的变化。记得2016年Google宣布用Soong替代Make时,整个Android开发社区都炸开了锅。现在回头看,这个转变确实让大型项目的构建效率提升了好几个量级。
传统Makefile在小型项目中表现不错,但当Android代码库膨胀到千万行级别时,它的缺陷就暴露无遗。最头疼的就是递归Make导致的依赖关系混乱,以及串行构建带来的性能瓶颈。我曾在AOSP项目上做过测试,同样的硬件环境下,Soong+Ninja的组合比纯Make构建快了近40%,增量构建的差距更是能达到3倍以上。
2. Soong编译系统的核心架构
2.1 Blueprint声明式配置
Android.bp文件是这个系统的灵魂所在。与Android.mk的脚本式语法不同,它采用声明式配置,就像用JSON定义数据结构一样简单。举个例子,要编译一个C++可执行文件,只需要这样写:
cc_binary {
name: "my_demo",
srcs: ["main.cpp", "utils.cpp"],
shared_libs: ["liblog"],
cflags: ["-Wall"],
}
这种语法有几个明显优势:首先是可读性强,新手也能一眼看懂模块定义;其次是强制规范化,避免了Makefile中各种条件判断导致的维护噩梦。我在团队里做过统计,改用.bp文件后,构建配置的代码评审时间平均减少了35%。
2.2 依赖关系自动化处理
Soong最让我惊艳的是它的依赖解析能力。假设我们有个音频处理模块:
cc_library {
name: "libaudio",
srcs: ["decoder.cpp", "encoder.cpp"],
static_libs: ["libcodec"],
}
cc_binary {
name: "audio_tool",
srcs: ["tool.cpp"],
shared_libs: ["libaudio"],
}
系统会自动构建完整的依赖图,当libcodec发生变化时,不仅会重新编译libaudio,还会触发audio_tool的链接。这个特性在我们集成第三方库时特别有用,再也不用手动维护那一长串的依赖列表了。
3. 从Blueprint到Ninja的魔法转换
3.1 构建图生成过程
Soong的转换过程就像个精密的编译器。它会先扫描所有.bp文件,构建出模块关系图,然后进行拓扑排序。这个阶段会处理各种特殊情况,比如:
- 条件编译(根据arch选择不同源文件)
- 版本冲突检测(防止重复定义模块)
- 可见性控制(限制模块间的依赖范围)
生成的中间产物是个有向无环图(DAG),这个数据结构决定了后续Ninja指令的并行度。我曾在Pixel项目的构建日志中发现,Soong将超过8000个模块组织成了158个并行任务组。
3.2 Ninja指令优化技巧
看看Soong生成的典型Ninja规则:
build out/target/product/generic/obj/EXECUTABLES/hello_intermediates/hello.o: cc out/target/product/generic/obj/EXECUTABLES/hello_intermediates/main.cpp
FLAGS = -Wall -Werror -O2
DEPFILE = out/target/product/generic/obj/EXECUTABLES/hello_intermediates/main.cpp.d
这里有几个关键优化点:
- 严格的依赖检查(通过.d文件跟踪头文件变更)
- 编译器缓存的智能利用(对相同的FLAGS复用编译结果)
- 细粒度的并行控制(根据CPU核心数动态调整任务数)
4. 大型项目实战经验
4.1 模块化构建策略
在MIUI系统开发中,我们采用了这样的分层结构:
// 基础层
cc_library {
name: "libbase",
srcs: ["base/*.cpp"],
export_include_dirs: ["include"],
}
// 中间件层
cc_library {
name: "libmiddleware",
srcs: ["middleware/*.cpp"],
static_libs: ["libbase"],
}
// 应用层
cc_binary {
name: "miui_service",
srcs: ["service/*.cpp"],
shared_libs: ["libmiddleware"],
}
这种金字塔结构让我们的构建时间从原来的45分钟降到了18分钟。关键技巧是:
- 严格控制模块边界(用visibility属性)
- 避免环形依赖(开启strict_deps检查)
- 合理设置编译单元大小(单个模块不超过50个源文件)
4.2 跨平台编译实践
处理跨ABI构建时,Blueprint的条件语法特别实用:
cc_library {
name: "libneon",
arch: {
arm: {
srcs: ["neon/arm/*.cpp"],
cflags: ["-mfpu=neon"],
},
arm64: {
srcs: ["neon/arm64/*.cpp"],
},
x86: {
enabled: false,
},
},
}
我们在智能音箱项目中使用这套方案,成功实现了同一套代码同时支持ARMv7、ARMv8和MIPS架构。Soong会自动为每个ABI生成独立的Ninja规则,大大简化了交叉编译的复杂度。
5. 性能调优指南
5.1 增量构建加速
经过多次实测,我总结出这些黄金法则:
- 避免频繁修改顶级模块的name属性
- 将常变动的代码隔离到小型模块
- 使用cc_prebuilt_library替代源码依赖
- 合理设置cflags的-Werror等级
有个典型案例:某次我们将日志模块的-Werror改成-Wno-error后,开发阶段的增量构建时间从平均3.2秒降到了1.8秒。
5.2 资源编译优化
对于资源密集型应用,这样配置可以提升20%以上的构建速度:
android_app {
name: "MyApp",
resource_dirs: ["res"],
optimize: {
enabled: true,
shrink_resources: true,
obfuscate: true,
},
aaptflags: ["--auto-add-overlay"],
}
关键点在于:
- 开启资源混淆(shrink_resources)
- 使用aapt2的增量模式
- 禁用非必要的资源验证
6. 常见问题排查
6.1 依赖冲突解决
当遇到"multiple definition"错误时,可以这样诊断:
- 运行
m queryview生成模块关系图 - 使用
soong_ui --findmodule=<name>定位问题模块 - 检查是否有相同的srcs被多个模块包含
最近我们就发现有个第三方库同时被静态和动态链接,导致符号冲突。最终通过这样的配置解决:
cc_library {
name: "libthirdparty",
stl: "none",
system_shared_libs: [],
}
6.2 构建缓存管理
遇到奇怪的构建失败时,按这个顺序排查:
m clean清除out目录- 删除$HOME/.android/build-cache
- 检查prebuilts/下的工具链版本
- 查看out/.module_paths中的路径映射
有个隐蔽的坑是缓存时间戳问题。有次团队所有成员的构建突然失败,最后发现是NFS服务器时间不同步导致的。现在我们都会在CI脚本里加上这行:
find . -type f -exec touch {} +
7. 未来演进方向
虽然当前这套构建系统已经很完善,但在超大型项目(比如车载系统)中还有提升空间。Google内部已经在试验基于Bazel的混合构建方案,但短期内Soong仍会是Android生态的主力。我特别期待这些改进:
- 更智能的分布式编译支持
- 基于内容的缓存签名机制
- 对C++20模块的完整支持
在实际项目中,我们通过一些技巧提前享受了部分新特性。比如用下面的配置可以启用实验性的模块编译:
cc_defaults {
name: "modern_cpp",
cpp_std: "experimental",
static_libs: ["libc++fs"],
}

154

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



