Android Studio与AOSP索引
把 AOSP 根目录直接拖进 Android Studio,能看到文件,却不一定能得到正确的跳转、宏定义和生成代码。 原因不是 IDE 不认识 Java 或 C++,而是 AOSP 的真实编译输入由 Soong、产品配置和 lunch 变体共同决定。 IDE 需要消费的是构建图的一个投影:某个源文件属于哪个模块、依赖哪些模块、使用哪些编译参数、哪些头文件 或源文件由构建过程生成。
Android 17 的 build/make/tools/ide_query/ 提供了这条投影链。本文从 ide_query.sh 和 Go 程序的入口 出发,追踪 Java 的 module_bp_java_deps.json、C/C++ 的 compile_commands.json,以及最终的 IdeAnalysis protobuf。前置知识是 lunch目标与编译变体、 模块化编译和 Ninja构建系统。 本文不讨论 Android Studio UI 菜单、Gradle Android 项目同步或 JetBrains 插件内部实现;重点是 IDE 所需 数据如何由 AOSP 源码产生,以及数据过期或缺失时如何定位。 本文面向已经能完成 lunch 和局部构建、但还不能解释 IDE 索引数据来自哪里的读者。读完后,应能从 ide_query.sh 追到 Java 模块依赖、C/C++ compdb 和 IdeAnalysis 消费结果,并区分构建图缺失、 生成文件缺失、查询部分失败与 IDE 缓存过期。
1. IDE索引
ide_query 是 IDE 数据提供者,不是 Android Studio 自带的 Gradle Sync 后端。Android Studio 是否通过 插件直接调用这套协议,取决于具体版本和插件;Android 17 AOSP 仓库只证明生产端协议。无论消费者如何 接入,直接导入 AOSP 根目录时都应把 .repo/ 和 out/ 排除在普通源码索引之外,再由构建数据补回真正 需要的生成文件,否则索引器会遍历大量 Git 元数据和重复生成物。
ide_query 接收一个 lunch target 和若干源文件路径。lunch target 必须是 <product>-<release>-<build_variant> 三段,程序把它解析成 LunchTarget,再从环境变量读取 ANDROID_BUILD_TOP、OUT_DIR 和 PREBUILTS_CLANG_TOOLS_ROOT。这些值决定了查询哪个 checkout、哪个 构建输出和哪个 Clang 分析器;IDE 本身不拥有这些状态。
ide_query.sh 只负责定位脚本、初始化构建环境并执行预置 Go;Linux 主机限制也在该脚本中明确检查。 真正决定结果的是 Go 程序的 main:没有输入文件时退出;.java/.kt 进入 Java 路径,.cc/.cpp/ .h 进入 C/C++ 路径,其他后缀被跳过。这里的“跳过”不是失败,调用方必须检查最终是否得到任何 target。
2. 一次查询的主线
main 首先调用 runMake(..., "nothing"),让基础构建输出保持可用,然后分别解析 Java 模块和 C/C++ 编译数据库。C/C++ 路径先调用 cc_analyzer 的 deps 模式确定需要构建的 target,再调用 Soong 构建 这些 target,最后用 inputs 模式生成编译参数和 generated files。
Go 源码中的 runMake 固定传入 SOONG_GEN_COMPDB=1,并把产品、release、variant 传给 soong_ui.bash。这解释了为什么 IDE 索引必须绑定一个构建变体:同一个 .cc 文件在不同 variant 下可能拥有不同的 -D、include 路径和生成头文件。
3. Java模块依赖
Java 查询加载 out/soong/module_bp_java_deps.json。文件的每个模块记录 path、dependencies、 srcs、jars 和 srcjars。findJavaModules 按模块名排序,把输入文件匹配到第一个覆盖它的模块, 跳过 .impl 模块和没有 jar 的模块;这样同一个源文件被多个模块覆盖时结果仍然稳定。
getJavaInputs 把匹配模块变成 BuildableUnit,再用队列遍历依赖,把直接和间接依赖都加入输出。 genFiles 会把位于 OUT_DIR 下且实际存在的生成文件读入 GeneratedFile。因此 Java 单元的消费者 不是简单的文件列表,而是“源文件 + 生成文件 + 依赖模块”的图。
| 字段 | 所有者 | IDE消费者 | 失效信号 |
|---|---|---|---|
srcs | Soong Java module | 项目源文件 | Android.bp 或模块源变更 |
dependencies | Soong module graph | 类型解析与跳转 | 依赖声明/variant 变更 |
jars/srcjars | Java构建动作 | classpath 与生成源码 | 生成动作失败或 jar 更新 |
GeneratedFile.contents | ide_query | 即时解析 | OUT_DIR 文件不存在 |
如果文件没有匹配模块,结果状态是 CODE_NOT_FOUND;这不是“IDE 索引慢”,而是该路径没有进入当前 构建图,可能是文件属于未选择的产品、被条件分支排除,或输入路径不属于当前 checkout。
4. C/C++编译输入
cc_analyzer 使用 Clang 的 JSONCompilationDatabase 读取 out/soong/development/ide/compdb/compile_commands.json。GetDeps 对每个活动文件查找编译命令, 返回对应源文件的构建 target;当前实现明确标注仍未查询 Ninja 图,因此 target 可能不是最小集合。
GetBuildInputs 再次读取同一条编译命令,保存工作目录和完整 CommandLine。随后 ScanIncludes 使用 Clang 预处理器追踪实际 include 文件,并把位于 OUT_DIR 下的生成头文件及其内容 写入结果。它会关闭 warning,把 -Wno-error 和 -w 加入分析参数,目的是让不完整源码仍能完成输入扫描, 不是宣称该源码可以成功编译。
源码文件:build/make/tools/ide_query/cc_analyzer/analyzer.cc
相关函数/类型:GetBuildInputs
auto cmds = db->get()->getCompileCommands(abs_file);
if (cmds.empty()) {
result.mutable_status()->set_code(Status::FAILURE);
continue;
}
auto includes = ScanIncludes(cmds.front(), llvm::vfs::createPhysicalFileSystem());这段代码的关键不在于读取 JSON,而在于 owner 转移:Soong 负责产生编译命令,Clang analyzer 负责解释 命令并发现生成输入,IdeAnalysis 只负责把结果交给 IDE。任何一层过期都会导致“能打开文件但跳转错误”。
5. 结果协议
ide_query_proto/ide_query.proto 定义 AnalysisResult 和 BuildableUnit。Java/Kotlin 的语言标记为 LANGUAGE_JAVA,C/C++ 为 LANGUAGE_CPP;每个源文件有 CODE_OK、CODE_NOT_FOUND 或 CODE_BUILD_FAILED。BuildableUnit 的 dependency_ids 允许消费者构建依赖图,generated_files 则 把生成内容作为内存输入提供给 IDE。
结果通过 stdout 以 protobuf 二进制写出,状态日志写到 stderr。调用方不能把 stdout 当作可读 JSON,也 不能把 stderr 的状态行当作协议内容。结果只代表本次查询时的构建图快照;Android.bp、产品配置、Clang 工具链或 OUT_DIR 变化后必须重新生成。
6. 失败边界
| 现象 | 源码位置 | 判断 | 恢复动作 |
|---|---|---|---|
No files provided | main | 调用没有输入路径 | 传入 .java、.kt、.cc、.cpp 或 .h |
invalid lunch target | LunchTarget.Set | 三段格式不满足 | 使用当前产品的完整 lunch target |
Java CODE_NOT_FOUND | findJavaModules | 文件不在当前模块图 | 检查模块条件和 source path |
| C/C++ 找不到 compile flags | cc_analyzer::GetDeps | compdb 缺失或未覆盖文件 | 先生成匹配 variant 的 compdb |
| working dir 在 checkout 外 | GetBuildInputs | 编译命令路径不属于 repo | 检查生成命令和 OUT_DIR 配置 |
| generated file 缺失 | ScanIncludes/genFiles | 生成输入尚未产出 | 构建对应模块后重新查询 |
| build target 失败但有部分结果 | main | runMake 记录错误后继续组装结果 | 先修复构建错误,再信任 IDE 结果 |
失败时 ide_query 可能仍输出部分 AnalysisResult。这是一种可诊断的部分成功,不是整仓索引成功; 调用方应同时检查每个结果的 status 和顶层 error。
7. 配置测试
仓库中的 prober fixture 为 JVM 和 C++ 各提供一个小型输入。JVM fixture 用 Foo.java、Bar.java 和 suite.textpb 生成预计算输出;C++ fixture 用 general.cc 和 Android.bp 验证编译参数、 生成 proto 头文件和 protobuf 依赖。ide_query.out 是固定输入下的结果快照,证明的是协议字段和 分析结果形状,不证明完整 AOSP checkout 的所有模块都能索引。
cc_analyzer 的源码分支还给出可反向验证的断言:无法加载 compdb 返回 FAILURE;文件没有编译命令 时为对应 source 写入失败状态;include 扫描失败时保留该文件的失败信息。由于 GetDeps 当前把源文件 名加上 ^ 作为 target,并明确留下“尚未查询 Ninja 图”的注释,读者不应把它解释成最小增量构建保证。
8. 实际验证
在 Linux AOSP checkout 中,先完成与目标产品一致的 lunch 和基础构建,再对少量文件运行查询:
source build/envsetup.sh
lunch <product>-<release>-<variant>
build/make/tools/ide_query/ide_query.sh \
--lunch_target=<product>-<release>-<variant> \
frameworks/base/core/java/android/os/Bundle.java \
system/core/init/init.cpp > /tmp/ide-analysis.pb/tmp/ide-analysis.pb 是 protobuf 二进制,不能用 cat 判断内容;应使用对应 proto 定义解码,或在 IDE 插件中读取 AnalysisResult.status、BuildableUnit.dependency_ids 和 generated_files。 若只需要 C/C++ 编译数据库,可先查看:
ls out/soong/development/ide/compdb/compile_commands.json
ls out/soong/module_bp_java_deps.json验证的实践路径是:任选一个源文件,先在 compile_commands.json 或 module_bp_java_deps.json 找到 owner,再追到 ide_query 选择该 owner 的代码,最后解释生成文件 和依赖为何出现在 protobuf 中。若构建配置变化后索引仍显示旧宏,优先检查 OUT_DIR、lunch target 和 compdb 时间,而不是先删除 IDE 缓存。
本文没有把 Android Studio 当作 AOSP 构建系统的 owner。IDE 只是结果消费者;真正决定可解析输入的是 Soong、产品配置、Clang 工具链和生成产物。后续的 Compdb 专题会单独展开 compile_commands.json 的生成条件和字段语义。
