01 - 项目全景与构建体系
MiCarSettings 新人培训手册 · 第一篇 适用读者:刚接手 MiCarSettings、需要快速理解「项目是怎么搭起来、怎么编出来」的 Android 工程师
本篇概览
本篇从最宏观的视角拆解 MiCarSettings:它是什么、源头在哪、目录怎么分层、四层模块之间怎么互相依赖,以及一行 ./gradlew :app:assembleXcdDebug 命令背后 Gradle 实际做了哪些事。读完后你会建立起「打开这个仓库不再迷路」的全局心智模型,后续每一篇(页面开发、路由、车辆接口、语音等)都会挂在这个骨架上。
读完你能掌握
- MiCarSettings 的产品定位,以及它为什么「长得像 AOSP Car Settings」——能区分哪些代码继承自上游、哪些是小米自研增量。
- 仓库的四层模块拓扑(app / base / settingsPage / settingsCommon)以及它们之间的依赖方向,拿到一个新功能能立刻判断该往哪一层放。
dcd/xcd/global三套 Flavor 的差异、各自的 sourceSet 目录约定,以及为什么需要三套。- 一行
./gradlew :app:assembleXcdDebug触发的完整构建链:Gradle 解析顺序、afterEvaluate注入平台 jar、bootstrapClasspath前置、checkstyle 全量拦截。 - 平台签名、平台 jar、installHook、checkstyle/detekt 这些「工程基础设施」在哪里配置、怎么影响你的日常提交。
一、项目定位与技术栈
1.1 它是什么
MiCarSettings 是小米智能座舱(车机)上的「车辆设置」App,包名 com.android.car.settings,是用户在车机屏幕上调节充电、显示、灯光、锁车、驾驶模式、媒体、音量、IoT、连接、安全服务、自动驾驶、小爱等参数的统一入口。它是一个系统特权应用:在 app/src/main/AndroidManifest.xml:21 处声明了 coreApp="true" 与 android:sharedUserId="android.uid.system",意味着它和 system_server 跑在同一 uid 下,能调用大量系统级 API。
1.2 源自 AOSP Car Settings
仓库根目录的 build.gradle、app/src/main/AndroidManifest.xml 顶部都保留了 Apache 2.0 头与 Copyright (C) 2017/2019 The Android Open Source Project 字样,入口目录 app/src/main/java/com/android/car/settings/common/ 下还能看到从 AOSP packages/apps/Car/Settings 继承下来的 BaseCarSettingsActivity.java、SubSettingsActivity.java、CarSettingActivities.java 等老结构文件。这解释了几个新人常困惑的现象:
- PreferenceController 范式:页面对应 Fragment + 一组
*PreferenceController,这是 AOSP Car Settings 的核心模式,MiCarSettings 在其上做了大量小米自研封装(详见后续「页面开发篇」)。 - 包名仍是
com.android.car.settings:因为要和 AOSP 的资源、权限、sharedUserId 体系兼容,不能随意改。 - Java + Kotlin 并存:AOSP 原生代码以 Java 为主(
common/、FallbackHome.java),小米增量大量使用 Kotlin(如app/src/main/java/com/android/car/settings/miauto/common/DeeplinkActivity.kt)。
1.3 技术栈速览
| 维度 | 选型 | 出处 |
|---|---|---|
| 构建工具 | AGP 7.2.0(com.android.tools.build:gradle:7.2.0);Gradle wrapper = 9.0-milestone-1(注意:7.2.0 是 AGP,不是 Gradle 发行版) | build.gradle:46、gradle/wrapper/gradle-wrapper.properties:4 |
| 主语言 | Kotlin 1.8.10 + Java 1.8 | build.gradle:22, 各模块 compileOptions |
| SDK | minSdk 32 / targetSdk 32 / compileSdk 33 / buildTools 29.0.2 | constants.gradle:2-5 |
| ABI | arm64-v8a(车机平台) | app/build.gradle:50-52 |
| 代码生成 | kapt(router APT、voice APT、SettingsLib 注解处理器) | app/build.gradle:4,127,135 |
| 代码质量 | Checkstyle 8.12 + detekt 1.22.0 | checkstyle.gradle:17,5 |
| 发布 | JFrog Artifactory(小米内部源 pkgs.d.xiaomi.net) | build.gradle:139-162 |
1.4 三 Flavor 总览
项目用 flavorDimensions "miPlatform" 切出三套产物(app/build.gradle:11-25):
| Flavor | 含义 | 主要差异 |
|---|---|---|
dcd | 国内某平台(watt 体系,jar 名带 _watt 后缀) | framework.jar、car.jar 走 *_watt 版本;资源叠加 src/dcddif + src/region/cn |
xcd | 国内另一平台(jar 名带 _1129 后缀) | framework.jar、car.jar 走 xcd_* 版本;资源叠加 src/xcddif + src/region/cn |
global | 海外版本 | 复用 xcd 的 jar,但资源叠加 src/xcddif + src/region/global,且换用独立 AndroidManifest |
📌 新手记住:dcd / xcd 不是「debug/release」,而是两套不同的车机平台,对应不同硬件平台编译出的 framework.jar / car.jar。
global在二进制层面复用 xcd 平台 jar,但在资源和 Manifest 上完全走海外分支。
二、四层模块拓扑
2.1 目录物理结构
仓库根目录下与代码相关的 4 个一级目录正好对应 4 个层次(其余如 buildSrc/、docs/、scripts/、ci_build/、.micode/ 属于工程基础设施):
MiCarSettings/
├── app/ # 第 1 层:应用入口(application 模块)
├── base/ # 第 2 层:基础库(library 模块集合)
│ ├── settingsBaseUi/ # UI 基础组件(含 colorpick-plugin)
│ ├── settingsBaseLib/ # 核心基础库
│ ├── settingsBasePB/ # protobuf 相关
│ ├── settingsVehicleLib/ # 车辆 Car* 接口封装
│ ├── settingsLibAndroid/ # AOSP SettingsLib 移植
│ ├── settingsLibUtils/ # 工具库
│ ├── settingsLibAdaptiveIcon/ # 自适应图标
│ ├── settingsLibTile/ # Tile 相关
│ ├── settingsLibDisplayUtils/ # 显示工具
│ ├── iconloaderlib/ # 图标加载
│ └── WifiTrackerLib/ # WiFi 追踪(AOSP 移植)
├── settingsPage/ # 第 3 层:功能页面(library 模块集合,按业务垂直切分)
│ ├── micarChargeSettings/ # 充电
│ ├── micarDisplaySettings/ # 显示
│ ├── micarLightSettings/ # 灯光
│ ├── micarLockSettings/ # 锁车
│ ├── micarDrivingSettings/ # 驾驶
│ ├── micarMedia/ # 媒体
│ ├── micarVolume/ # 音量
│ ├── micarIotSettings/ # IoT
│ ├── micarConnectionSettings/ # 连接(WiFi/蓝牙)
│ ├── micarSafetyServiceSettings/ # 安全服务
│ ├── settingsAutoPilot/ # 自动驾驶
│ ├── settingsSystem/ # 系统
│ ├── xiaoAiSettings/ # 小爱
│ ├── micarSnapshot/ # 快照
│ ├── VehicleBodyControl/ # 车身控制
│ └── globalOnly/ # 海外专属页面(仅 global flavor 依赖)
└── settingsCommon/ # 第 4 层:横切关注点(路由/语音/质量工具/checkstyle 配置)
├── plugin/router/ # 路由框架(annotation + routerApt + routerManager)
├── plugin/voice/ # 语音搜索(voicesearchannotation + voicesearchapt)
├── rtiLib/ # RTI 库
├── tools/qualitytools/ # 质量工具(debugImplementation)
├── tools/checkstyle/ # checkstyle.xml / detekt.yml / suppression
├── tools/out/target/... # ⚠️ 平台预编译 jar(framework/car/wifi-tracker)
└── libs/ # 第三方 jar
2.2 应用入口类(位于 app 模块)
| 类 | 路径 | 角色 |
|---|---|---|
SettingsApplication | app/src/main/java/com/android/car/settings/miauto/SettingsApplication.java | 应用入口,被 @Module/@Modules 注解标记,由 router APT 生成模块映射;负责 DfxManager、Logger 初始化 |
BaseCarSettingsActivity | app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java | 所有设置页面的 Activity 基类(AOSP 继承) |
SubSettingsActivity | app/src/main/java/com/android/car/settings/common/SubSettingsActivity.java | 二级页面容器 Activity |
DeeplinkActivity | app/src/main/java/com/android/car/settings/miauto/common/DeeplinkActivity.kt | 深链/语音入口 Activity,接收外部 Intent 再分发 |
CarSettingActivities.HomepageActivity | app/src/main/java/com/android/car/settings/common/CarSettingActivities.java | 首页 Activity |
FallbackHome | app/src/main/java/com/android/car/settings/FallbackHome.java | 系统启动期间的兜底首页 |
2.3 依赖方向(Mermaid)
graph TD APP["app<br/>(application: SettingsApplication / Activities)"] PAGE["settingsPage/*<br/>(16 个功能页面 library)"] BASE["base/*<br/>(settingsBaseUi / settingsBaseLib / settingsVehicleLib / ...)"] COMMON["settingsCommon/*<br/>(router / voice / rtiLib / qualitytools)"] PLATFORM["平台产物 jar<br/>(framework / car / wifi-tracker)<br/>settingsCommon/tools/out/target/"] APP -->|implementation project| PAGE APP -->|implementation project| BASE APP -->|kapt project| COMMON APP -.->|globalImplementation| PGO["settingsPage:globalOnly"] PAGE -->|implementation/api project| BASE PAGE -->|kapt project| COMMON BASE -->|kapt project| COMMON COMMON -->|compileOnly files| PLATFORM BASE -->|compileOnly files| PLATFORM PAGE -->|compileOnly files| PLATFORM APP -->|implementation| PAGE classDef layer fill:#eef,stroke:#446,stroke-width:1px classDef ext fill:#efe,stroke:#464,stroke-width:1px class APP,PAGE,BASE,COMMON layer class PLATFORM,PGO ext
关键纪律:
- 依赖严格自上而下:app → page → base,page 之间不互相依赖(业务隔离),所有 page 都可以依赖 base,但 base 绝不反向依赖 page。
settingsCommon是横切层,主要通过 kapt 注解处理器(router / voice)被上层使用,自身不承载业务。settingsPage/globalOnly是唯一被 app 以globalImplementation引用的 page 模块(app/build.gradle:168),意味着 dcd/xcd 产物里不会包含海外专属代码。
📌 新手记住:拿到一个新需求先问「它属于哪一层」。一个新页面 →
settingsPage/<xxx>Settings;一个跨页面共用的 UI 组件 →base/settingsBaseUi;一个车辆属性读取 →base/settingsVehicleLib;一个需要被路由/语音识别到的东西 → 注解写在 page 里,APT 在settingsCommon里生成代码。
三、settings.gradle 如何组织模块
3.1 include 列表
settings.gradle 用一组 include 把所有模块注册进 Gradle 构建图。源码在 settings.gradle:60-98:
// settings.gradle:60-98 (节选)
include ":settingsPage:xiaoAiSettings"
include ":base:settingsBaseUi"
include ":base:settingsBaseLib"
include ":base:settingsVehicleLib"
include ":settingsPage:settingsAutoPilot"
include ":base:settingsBasePB"
include ":settingsPage:micarIotSettings"
// ... 其余 settingsPage 模块 ...
include "${MiCarSettingsCommon_LibProjectName}:tools:qualitytools" // :settingsCommon:tools:qualitytools
include "${MiCarSettingsCommon_LibProjectName}:rtiLib"
include ':settingsCommon:plugin:voice:voicesearchapt'
include ':settingsCommon:plugin:voice:voicesearchannotation'
include ':settingsCommon:plugin:router:annotation'
include ':settingsCommon:plugin:router:routerApt'
include ':settingsCommon:plugin:router:routerManager'
include ':base:settingsLibAndroid'
include ':app' // app 模块放在靠后位置
include ':base:settingsLibUtils'
// ... 其它 base lib ...
include ':settingsPage:globalOnly'其中 ${MiCarSettingsCommon_LibProjectName} 在 gradle.properties:35 被定义为 :settingsCommon,所以 ${...}:tools:qualitytools 实际就是 :settingsCommon:tools:qualitytools。
3.2 几个值得注意的设计
rootProject.name被注释掉了(settings.gradle:60//rootProject.name = 'MiCarSettings')。Gradle 会用仓库目录名作为rootProject.name,也就是MiCarSettings。这个细节很关键,因为checkstyle.gradle和constants.gradle都用rootProject.name来判断当前是否在主工程上下文(见后文)。- pluginManagement 优先小米内网源:
settings.gradle:17-58把小米 Artifactory 的gradle-plugins-remote、maven-snapshot-virtual等放最前面,确保内网插件优先解析。 - include 顺序不影响构建顺序,构建顺序由依赖图决定(app 依赖谁,谁就先编)。
📌 新手记住:新增一个模块,必须先在
settings.gradle加一行include,否则 Gradle 根本看不到它。删一个模块,除了删目录,也要回来删这一行,否则构建会因为找不到目录而报错。
四、完整构建流程
4.1 一行命令背后发生了什么
当你执行 ./gradlew :app:assembleXcdDebug 时,Gradle 实际经历了下面这条流水线:
flowchart TD A["1. gradlew 启动<br/>读取 gradle.properties / jvmargs=-Xmx4096m"] --> B["2. 解析 settings.gradle<br/>注册所有 include 模块"] B --> C["3. 配置根 build.gradle<br/>apply dependencies.gradle / constants.gradle"] C --> D["4. buildscript classpath 装载<br/>AGP 7.2.0 / Kotlin 1.8.10 / detekt 1.22.0 / protobuf / jfrog"] D --> E["5. 对每个子项目执行<br/>allprojects 钩子<br/>① apply checkstyle.gradle<br/>② 配置 repositories"] E --> F["6. loadBuildConfig<br/>读 project.properties<br/>注入 versionName/versionCode"] F --> G["7. 各子项目配置阶段<br/>读各自 build.gradle<br/>注册 com.android.application/library"] G --> H["8. afterEvaluate 钩子<br/>dependencies.gradle 自动注入<br/>dcdCompileOnly/xcdCompileOnly/..."] H --> I["9. gradle.projectsEvaluated<br/>把 framework.jar 前置到<br/>JavaCompile.bootstrapClasspath"] I --> J["10. preBuild<br/>触发 installHook:<br/>copyPrePushHooks1/2/3"] J --> K["11. checkStyleCode 可选<br/>javaCheckstyle + detekt"] K --> L["12. assembleXcdDebug<br/>编译 Java/Kotlin → 处理资源 → 打包 APK"] L --> M["13. 签名<br/>platform.keystore<br/>android.uid.system"] M --> N["14. 输出<br/>app/build/outputs/apk/xcd/debug/app-xcd-debug.apk"] classDef phase fill:#eef,stroke:#446 classDef key fill:#fee,stroke:#944,stroke-width:2px class H,I,K key class A,B,C,D,E,F,G,J,L,M,N phase
下面逐段拆解其中容易踩坑的环节。
4.2 根 build.gradle 的两个 apply
build.gradle:19-20 在 buildscript 之外先 apply 了两个脚本,它们的作用是把所有「与子项目无关的全局变量」先挂到 rootProject.ext:
// build.gradle:19-20
apply from: 'dependencies.gradle' // 定义 afterEvaluate { } 钩子,自动注入平台 jar
apply from: 'constants.gradle' // 定义 rootProject.ext.jarLib / SDK 版本等常量注意这两个 apply 必须在 buildscript { } 之外、allprojects { } 之前,因为 allprojects 钩子里会用 rootProject.ext 的值。
4.3 allprojects 钩子:checkstyle 全量应用 + 版本号注入
build.gradle:65-137 的 allprojects { configProject -> ... } 是每一个子项目(包括 rootProject 自己)都会执行的配置块,做三件事:
// build.gradle:65-93 (精简)
allprojects { configProject ->
// ① 给所有子项目(含根)应用 checkstyle.gradle
apply from: rootProject.file("checkstyle.gradle")
// ② 给所有子项目(含根)配置 repositories(小米内网源 + google + jcenter)
repositories { ... }
// ③ 只在根项目上执行一次 loadBuildConfig()
if (rootProject == configProject)
loadBuildConfig()
}loadBuildConfig()(build.gradle:164-222)的逻辑是:优先读 Jenkins 注入的环境变量 GLOBAL_APP_VERSION_NAME/CODE,没有就 fallback 到 project.properties:
# project.properties (真实内容)
versionName=1.0.0.165-dev
versionCode=2026063001最终把 versionName / versionCode / artifactory_user / artifactory_password / autoBindDisable 挂到 rootProject.ext,供所有子模块的 defaultConfig 引用(如 app/build.gradle:47-48 的 versionCode rootProject.ext.versionCode)。
4.4 gradle.projectsEvaluated:framework.jar 前置到 bootstrapClasspath
这是最容易踩坑、也最容易被新人忽略的一段。build.gradle:100-136 在所有子项目配置完成后,给所有 JavaCompile 任务插了一段编译参数:
// build.gradle:100-136 (精简,带中文注释)
gradle.projectsEvaluated {
// 从命令行 task 名里嗅探当前 flavor
def currentFlavor = {
def taskNames = gradle.startParameter.taskNames
for (taskName in taskNames) {
if (taskName.toLowerCase().contains("assemble")) return taskName
}
return ""
}()
tasks.withType(JavaCompile) {
options.compilerArgs << '-Xlint:-deprecation' << '-Xlint:-unchecked'
if (options.bootstrapClasspath != null) {
Set<File> fileSet = options.bootstrapClasspath.getFiles()
List<File> newFileList = new ArrayList<>()
// dcd 用 watt framework.jar,xcd/global 用 xcd framework.jar
if (currentFlavor.contains("Dcd")) {
newFileList.add(new File(rootProject.ext.jarLib.DCD_FRAMEWORK_JARPATH))
newFileList.addAll(fileSet)
options.bootstrapClasspath = files(newFileList.toArray())
} else if (currentFlavor.contains("Xcd") || currentFlavor.contains("Global")) {
newFileList.add(new File(rootProject.ext.jarLib.XCD_FRAMEWORK_JARPATH))
newFileList.addAll(fileSet)
options.bootstrapClasspath = files(newFileList.toArray())
}
}
}
}为什么需要这一段? 因为 MiCarSettings 调用了大量车机平台定制过的 android.* / android.car.* 隐藏 API,这些 API 不在公开 SDK 里,而是在 settingsCommon/tools/out/target/.../framework_intermediates/*.jar 这种「平台预编译 jar」里。把 platform framework.jar 前置到 bootstrapClasspath(注意是前置、不是 append),才能让 javac 优先从平台 framework 里解析类符号,而不是从 android.jar(公开 SDK)里找不到就报错。
这也解释了为什么第一次拉代码后必须先有
settingsCommon/tools/out/下的平台产物——否则constants.gradle里指向的 jar 路径不存在,编译期cannot find symbol会铺天盖地。
4.5 dependencies.gradle:afterEvaluate 自动注入三套平台 jar
如果说上一节解决的是「编译时类符号从哪来」,dependencies.gradle 解决的就是「不同 flavor 用哪一版 jar」。它通过 allprojects.afterEvaluate 钩子,给每一个 Android 子项目自动添加依赖,子模块自己的 build.gradle 完全不用写:
// dependencies.gradle:1-54 (精简,带中文注释)
allprojects {
afterEvaluate { project ->
// 只对 Android 项目生效,跳过 java-library(如 router annotation)
if (project.hasProperty("android") && !project.plugins.hasPlugin("java-library")) {
if (!new File(project.projectDir, "build.gradle").exists()) return
println "Auto Configure project(${project.name})"
def deps = project.dependencies
// ① framework.jar:三套 flavor 各自的路径
deps.add("xcdCompileOnly", files(rootProject.xcd_framework_lib_path))
deps.add("globalCompileOnly", files(rootProject.xcd_framework_lib_path))
deps.add("dcdCompileOnly", files(rootProject.dcd_framework_lib_path))
// ② car.jar(android.car_*)
deps.add("xcdCompileOnly", files(rootProject.xcd_car_ui_lib_path))
deps.add("globalCompileOnly", files(rootProject.xcd_car_ui_lib_path))
deps.add("dcdCompileOnly", files(rootProject.dcd_car_ui_lib_path))
// ③ wifi-tracker jar(dcd 用 _8295,xcd/global 用 _1129)
deps.add("dcdCompileOnly", files(rootProject.dcd_wifi_trackerLib_path))
deps.add("globalCompileOnly", files(rootProject.xcd_wifi_trackerLib_path))
deps.add("xcdCompileOnly", files(rootProject.xcd_wifi_trackerLib_path))
// ④ 全 flavor 共用的 compileOnly
deps.add("compileOnly", files(rootProject.car_setup_wizard_lib_utils_path))
deps.add("compileOnly", rootProject.CarUserLib)
deps.add("implementation", 'com.mi.car:dfx_sdk:0.0.6')
// ⑤ xcd/global 额外的 androidx + CarHelperLib
deps.add("xcdCompileOnly", 'androidx.fragment:fragment:1.2.0')
deps.add("xcdCompileOnly", 'androidx.tracing:tracing:1.0.0')
deps.add("xcdCompileOnly", 'androidx.preference:preference:1.1.0')
deps.add("xcdCompileOnly", files(rootProject.xcd_CarHelperLib))
// global 同上……
// ⑥ detekt 代码质量插件
deps.add("detektPlugins", rootProject.detektFormatting)
deps.add("detektPlugins", rootProject.detektRules)
}
}
}自动注入的好处:你新加一个 library 模块,只要它有 build.gradle 且 apply 了 com.android.library,平台 jar、car.jar、wifi-tracker jar、dfx_sdk、detekt 插件就全自动挂上了——不需要在新模块的 dependencies { } 里重复写一遍。
4.6 平台 jar 路径(constants.gradle)
constants.gradle 把所有平台 jar 的路径集中管理。注意路径都基于 settingsCommon/tools/out/target/common/obj/JAVA_LIBRARIES/:
| ext 变量 | 实际路径(相对仓库根) | 用途 |
|---|---|---|
DCD_FRAMEWORK_JARPATH | settingsCommon/tools/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/javalib_watt_250721.jar | dcd 平台 framework |
XCD_FRAMEWORK_JARPATH | settingsCommon/tools/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/xcd_javalib_1129.jar | xcd/global 平台 framework |
dcd_car_ui_lib_path | .../android.car_intermediates/android.car_watt0911.jar | dcd 平台 car.jar |
xcd_car_ui_lib_path | .../android.car_intermediates/xcd_android.car_1129.jar | xcd/global 平台 car.jar |
dcd_wifi_trackerLib_path | .../WifiTrackerLib_intermediates/classes_8295.jar | dcd WiFi 追踪 |
xcd_wifi_trackerLib_path | .../WifiTrackerLib_intermediates/xcd_wifi_tracker_1129.jar | xcd/global WiFi 追踪 |
car_setup_wizard_lib_utils_path | .../car-setup-wizard-lib-utils_intermediates/classes_8295.jar | 共用 setupwizard |
xcd_CarHelperLib | .../car-helper-lib_intermediates/xcd_car_help_lib_1129.jar | xcd/global CarHelper |
文件名里的
_watt/_1129/_8295后缀是各平台构建批次的日期/代号标记,升级平台产物时这些文件名会变,所以要改constants.gradle,而不是去各子模块改。
4.7 依赖注入流程图
flowchart LR subgraph CONST["constants.gradle<br/>(构建前已 apply)"] R[rootProject.ext.jarLib.*<br/>DCD_FRAMEWORK_JARPATH<br/>XCD_FRAMEWORK_JARPATH<br/>...] end subgraph AE["dependencies.gradle<br/>allprojects.afterEvaluate"] CHECK{"project 有 android 属性?<br/>非 java-library?<br/>有 build.gradle?"} D1["deps.add('dcdCompileOnly',<br/>files(dcd_framework_lib_path))"] D2["deps.add('xcdCompileOnly',<br/>files(xcd_framework_lib_path))"] D3["deps.add('globalCompileOnly',<br/>files(xcd_framework_lib_path))"] D4["deps.add('compileOnly',<br/>CarUserLib / setupwizard)"] D5["deps.add('implementation',<br/>dfx_sdk)"] D6["deps.add('detektPlugins',<br/>formatting + rules)"] end subgraph PE["build.gradle<br/>gradle.projectsEvaluated"] B1["嗅探 task 名里的 flavor"] B2["把对应 framework.jar<br/>前置到 JavaCompile.bootstrapClasspath"] end subgraph MOD["每个 Android 子模块<br/>(app / base/* / settingsPage/*)"] M1["自己写的 dependencies {}"] M2[自动注入的 dcd/xcd/globalCompileOnly] M3[bootstrapClasspath 前置] end R --> CHECK CHECK -->|是| D1 CHECK -->|是| D2 CHECK -->|是| D3 CHECK -->|是| D4 CHECK -->|是| D5 CHECK -->|是| D6 D1 & D2 & D3 --> M2 B1 --> B2 --> M3 M1 --> MOD
📌 新手记住:第一次构建报
cannot find symbol android.xxx.HiddenApi,99% 是settingsCommon/tools/out/target/下缺平台 jar。问平台组的同事要一份最新的产物放进去,或者跑平台那边的make把这几个 jar 产物同步过来。
五、三 Flavor 机制详解
5.1 Flavor 配置
app/build.gradle:11-25 定义了 flavor 维度和三个变体:
// app/build.gradle:11-25
flavorDimensions "miPlatform"
productFlavors {
dcd { dimension "miPlatform" }
xcd { dimension "miPlatform" }
global { dimension "miPlatform" }
}所有 base/page 子模块(如 settingsPage/micarLightSettings/build.gradle:10-24)都声明同样的 flavorDimensions 和三个 productFlavors,以保证 app 和 library 的 flavor 一一匹配(Gradle 称为 flavor matching)。匹配错会导致 Variant 'xxx' has no matching library variant 报错。
5.2 sourceSet 映射
每个 flavor 通过 sourceSets 声明「在 main 的基础上叠加哪些目录」。app 模块的配置(app/build.gradle:60-88)特别能说明问题:
// app/build.gradle:60-88 (精简,带中文注释)
sourceSets {
main { /* 公共:src/main/{java,res,aidl} */ }
dcd {
java.srcDirs = ['src/main/java', 'src/dcddif/java'] // dcd 专属代码
res.srcDirs = ['src/main/res', 'src/dcddif/res', 'src/region/cn/res'] // 国内资源
aidl.srcDirs = ['src/main/aidl', 'src/dcddif/aidl']
}
xcd {
java.srcDirs = ['src/main/java', 'src/xcddif/java'] // xcd 专属代码
res.srcDirs = ['src/main/res', 'src/xcddif/res', 'src/region/cn/res'] // 国内资源
aidl.srcDirs = ['src/main/aidl', 'src/xcddif/aidl']
}
global {
manifest.srcFile 'src/region/global/AndroidManifest.xml' // 海外独立 Manifest
java.srcDirs = ['src/main/java', 'src/xcddif/java', 'src/region/global/java']
res.srcDirs = ['src/main/res', 'src/xcddif/res', 'src/region/global/res']
aidl.srcDirs = ['src/main/aidl', 'src/xcddif/aidl']
}
}页面模块(以 settingsPage/micarLightSettings/build.gradle 为例)的 global 用的是 src/global/,而不是 src/region/global/,注意 app 和 page 模块的目录命名略有差异。
5.3 sourceSet 叠加图
flowchart TD subgraph MAIN["src/main/ (所有 flavor 共用)"] M1[java / res / aidl] end subgraph DCD["dcd flavor"] D1[src/dcddif/java] D2[src/dcddif/res] D3[src/region/cn/res] end subgraph XCD["xcd flavor"] X1[src/xcddif/java] X2[src/xcddif/res] X3[src/region/cn/res] end subgraph GBL["global flavor"] G1[src/region/global/AndroidManifest.xml<br/>替换 main 的 Manifest] G2[src/region/global/java] G3[src/region/global/res] end OUT_DCD["app-dcd.apk"] OUT_XCD["app-xcd.apk"] OUT_GBL["app-global.apk"] MAIN --> OUT_DCD MAIN --> OUT_XCD MAIN --> OUT_GBL D1 & D2 & D3 --> OUT_DCD X1 & X2 & X3 --> OUT_XCD X1 & X2 -.复用.-> OUT_GBL G1 & G2 & G3 --> OUT_GBL classDef common fill:#eef,stroke:#446 classDef dcd fill:#fef,stroke:#646 classDef xcd fill:#eff,stroke:#466 classDef gbl fill:#ffe,stroke:#664 class M1 common class D1,D2,D3 dcd class X1,X2,X3 xcd class G1,G2,G3 gbl
关键观察:
dcd/xcd各有专属的dcddif/xcddif代码目录,但都共用src/region/cn/res(国内资源)。global复用xcddif的代码(因为海外和 xcd 共平台),但额外叠加src/region/global/的代码和资源,并且替换掉 AndroidManifest。globalOnly模块(settingsPage/globalOnly)只在 app 的 global flavor 下被依赖(app/build.gradle:168globalImplementation project(":settingsPage:globalOnly")),所以 dcd/xcd 包里完全没有海外业务代码。
5.4 Flavor 对比表
| 维度 | dcd | xcd | global |
|---|---|---|---|
| 平台 | 国内 watt 平台 | 国内另一平台 | 海外(复用 xcd 平台) |
| framework.jar | javalib_watt_250721.jar | xcd_javalib_1129.jar | xcd_javalib_1129.jar |
| car.jar | android.car_watt0911.jar | xcd_android.car_1129.jar | xcd_android.car_1129.jar |
| wifi-tracker jar | classes_8295.jar | xcd_wifi_tracker_1129.jar | xcd_wifi_tracker_1129.jar |
| 专属代码目录 | src/dcddif/ | src/xcddif/ | src/xcddif/ + src/region/global/ |
| 专属资源目录 | src/dcddif/res + src/region/cn/res | src/xcddif/res + src/region/cn/res | src/xcddif/res + src/region/global/res |
| AndroidManifest | src/main/AndroidManifest.xml | src/main/AndroidManifest.xml | src/region/global/AndroidManifest.xml(独立) |
| 含 globalOnly 模块 | 否 | 否 | 是 |
| 构建产物 | app-dcd-{debug,release}.apk | app-xcd-{debug,release}.apk | app-global-{debug,release}.apk |
📌 新手记住:要加一个「只在国内某平台出现」的功能,往
src/dcddif/或src/xcddif/放;「国内通用、海外不要」的功能往src/region/cn/放;「海外专属」的功能往src/region/global/放或者直接进settingsPage/globalOnly模块。「所有平台都要」就老老实实放src/main/。
六、签名机制
6.1 平台签名
app/build.gradle:26-39 的 signingConfigs 把 debug 和 release 都指向同一个 platform.keystore:
// app/build.gradle:26-39
signingConfigs {
debug {
storeFile file('../platform.keystore') // 仓库根目录的 platform.keystore
storePassword 'android'
keyAlias 'platform_key'
keyPassword 'android'
}
release {
storeFile file('../platform.keystore')
storePassword 'android'
keyAlias 'platform_key'
keyPassword 'android'
}
}platform.keystore 真实存在于仓库根目录(/home/zbc/car/MiCarSettings/platform.keystore,2915 字节)。这是 AOSP 系统的 platform 签名密钥——所有声明 android:sharedUserId="android.uid.system" 的应用都必须用这个 key 签名,否则系统拒绝安装或拒绝提升到 system uid。
6.2 为什么必须用平台签名
app/src/main/AndroidManifest.xml 头部声明:
<!-- app/src/main/AndroidManifest.xml (关键属性) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.settings"
coreApp="true"
android:sharedUserId="android.uid.system">coreApp="true":核心应用,系统启动早期就能跑。android:sharedUserId="android.uid.system":和 system_server 共享 uid,从而获得所有signature|system级别的权限(MODIFY_PHONE_STATE、DUMP、CONTROL_DISPLAY_UNITS等大量系统权限)。
这两个特权只有用 platform.keystore 签名才生效。这也是 MiCarSettings 能直接调用车机隐藏 API、Car 服务、修改系统设置的根本原因。
📌 新手记住:本地 debug 包打出来直接装到车机上就行,因为它和系统共享 platform 签名。但如果你换了一台没有把这个应用预置签名白名单的车机,可能装不上——这是平台签名机制的约束,不是你代码的问题。
七、依赖体系
7.1 三套 framework.jar / car.jar 的差异
| 依赖项 | dcd 注入到 | xcd 注入到 | global 注入到 |
|---|---|---|---|
| framework.jar | dcdCompileOnly (dcd_framework_lib_path = watt) | xcdCompileOnly (xcd_framework_lib_path = 1129) | globalCompileOnly (xcd_framework_lib_path = 1129) |
| car.jar | dcdCompileOnly (watt) | xcdCompileOnly (1129) | globalCompileOnly (1129) |
| wifi-tracker | dcdCompileOnly (classes_8295) | xcdCompileOnly (xcd_1129) | globalCompileOnly (xcd_1129) |
| CarHelperLib | 不注入 | xcdCompileOnly | globalCompileOnly |
| setupwizard | compileOnly(全 flavor 共用) | compileOnly | compileOnly |
| CarUserLib (maven) | compileOnly | compileOnly | compileOnly |
| dfx_sdk 0.0.6 | implementation | implementation | implementation |
7.2 为什么用 compileOnly 而不是 implementation
所有平台 jar 都用 xxxCompileOnly,意味着这些 jar 只在编译期可见、不打进 APK。原因:这些类在车机系统镜像里已经存在(/system/framework/framework.jar 等),运行时由系统 classloader 提供。如果再 implementation 打进去,会和系统版本冲突,引发 ClassCastException 或 LinkageError。
7.3 app 的实现依赖(节选)
app/build.gradle:114-170 列出了 app 真正 implementation 的模块:
// app/build.gradle:114-170 (节选)
dependencies {
kapt files('settingsCommon/libs/settings_support/libs/SettingsLib-annotation-processor.jar')
implementation("com.xiaomi.phonecarlink:complex:0.1.40")
// 汽车问答(语音搜索 APT)
kapt project(":settingsCommon:plugin:voice:voicesearchapt")
// 路由 APT
kapt project(":settingsCommon:plugin:router:routerApt")
// 质量工具(仅 debug)
debugImplementation project(":settingsCommon:tools:qualitytools")
// UI 基础
implementation project(":base:settingsBaseUi")
implementation project(":base:settingsVehicleLib")
implementation project(":base:settingsBaseLib")
implementation project(":base:settingsLibAndroid")
implementation project(":base:settingsLibTile")
// 全部功能页面
implementation project(":settingsPage:settingsSystem")
implementation project(":settingsPage:micarChargeSettings")
implementation project(":settingsPage:micarConnectionSettings")
// ... 15 个 page 模块 ...
globalImplementation project(":settingsPage:globalOnly") // 仅 global flavor
}注意几个关键点:
- kapt 而非 implementation 用于 APT 模块(router/voice),因为它们是注解处理器,运行时不需要。
globalImplementation是 Gradle 自动为 global flavor 创建的依赖配置(<flavor>Implementation),仅 global 包含 globalOnly。debugImplementation让 qualitytools 只在 debug 包里出现,release 包瘦身。
📌 新手记住:在 page 模块里写代码,需要用到隐藏 API 时不用自己在
dependencies { }里加 framework.jar——dependencies.gradle已经自动注入了。直接import android.xxx.HiddenApi编译就行,前提是settingsCommon/tools/out/下的 jar 是最新的。
八、代码质量:checkstyle + detekt
8.1 配置位置
| 文件 | 路径 | 作用 |
|---|---|---|
checkstyle.gradle | 仓库根目录 | 应用到所有子项目的 checkstyle + detekt 任务定义 |
checkstyle.xml | settingsCommon/tools/checkstyle/checkstyle.xml | Java 代码规范规则 |
checkstyle-suppression.xml | settingsCommon/tools/checkstyle/checkstyle-suppression.xml | checkstyle 豁免清单 |
detekt.yml | settingsCommon/tools/checkstyle/detekt.yml | Kotlin 代码规范规则 |
8.2 三个任务
checkstyle.gradle:22-93 定义了三个任务,依赖关系如下:
flowchart LR CSC["checkStyleCode<br/>(group: build)"] JC["javaCheckstyle<br/>(Checkstyle, Java)"] DK["detekt<br/>(Kotlin)"] XML["checkstyle.xml<br/>+ suppression"] YML["detekt.yml"] CSC --> JC CSC --> DK XML --> JC YML --> DK JC -->|扫描 src/ 或<br/>-PchangedJavaFiles| OUT1[违规报告] DK -->|扫描 src/ 或<br/>-PchangedKotlinFiles| OUT2[违规报告]
javaCheckstyle:扫**/*.java,排除gen/、test/、build/、pb/、R.java、BuildConfig.java。detekt:扫 Kotlin + Java 源码(含 test),输出 html + txt 报告。checkStyleCode:聚合任务,dependsOn javaCheckstyle + detekt,是日常用的入口。
8.3 增量检查
checkstyle.gradle:64-87 的 resolveChangedFiles(propName) 支持只检查命令行传入的文件列表:
# 只检查改动的 Java 文件
./gradlew javaCheckstyle -PchangedJavaFiles="$(git diff --name-only HEAD | grep '\.java$' | tr '\n' ';')"
# 只检查改动的 Kotlin 文件
./gradlew detekt -PchangedKotlinFiles="$(git diff --name-only HEAD | grep '\.kt$' | tr '\n' ';')"这是 CI 上提速的关键机制:不传 -P 就全量扫 src/;传了就只扫给定文件清单。
8.4 在哪应用
build.gradle:67 的 allprojects { apply from: rootProject.file("checkstyle.gradle") } 把 checkstyle 应用到包括根项目在内的所有项目。这意味着每新增一个模块,checkstyle/detekt 就自动生效,不需要写任何配置。
📌 新手记住:本地跑
./gradlew checkStyleCode提交前自检;CI 上一定会有这两道关卡,挂了就过不去。detekt.yml里关掉了CommentOverPrivateProperty,但ComplexCondition(阈值 5)、LongParameterList等是开的——参数超过 5 个、条件嵌套超过 5 层都会报。
九、installHook 与提交门禁
9.1 installHook.gradle 的设计
app/build.gradle:7 apply 了 app/installHook.gradle,它定义了三个 Copy 任务(installHook.gradle:1-18):
// app/installHook.gradle
task copyPrePushHooks1(type: Copy) {
from file("hooks/pre-push")
into file(".git/hooks")
}
task copyPrePushHooks2(type: Copy) {
from file("hooks/pre-push")
into file("../MiCarSettings/.git/hooks")
}
task copyPrePushHooks3(type: Copy) {
from file("hooks/pre-push")
into file("../settingsCommon/.git/hooks")
}
preBuild.dependsOn copyPrePushHooks1
preBuild.dependsOn copyPrePushHooks2
preBuild.dependsOn copyPrePushHooks3意图:每次 preBuild 阶段,把 app/hooks/pre-push 脚本复制到三个 git 仓库(app 子仓、主仓、settingsCommon 子仓)的 .git/hooks/pre-push,这样 git push 前会自动触发。
注意:实际的
pre-push脚本位于settingsCommon/tools/hooks/pre-push(app/hooks 目录在当前代码库中为空)。该脚本内容是先跑./gradlew javaCheckstyle,再跑./gradlew detekt,任一失败就exit非零,阻断 push。
9.2 PREUPLOAD.cfg(repo 工具层门禁)
仓库根目录 PREUPLOAD.cfg 是 Android repo 工具的提交门禁配置:
# PREUPLOAD.cfg
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES}
[Builtin Hooks]
commit_msg_changeid_field = true
commit_msg_test_field = true它在 repo upload(Gerrit 推送)阶段触发:
checkstyle_hook/ktlint_hook:再跑一次代码规范检查。commit_msg_changeid_field:强求 commit message 带Change-Id:(Gerrit 必需,用git commit -s自动加)。commit_msg_test_field:强求 commit message 带Test:字段。
9.3 门禁流水线图
sequenceDiagram participant Dev as 开发者 participant Git as 本地 Git participant Hook as pre-push hook<br/>(installHook 装的) participant Repo as repo upload<br/>(PREUPLOAD.cfg) participant Ger as Gerrit Review Dev->>Git: git commit -s -m "..." Dev->>Git: git push origin HEAD:refs/for/dev Git->>Hook: 触发 pre-push Hook->>Hook: ./gradlew javaCheckstyle Hook->>Hook: ./gradlew detekt alt 任一失败 Hook-->>Git: exit 非零, 阻断 push Git-->>Dev: push 被拒 else 全部通过 Hook-->>Git: exit 0 Git->>Repo: 推送到 Gerrit end Repo->>Repo: checkstyle.py / ktlint.py Repo->>Repo: 校验 Change-Id / Test 字段 Repo->>Ger: 通过则进入 Review 队列
📌 新手记住:提交前永远先
./gradlew checkStyleCode自检一次。git commit -s -m的-s必加(生成Signed-off-by和Change-Id),commit message 末尾必须带Test:字段说明自测情况,否则repo upload会被门禁拦下。
十、构建产物与发布
10.1 产物路径
flowchart TD CMD["./gradlew :app:assembleXcdDebug<br/>(或 dcd / global)"] CMD --> CFG["配置阶段<br/>读 project.properties<br/>versionCode/Name"] CFG --> DEP["依赖解析<br/>所有 library 模块 assembleXcdDebug"] DEP --> COMP["编译<br/>javac + kotlinc<br/>bootstrapClasspath 含平台 framework"] COMP --> RES["资源合并<br/>main + xcddif + region/cn"] RES --> DEX["dex (arm64-v8a only)"] DEX --> APK["打包 APK"] APK --> SIGN["签名<br/>platform.keystore"] SIGN --> OUT["app/build/outputs/apk/xcd/debug/app-xcd-debug.apk"] CMD2["./gradlew :app:artifactoryPublish<br/>(或 updateApkAssembleXcdRelease)"] CMD2 --> REL["读 task 名嗅探 flavor"] REL --> ASS["dependsOn assembleXcdRelease"] ASS --> PUB["publishing.publications.xcdApp<br/>groupId=com.mi.car<br/>artifactId=MiCarSettings<br/>version=1.0.0.165-dev-xcd"] PUB --> ART["上传到<br/>pkgs.d.xiaomi.net/artifactory/releases"] classDef out fill:#efe,stroke:#464 class OUT,ART out
10.2 发布任务
build.gradle:235-242 定义了三个发布快捷任务:
// build.gradle:235-242
task updateApkAssembleXcdRelease(dependsOn: ['app:artifactoryPublish']) {}
task updateApkAssembleDcdRelease(dependsOn: ['app:artifactoryPublish']) {}
task updateApkAssembleGlobalRelease(dependsOn: ['app:artifactoryPublish']) {}app/build.gradle:172-219 的 artifactoryPublish 会从 gradle.startParameter.taskNames 嗅探当前 flavor,自动 dependsOn 对应的 assembleXxxRelease,并把对应 APK 作为 maven artifact 发布到小米 Artifactory(groupId=com.mi.car, artifactId=MiCarSettings, 三 flavor 各一个 -dcd/-xcd/-global 后缀)。
10.3 仓库根的便捷脚本
| 脚本 | 内容 | 用途 |
|---|---|---|
build.sh | . build/envsetup.sh; lunch watt-userdebug; make CarSettings | AOSP 源码树内编译(非 Gradle) |
project_publish_dcd_app.sh 等 | 调 Gradle 发布任务 | CI 内部用 |
注意 build.sh 是给 AOSP 全源码树场景用的(在 lunch watt-userdebug 后跑 make CarSettings),日常 Android Studio 开发不用它,直接 Gradle 即可。
📌 新手记住:日常开发用
./gradlew :app:assembleXcdDebug(或 Dcd/Global)。发版走updateApkAssembleXcdRelease一类任务,由 CI 完成。如果你只是想看页面效果,debug 包足够;release 包只在发版或验证 ProGuard 时才编。
十一、常见踩坑速查
| 现象 | 根因 | 解决 |
|---|---|---|
cannot find symbol android.xxx.Foo | settingsCommon/tools/out/target/ 下缺平台 jar 或 jar 过期 | 找平台组要最新产物覆盖;或确认 constants.gradle 路径与文件名匹配 |
Variant 'xxxDebug' has no matching library variant | 新加的 library 模块没声明 flavorDimensions / productFlavors | 复制任一 page 模块的 flavorDimensions + 三 flavor 块 |
Variant 报错且新模块没 dcddif/xcddif 目录 | sourceSets 引用了不存在的目录 | 在新模块下建好 src/main/、src/dcddif/、src/xcddif/、src/global/(至少建空目录) |
| push 被 hook 拒,提示 checkstyle 失败 | 代码不符合 checkstyle.xml | 本地 ./gradlew javaCheckstyle 看详情,按报告改 |
| push 被 hook 拒,提示 detekt 失败 | Kotlin 不符合 detekt.yml | 本地 ./gradlew detekt 看 html 报告 |
repo upload 被拦,提示缺 Change-Id | commit 时没加 -s 或 message 格式不对 | git commit --amend -s 补签名,重写 message 加 Change-Id: 和 Test: 字段 |
装不上车机,INSTALL_FAILED_SHARED_USER_INCOMPATIBLE | 包没用 platform 签名 | 确认 app/build.gradle 的 signingConfigs 指向 platform.keystore,且仓库根 keystore 未被替换 |
改了 constants.gradle 不生效 | Gradle 缓存 | ./gradlew --refresh-dependencies 或关掉 AS 重 sync |
全文总结
MiCarSettings 是一个源自 AOSP Car Settings、被小米深度定制为系统特权应用的「车机设置」中心。它的工程结构遵循严格的四层依赖纪律(app → settingsPage → base → settingsCommon),用 miPlatform 这一个 flavorDimension 切出 dcd / xcd / global 三套产物,对应国内两套车机平台 + 海外一套。所有平台定制的隐藏 API 通过 dependencies.gradle 的 afterEvaluate 自动注入到每个子模块的 dcdCompileOnly / xcdCompileOnly / globalCompileOnly,再由 gradle.projectsEvaluated 把 framework.jar 前置到 JavaCompile.bootstrapClasspath——这两段钩子是理解整个构建的钥匙。
掌握本篇后,你应该能:定位一个文件该放哪一层、知道为什么需要平台 jar、能在本地用一行 Gradle 命令编出 APK、并理解 push/上传时门禁为什么会对你的代码和 commit message 这么严格。下一篇《02-页面开发与 PreferenceController 范式》会带你深入到 page 模块内部,看一个具体设置页是怎么用 Fragment + PreferenceController 写出来的。