第十三章:模块依赖与编译顺序
依赖管理,说白了就是告诉构建系统「谁先干,谁后干」。
我在AOSP里摸爬滚打这些年,见过太多因为依赖没写对导致的编译失败。有些是编译顺序乱了,有些是循环依赖卡死了。今天咱们就把这块彻底讲透。
13.1 依赖声明:告诉系统你的模块需要谁
在Android.bp里声明依赖,最常用的就是 deps 和 shared_libs。我个人习惯把依赖分成两类:编译时依赖和运行时依赖。
13.1.1 编译时依赖
编译时依赖用 deps 声明。比如你的模块需要某个头文件,或者需要链接一个静态库:
cc_library_static {
name: "libmy_utils",
srcs: ["utils.cpp"],
export_include_dirs: ["include"],
}
cc_binary {
name: "my_app",
srcs: ["main.cpp"],
deps: [":libmy_utils"],
}
这里 :libmy_utils 前面的冒号表示引用同一个Android.bp文件里的模块。如果是其他目录的模块,需要写完整路径:
deps: ["//external/zlib:libz"],
13.1.2 运行时依赖
运行时依赖用 shared_libs。说白了就是你的程序在运行时要加载的.so文件:
cc_binary {
name: "my_app",
srcs: ["main.cpp"],
shared_libs: [
"libcutils",
"libutils",
"libbinder",
],
}
小提示: 我个人建议,能用 shared_libs 就别用 deps 去链接静态库。动态链接能减小最终镜像体积,而且升级方便。
13.2 编译顺序控制:让构建系统按你的节奏来
你想想看,如果模块A依赖模块B,那B肯定得先编译完。Soong会自动分析依赖图,但有时候我们需要手动干预。
13.2.1 隐式依赖 vs 显式依赖
隐式依赖是Soong自己推导出来的。比如你 #include 了某个头文件,Soong会尝试找到提供这个头文件的模块。但这种方式不可靠,我遇到过好几次因为隐式依赖没解析对导致的编译失败。
显式依赖就是你在 deps 里明确写出来的。这是最稳妥的方式。我的原则是:所有外部依赖必须显式声明。
13.2.2 使用 required 和 host_required
有时候你的模块不需要链接某个库,但需要它先编译好。比如你的测试程序需要先编译一个辅助工具:
cc_test {
name: "my_test",
srcs: ["test.cpp"],
required: ["test_helper_tool"],
}
required 保证 test_helper_tool 在 my_test 之前编译,但不会链接到最终产物里。
13.2.3 编译顺序的调试技巧
我曾经遇到过一个诡异的问题:同样的代码,在AOSP master上能编译,在某个release分支上就报链接错误。后来发现是编译顺序变了。
怎么调试?用这个命令:
# 查看模块的依赖关系
$ out/soong/.bootstrap/bin/soong_docs --module my_app
# 生成依赖图(dot格式)
$ out/soong/.bootstrap/bin/soong_docs --dot my_app > deps.dot
$ dot -Tpng deps.dot -o deps.png
核心要点: 编译顺序不是靠「猜」的,而是靠依赖图推导出来的。你的 deps 写得越精确,编译顺序就越稳定。
13.3 循环依赖处理:别让模块互相「抱死」
循环依赖是构建系统里最头疼的问题之一。模块A依赖B,B依赖C,C又依赖A——这就死锁了。
13.3.1 循环依赖的表现
当你看到这样的错误时,基本就是循环依赖:
error: circular dependency between modules:
lib_a -> lib_b -> lib_c -> lib_a
Soong会直接报错,不会继续编译。这是好事,因为循环依赖在编译时就会暴露,而不是等到运行时才崩溃。
13.3.2 常见的循环依赖场景
我总结了几种常见的循环依赖:
| 场景 | 说明 | 解决方案 |
|---|---|---|
| 头文件循环引用 | A.h 包含 B.h,B.h 又包含 A.h | 使用前置声明,或者拆分头文件 |
| 库互相调用 | libA 调用 libB 的函数,libB 也调用 libA 的函数 | 提取公共代码到新库 libCommon |
| 静态库循环 | 两个静态库互相依赖 | 合并成一个库,或者改用动态库 |
13.3.3 实战:如何打破循环依赖
我曾经在做一个系统服务时遇到了循环依赖。模块A需要模块B的接口,模块B又需要模块A的回调。当时我的做法是:
- 提取接口层:把A和B共同依赖的接口定义抽出来,放到一个单独的模块C里。
- 使用依赖注入:A和B不再直接依赖对方,而是通过接口C交互。
- 调整链接方式:把其中一个库改成动态链接。
具体代码示例:
// 原来的循环依赖
cc_library {
name: "lib_a",
deps: [":lib_b"],
}
cc_library {
name: "lib_b",
deps: [":lib_a"],
}
// 改造后
cc_library {
name: "lib_interface", // 公共接口
srcs: ["interface.cpp"],
}
cc_library {
name: "lib_a",
deps: [":lib_interface"],
}
cc_library {
name: "lib_b",
deps: [":lib_interface"],
}
警告: 千万不要试图用 LOCAL_ALLOW_UNDEFINED_SYMBOLS 来绕过循环依赖。这相当于掩耳盗铃,运行时必定崩溃。我曾经接手过一个项目,前任就是这么干的,结果每次启动都随机崩溃,排查了整整一周。
13.4 依赖管理的进阶技巧
嗯,这里再分享几个我常用的技巧。
13.4.1 使用 defaults 统一管理依赖
如果你的多个模块依赖同一组库,可以用 defaults 来减少重复:
cc_defaults {
name: "my_defaults",
shared_libs: [
"libcutils",
"libutils",
],
}
cc_binary {
name: "app1",
defaults: ["my_defaults"],
srcs: ["app1.cpp"],
}
cc_binary {
name: "app2",
defaults: ["my_defaults"],
srcs: ["app2.cpp"],
}
13.4.2 条件依赖
不同平台可能需要不同的依赖。用 arch 块来控制:
cc_library {
name: "lib_foo",
srcs: ["foo.cpp"],
arch: {
arm: {
shared_libs: ["lib_neon_optimized"],
},
x86: {
shared_libs: ["lib_sse_optimized"],
},
},
}
13.5 依赖图可视化
为了让你更直观地理解依赖关系,我画了一张图:
这张图展示了一个典型的依赖层次。注意看,lib_interface 在最中间,它被多个模块依赖,但自己不依赖任何上层模块。这就是打破循环依赖的关键——把公共部分抽出来放到下层。
总结一下: 依赖管理说白了就是三件事——声明清楚、控制顺序、避免循环。写 deps 的时候多花一分钟,后面能省下一天排查时间。我在项目里见过太多「先编译试试,不行再加依赖」的做法,结果就是依赖图越来越乱,最后谁都不敢动。
记住一个原则:依赖图应该是无环有向图。如果发现循环,别想着绕过去,老老实实重构代码。这是最省心的做法。
公众号:蓝海资料掘金营,微信deep3321