ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

看懂AOSP源码树:构建系统、device与vendor目录全解析

看懂AOSP源码树:构建系统、device与vendor目录全解析 很多做 Android 开发的同行第一次把 AOSP 完整源码拉到本地磁盘之后站在根目录前第一反应往往不是兴奋而是一种“家谱没带”的茫然。一份干净的 AOSP 源码树一级目录加上二级目录少说几十个每个目录又套着大量子目录。build.sh、build、frameworks、packages、hardware、device、vendor……这些名字单看都知道大概但要让它们变成一张“谁负责什么、跟谁串联”的结构图大部分人得花不少时间。这篇文章的目标就是帮你把这张结构图梳理出来让“家谱”真正派上用场。我会沿着两条线来解一条是从 build.sh 这个构建入口往前追溯看看 AOSP 的编译系统到底是怎么组织出 out/ 目录里的各种镜像另一条是从 vendor 目录往深处走把设备商代码、HAL 实现、二进制闭源库这些“暗线”也挖出来。理清这两条线再去看任何 Android 版本源码你都不会再迷路。1. 先看入口build.sh 背后是谁在指挥1.1 原生 AOSP 里其实很少见到根目录 build.sh坦白说以 source.android.com 默认的 manifest 拉下来的 AOSP根目录通常没有 build.sh。网上很多搜索词指向“AOSP build.sh”基本都来自两类情况一是厂商的源码包为了统一 CI 流程自己写一个封装脚本二是第三方开源项目里为了方便演示把官方命令包了一层。真正 Google 官方推荐的构建入口是三个命令source build/envsetup.sh lunch aosp_arm64-userdebug make -j$(nproc)那为什么大家都喜欢从 build.sh 说起因为它确实把整套构建动作浓缩成了一个人人能看懂的“入口”。一个典型的厂商 build.sh 往往只有十几行但里面已经包含了环境加载、产品选择、并行编译这几件最关键的事。我贴一个常见的封装结构#!/bin/bash set -e LUNCH_TARGET${TARGET_PRODUCT}-${TARGET_VARIANT:-userdebug} source build/envsetup.sh lunch $LUNCH_TARGET make -j$(nproc)别小看这段脚本。在公司里Build 服务器每天就是靠类似的脚本定时跑一次产出 out/ 目录里的系统镜像再交给测试部门刷机。它本身不是 AOSP 的一部分但作为构建入口却是理解整个源码树的钥匙。1.2 构建系统的三代迭代从 make 到 kati、Soong、Ninja我见过不少人在看 AOSP 构建系统时被那一堆“mk 文件、bp 文件、ninja 文件”搞晕。其实把这几十年的演进拉成一条线就清楚了。早期 AOSP 完全依赖 GNU Make。每个目录放一个 Android.mk里头写明白这个模块怎么编译、链接了哪些库、最终生成什么文件。Make 虽然强大但 Android 这种几万个模块的超大规模项目起步时间越来越不可接受。于是 Google 从 Android 7 开始逐步引入 kati把 Android.mk 转换成 Ninja 格式的规则再配合 Ninja 来做并行任务调度。到了 Android 8、9 以后又推出 Soong 和 Blueprint。Soong 负责读取 Android.bp描述模块、依赖、编译属性Blueprint 负责管理这些模块之间的关系最终输出的还是 build.ninja 文件。用大白话说Android.mk 是上一代“族谱”写法Android.bp 是新一代族谱写法Ninja 才是最后真正干活的包工头。现在新建源码模块官方默认都是 Android.bp但存量代码里还是有不少 Android.mk所以构建系统必须两套并行兼容。在 out/ 目录下的 build- 文件夹里你能看到一个超大的 build.ninja整个源码树几千个模块的编译规则基本都在那一个文件里串起来了。1.3 lunch 到底做了什么为什么 vendor 目录跟它有关很多新手有一个误区以为 lunch 只是简单选一个“用户名”。其实 lunch 背后做的是整个产品配置的解析。你执行 lunch aosp_arm64-userdebug 时构建系统会去查找 device/ 目录下对应的产品配置把这些信息读进来目标架构是 arm64编译类型是 userdebug依赖哪些模块生成哪些分区镜像system/vendor/product/odm 分区里放什么东西很多都由这一层决定。你输入的产品名会对应到 AndroidProducts.mk比如 device/google/codename/AndroidProducts.mk。这里有一个关键点lunch 不只是选择一个字符串它会加载一个完整的 Make 上下文其中就包括 PRODUCT_PACKAGES 变量。这个变量会把 vendor 目录或其他目录里编译出的模块一个个塞进 system.img、vendor.img 或 product.img。所以你后面在源码里新加一个 APK 或原生程序除了建好 Android.bp还要把这个模块名加进对应产品 mk 文件的 PRODUCT_PACKAGES 里否则编译不会把它包进镜像。这也是很多新手改了代码却“什么都没变”的常见原因。2. 按图索骥AOSP 顶层目录的“家谱”分类2.1 顶层目录总览先给几十个目录分个房以我常用的 Android 13 分支为例正常情况下根目录会看到 build、frameworks、packages、hardware、device、kernel、external、prebuilts、system、vendor 等一大串名字。第一次进来的人容易眼花但我习惯把它们分成六个“家族”。家族主要目录职责一句话框架核心frameworks/base、frameworks/av、frameworks/native系统框架、多媒体、图形与原生服务系统应用packages/apps、packages/modulesSettings、SystemUI、Launcher 等内核与硬件kernel、hardware、bootable、device、vendor内核、HAL、启动相关、厂商适配基础运行库bionic、libcore、art、externalC/C 运行库、Java 核心库、第三方库构建与工具链build、soong、prebuilts、toolchain整个编译体系的支柱测试与兼容性cts、test、platform_testing、compatibility兼容性测试与平台测试有了这个大框架再去读单个目录的内部结构心里就有底了。每一个目录背后不仅是一堆代码还代表了 Android 系统的一条分支脉络。2.2 重点目录的“出身”与“继承关系”先看 frameworks/base。Android 日常开发里说的 Framework API十有八九都在这。你熟悉的 Activity、Service、Context、Binder 机制都在 frameworks/base/core/java/android 下。SystemServer 的各种服务比如 ActivityManagerService、PackageManagerService则在 frameworks/base/services 里。整个系统启动时先拉起 native 层的 init接着启动 Zygote然后由 Zygote fork 出 SystemServerSystemServer 再去启动各个核心服务这一套骨架全在 frameworks/base 内完成。frameworks/av 和 frameworks/native 是另外两个近亲。frameworks/av 管的是音频、视频、相机这些多媒体框架比如 MediaServer、Stagefright 播放管线都在这里面frameworks/native 主要提供 SurfaceFlinger、图形缓冲区、Binder 的 native 实现。很多做图形渲染和相机开发的同行日常都是在这几个目录里打转。再看 packages/apps。这是系统级应用的聚集地Settings、Launcher3、SystemUI、Camera2、Gallery2 等等。有人会问 SystemUI 和 Frameworks 的关系其实 SystemUI 本身是一个应用进程负责状态栏、快捷开关、手势导航等 UI 部分但它大量通过 Binder 调用框架层的服务。如果你想改状态栏布局找 packages/apps/SystemUI如果你想改通知的判重逻辑那就要往 frameworks/base/services 里跑了。2.3 几个常被忽略但极其重要的目录external 目录值得单独说。它存放的是 Android 系统嵌入的第三方开源库比如 libpng、jsoncpp、sqlite、expat 等。对很多做系统裁剪的团队来说external 里的库版本升级都是一项风险不小的工作因为谁也不知道底层哪个模块对当前 API 有隐式依赖。prebuilts 是另一块特殊地盘。里面装的是预编译好的工具链和编译器比如 clang、gcc、go、sdk。因为 AOSP 编译必须依赖特定的交叉编译器如果 prebuilts 目录被误删或分支不对整个编译立刻失败。常见报错就是找不到 clang 编译器这不是代码问题而是工具链问题。还有一个容易被忽略的是 system 目录。注意它跟 out/ 目录里最终生成的 system.img 不是一回事。源码层的 system/ 主要放核心系统服务与底层协议比如 system/core 里有 init、libutils、libcutils、adbsystem/sepolicy 管的是 SELinux 安全策略system/connectivity 管网络相关服务。很多做系统稳定性的问题最后追根溯源都在 system/core 里。2.4 一些“过气”目录和版本差异如果你去看老版本 AOSP比如 Android 6、7 的源码树根目录里还能看到 dalvik 目录那是旧的虚拟机实现。现在系统早已换成 ART 运行时新版本库里 dalvik 目录基本被移除或合并到 art 目录。早期还有 pdk 目录全称 Platform Development Kit后来也很少提了。加上 repo 不同 manifest 的配置差异同一个源码树下哪些仓库会被拉下来也是会变的。因此不要拿着某个教程里的目录清单去硬套另一个版本的 AOSP。最可靠的做法是看根目录的 Android.bp 或 build/soong 里的配置再结合 repo manifest 去理解当前源码树到底包含哪些模块。3. 顺着 device 走进 vendor厂商私有代码是怎么混进来的3.1 为什么 AOSP 里要单独立一个 vendor从原始 AOSP 的角度看它是一个“公共开源基线”本身不带任何厂商私有内容。但真实手机上的 Android 系统必然包含高通的调制解调器、拍照增强算法、快充方案、指纹识别驱动、各种闭源 so 库。这些代码和二进制不可能开源也不可能直接写进 AOSP 主分支于是 vendor 目录就成了厂商专属内容的集散地。对应到产品上vendor 目录通常出现在两个层面。一种是仓库层面厂商自己的 repo 配置里会单独拉一个 vendor/厂商名/产品名 仓库甚至多个仓库另一种是编译路径层面构建时会把 vendor 下的模块编进独立的 vendor.img 分区。在 Android 8.0 引入 Project Treble 之后vendor 分区的独立性又强化了一步系统分区归 AOSP 框架厂商分区归设备商两者通过稳定的 HAL 接口通信。这样一来系统升级时不需要拉着整个厂商实现一起重编架构解耦价值非常大。3.2 device 与 vendor 的分工边界一家管户籍一家管私房钱device 和 vendor 是最容易被搞混的两个目录。我打个比方device 是“户口本”里面清楚记录你这个产品是什么配置、用哪块屏幕、有哪些传感器、要编译哪些公共模块vendor 是“私房钱保险柜”里面放的是厂商自己的实现、私有服务和不愿意公开的二进制库。典型的 device/ / 下你会看到 BoardConfig.mk、AndroidProducts.mk、device.mk、init 相关 rc 文件等。BoardConfig.mk 定义硬件体系结构、相对路径、分区大小AndroidProducts.mk 定义可 lunch 的产品组合device.mk 则把各种模块打包进 PRODUCT_PACKAGES。而 vendor/ / 下通常有 proprietary-files.txt、Android.bp、APK、so 库、配置文件等。proprietary-files.txt 往往列出一大堆从手机里提取出来的闭源文件构建过程中会复制到系统目录Android.bp 则定义厂商模块的编译方式。很多芯片方案商比如高通就会提供类似 vendor/qcom 的整套仓库里面包含音频、显示、定位、传感器等 HAL 实现。3.3 想读懂 vendor 目录得先认识 HAL 与 VNDK进入 vendor 目录后你迟早会遇到 HALHardware Abstraction Layer。从概念上说HAL 是 Android 系统与硬件之间的“翻译层”。frameworks/native 和 frameworks/av 是上层服务它们通过 Binder 调用 HAL 接口硬件厂家在 vendor 里实现这些接口最终操作实际的硬件。说到这VNDK 也绕不开。VNDK 全称 Vendor Native Development Kit是 Google 为厂商进程规定的一套“可链接库名单”。因为 vendor 分区里的二进制会跟不同版本的系统分区搭配如果随意链接系统私有库总有一天因为接口不兼容而崩溃。VNDK 把当前版本里开源、稳定的 native 库固定下来vendor 进程只允许链接系统提供的 NDK 和 VNDK 公共库避免底层 ABI 漂移问题。你在看 vendor 库的 Android.bp 时会频繁看到 vendor_available 或 vendor: true 这样的属性就是构建系统在这个约定下做依赖检查。3.4 为什么 vendor 目录经常“被删了也能编出一半”接触过其他厂商源码的开发者会发现vendor 目录有时候看起来像从某个真实手机上逆向出来的。这是因为很多二线厂商或开源爱好者没有拿到芯片原厂的完整 BSP只能从官方刷机包里提取闭源二进制再配合 AOSP 源码重新组织编译。这类源码的风险点在于提取的 so 库可能依赖特定系统版 lib跟当前 AOSP 分支的 ABI 不一致编译能过跑到设备上却会 dlopen 失败。遇到这类问题基本都是在 vendor 库的链接依赖和 VNDK 版本上出了问题。4. 从源码到刷机包目录结构到镜像产物的对应关系4.1 out 目录是怎么把源码变成 system.img、vendor.img 的理解了源码目录结构下一步就是要看懂 out/ 目录。执行 lunch 和 make 之后真正改写的目录基本都在 out/target/product/产品名/ 下。你会看到 system、vendor、product、odm、root、ramdisk 这些子目录。构建系统会把分区的中间文件放进这些目录比如 system 分区内容先放到 out/.../system/然后把整个目录打成 system.img。对应关系并不神秘frameworks/base 编译出来的 framework.jar、services.jar 会放进 system/frameworkpackages/apps/Settings 编译出来的 APK 会放进 system/app 或 system/priv-appvendor 目录下编译出的模块会放进 out/.../vendor/device 里配置的 ramdisk 内容会放进 root/ 或 ramdisk/。最终打包成 boot.img、system.img、vendor.img、product.img就组成了我们刷机的整套镜像。如果你只改了 frameworks/base 下某个 Java 文件想快速验证不需要整个 make。先 source build/envsetup.sh再在对应目录里执行 m 或 mm编译出单个模块然后用 adb push 或直接重新打包 system.img 刷入设备。这样省掉一两个小时的全量编译时间。4.2 实例在 vendor 目录新增一个模块并打入 vendor.img我拿一个最常用的场景举例你想在 Android 系统里加一个只有厂商自己能用的原生工具程序并希望它最终出现在 vendor 分区里。先在 vendor/ / /my_tool/ 下创建源码文件 my_tool.cpp然后在同目录建 Android.bpcc_binary { name: my_tool, srcs: [my_tool.cpp], shared_libs: [ libbase, libcutils, liblog, ], vendor: true, }注意 vendor: true 这个属性它告诉构建系统该模块是给 vendor 分区用的依赖检查也要按 VNDK 规则来不能随便链接系统私有库。接着在 device/ / /device.mk 里加一行PRODUCT_PACKAGES my_tool编译时执行source build/envsetup.sh lunch 你的产品-userdebug make my_tool编译成功后产物会出现在 out/target/product/产品名/vendor/bin/my_tool。如果想让整包镜像也包含它再执行 make vendorimage 或 make -j。最后用 fastboot flash vendor vendor.img 就能刷进设备。这里有几个很容易踩的坑。第一模块名和文件名可以不一致但 PRODUCT_PACKAGES 里的名字必须跟 Android.bp 里 name 字段完全一致大小写都要一致。第二vendor 分区里的二进制默认 SELinux 限制较严如果工具需要访问特定节点很可能被 SELinux 挡住这时候还要在 device/.../sepolicy 里补 allow 规则不是单纯编译成功就万事大吉。4.3 常见镜像构建命令速查很多刚接触源码编译的人记不住哪些命令生成哪些镜像我列一个自己常用的速查表目标命令说明完整构建make -j$(nproc)生成 boot、system、vendor 等全部镜像仅构建单个模块m 模块名模块名对应 Android.bp 中的 name当前目录模块mm需在模块目录下编译该目录所有模块指定目录模块mmm 路径编译指定路径下模块仅生成 systemmake systemimage只打包 system.img仅生成 vendormake vendorimage只打包 vendor.img清理后重新安装make installclean删掉中间产物保留 out 配置比 clean 快几倍我平时切产品版本时都会先用 installclean 而不是 clean。clean 会把 out 目录整个清掉重新全量编译耗时巨大installclean 只清理上次生成的产物和中间文件能省下不少时间。但如果换了 lun ch 目标或者改了系统架构还是老老实实 clean。5. 新手常踩的目录坑与排查经验5.1 常见问题速查表我把刚接触 AOSP 时最容易遇到的问题整理成了一张表都是实际开发里反复出现的情况。现象可能原因处理方式编译报错找不到 vendor/厂商 目录仓库 manifest 没拉 vendor 仓库或 vendor 被排除检查 .repo/manifests 配置补拉对应仓库lunch 时找不到目标产品AndroidProducts.mk 没被识别或产品配置不在 device 下确认源码树中产品目录存在执行 lunch 后重新查列表mm 编译成功但设备没变化产物没有 push 到设备或系统镜像没更新用 adb push 到对应路径或重新打包对应分区镜像运行到设备上 dlopen 失败vendor so 库链接了非 VNDK 系统库查看 logcat确认缺失库调整 Android.bp 的静态/动态链接切换产品后莫名编译失败out 目录残留上一目标的产物执行 make installclean 再编译根目录找不到某个目录当前分支或 manifest 不包含该仓库不要按旧文档硬找先看 manifest 中实际拉取了哪些仓库5.2 培养“地图思维”先看依赖和图谱再看具体实现见过太多人一上来就翻 frameworks/base 某个类找半天找不到其实是不熟悉源码树的“区块布局”。我自己的习惯是拿到一份新源码后不看代码先执行一遍 repo sync然后把 manifest.xml 拉到文本里看看所有仓库名是什么。仓库层级本身就是一张地图比如哪几个仓库属于 device哪几个属于 vendor哪几个属于 platform 公共代码从名字就能看出归属。再展开一层每个目录下的 Android.bp 或 Android.mk 都可以顺藤摸瓜。比如你想知道 SystemUI 在哪个仓库先查 packages/apps/SystemUI 下的 Android.bp想知道某个启动服务从哪来搜它的 rc 文件想知道 system_server 怎么起来的可以顺着 frameworks/base/services 的入口追。这些文件系统里的“登记信息”比盲目翻源码可靠得多。5.3 关于磁盘、内存和服务器配置的实战建议AOSP 源码的体积在 Android 12、13 时代已经非常庞大。干净的 repo 同步加完整编译建议至少预留 250GB 到 300GB 磁盘空间实际我见过不少项目用掉了 400GB 以上。内存方面编译大模块时 16GB 起步比较舒服32GB 更适合日常开发。如果公司有编译服务器一定要保证 out 目录单独挂盘不要让源码和编译产物挤在同一块硬盘上否则 IO 会成为最大瓶颈。另外如果你的项目只用部分模块比如只改 Settings 或 SystemUI可以考虑使用更精简的构建目标或者用 ccache 加速编译。ccache 配置好之后反复编译同一模块能快非常多但在多台机器共享同一份源码目录时要小心缓存命中问题避免因为缓存路径不同导致奇怪的结果。5.4 千万别忽略 git 分支和 repo 同步状态目录结构再清楚分支不对也白搭。vendor 目录和 device 目录经常不在同一个仓库节奏上尤其是芯片厂商的 BSP 更新频率跟 AOSP 官方分支完全不同步。我自己遇到过好几次编译莫名失败最后发现是 vendor 仓库还停在几个月前的旧提交而 frameworks 已经切到了新版本两边接口对不上。判断方法很简单在源码根目录执行 git status 看当前分支是否干净再进入到可疑的 vendor 或 device 目录里分别 git log 看提交时间。如果 vendor 跟 AOSP 版本明显脱节优先让 vendor 仓库回到产品需求对应的 tag 或分支而不是强行改代码去适配。6. 我踩过这些坑之后留给自己的一套读码顺序最后分享一个我这些年养成的读码顺序。拿到任何一份 Android 源码我不急着看 API 或算法而是先看根目录的 build/soong、build/envsetup.sh 和 device/厂商/产品 下的产品配置。这三处组合起来能让整个源码树“活”起来构建系统告诉你哪些模块会被编译产品配置告诉你这些模块要被装进哪个分区vendor 目录则告诉你哪些实现是厂商私有的。等这条主脉络清晰了再看自己关心的功能模块。比如想研究通知栏就顺着 SystemUI 的状态栏实现走想研究媒体播放就顺着 frameworks/av 的 MediaPlayer 往底层追。每当遇到一个目录名先问自己三个问题它属于哪个家族它依赖谁它最终会被打进哪个分区。能回答上这三个问题的人在 AOSP 源码里基本不会迷失。回到标题那句话AOSP 的家谱其实一点都不玄乎。build.sh 是门楣frameworks 和 packages 是主屋hardware 和 device 是地基vendor 则是深藏在宅子里的库房。只要把这张图刻在脑子里后续不管是移植、定制、裁剪还是排查问题都会顺手很多。希望这篇梳理能帮正在啃源码的你少走几段弯路。
返回列表