01 - 项目全景与构建体系

MiCarSettings 新人培训手册 · 第一篇 适用读者:刚接手 MiCarSettings、需要快速理解「项目是怎么搭起来、怎么编出来」的 Android 工程师


本篇概览

本篇从最宏观的视角拆解 MiCarSettings:它是什么、源头在哪、目录怎么分层、四层模块之间怎么互相依赖,以及一行 ./gradlew :app:assembleXcdDebug 命令背后 Gradle 实际做了哪些事。读完后你会建立起「打开这个仓库不再迷路」的全局心智模型,后续每一篇(页面开发、路由、车辆接口、语音等)都会挂在这个骨架上。

读完你能掌握

  1. MiCarSettings 的产品定位,以及它为什么「长得像 AOSP Car Settings」——能区分哪些代码继承自上游、哪些是小米自研增量。
  2. 仓库的四层模块拓扑(app / base / settingsPage / settingsCommon)以及它们之间的依赖方向,拿到一个新功能能立刻判断该往哪一层放。
  3. dcd / xcd / global 三套 Flavor 的差异、各自的 sourceSet 目录约定,以及为什么需要三套。
  4. 一行 ./gradlew :app:assembleXcdDebug 触发的完整构建链:Gradle 解析顺序、afterEvaluate 注入平台 jar、bootstrapClasspath 前置、checkstyle 全量拦截。
  5. 平台签名、平台 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.gradleapp/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.javaSubSettingsActivity.javaCarSettingActivities.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:46gradle/wrapper/gradle-wrapper.properties:4
主语言Kotlin 1.8.10 + Java 1.8build.gradle:22, 各模块 compileOptions
SDKminSdk 32 / targetSdk 32 / compileSdk 33 / buildTools 29.0.2constants.gradle:2-5
ABIarm64-v8a(车机平台)app/build.gradle:50-52
代码生成kapt(router APT、voice APT、SettingsLib 注解处理器)app/build.gradle:4,127,135
代码质量Checkstyle 8.12 + detekt 1.22.0checkstyle.gradle:17,5
发布JFrog Artifactory(小米内部源 pkgs.d.xiaomi.netbuild.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 模块)

路径角色
SettingsApplicationapp/src/main/java/com/android/car/settings/miauto/SettingsApplication.java应用入口,被 @Module/@Modules 注解标记,由 router APT 生成模块映射;负责 DfxManager、Logger 初始化
BaseCarSettingsActivityapp/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java所有设置页面的 Activity 基类(AOSP 继承)
SubSettingsActivityapp/src/main/java/com/android/car/settings/common/SubSettingsActivity.java二级页面容器 Activity
DeeplinkActivityapp/src/main/java/com/android/car/settings/miauto/common/DeeplinkActivity.kt深链/语音入口 Activity,接收外部 Intent 再分发
CarSettingActivities.HomepageActivityapp/src/main/java/com/android/car/settings/common/CarSettingActivities.java首页 Activity
FallbackHomeapp/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 几个值得注意的设计

  1. rootProject.name 被注释掉了settings.gradle:60 //rootProject.name = 'MiCarSettings')。Gradle 会用仓库目录名作为 rootProject.name,也就是 MiCarSettings。这个细节很关键,因为 checkstyle.gradleconstants.gradle 都用 rootProject.name 来判断当前是否在主工程上下文(见后文)。
  2. pluginManagement 优先小米内网源settings.gradle:17-58 把小米 Artifactory 的 gradle-plugins-remotemaven-snapshot-virtual 等放最前面,确保内网插件优先解析。
  3. 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-20buildscript 之外先 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-137allprojects { 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-48versionCode 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_JARPATHsettingsCommon/tools/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/javalib_watt_250721.jardcd 平台 framework
XCD_FRAMEWORK_JARPATHsettingsCommon/tools/out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/xcd_javalib_1129.jarxcd/global 平台 framework
dcd_car_ui_lib_path.../android.car_intermediates/android.car_watt0911.jardcd 平台 car.jar
xcd_car_ui_lib_path.../android.car_intermediates/xcd_android.car_1129.jarxcd/global 平台 car.jar
dcd_wifi_trackerLib_path.../WifiTrackerLib_intermediates/classes_8295.jardcd WiFi 追踪
xcd_wifi_trackerLib_path.../WifiTrackerLib_intermediates/xcd_wifi_tracker_1129.jarxcd/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.jarxcd/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

关键观察

  1. dcd / xcd 各有专属的 dcddif / xcddif 代码目录,但都共用 src/region/cn/res(国内资源)。
  2. global 复用 xcddif 的代码(因为海外和 xcd 共平台),但额外叠加 src/region/global/ 的代码和资源,并且替换掉 AndroidManifest
  3. globalOnly 模块(settingsPage/globalOnly)只在 app 的 global flavor 下被依赖(app/build.gradle:168 globalImplementation project(":settingsPage:globalOnly")),所以 dcd/xcd 包里完全没有海外业务代码

5.4 Flavor 对比表

维度dcdxcdglobal
平台国内 watt 平台国内另一平台海外(复用 xcd 平台)
framework.jarjavalib_watt_250721.jarxcd_javalib_1129.jarxcd_javalib_1129.jar
car.jarandroid.car_watt0911.jarxcd_android.car_1129.jarxcd_android.car_1129.jar
wifi-tracker jarclasses_8295.jarxcd_wifi_tracker_1129.jarxcd_wifi_tracker_1129.jar
专属代码目录src/dcddif/src/xcddif/src/xcddif/ + src/region/global/
专属资源目录src/dcddif/res + src/region/cn/ressrc/xcddif/res + src/region/cn/ressrc/xcddif/res + src/region/global/res
AndroidManifestsrc/main/AndroidManifest.xmlsrc/main/AndroidManifest.xmlsrc/region/global/AndroidManifest.xml(独立)
含 globalOnly 模块
构建产物app-dcd-{debug,release}.apkapp-xcd-{debug,release}.apkapp-global-{debug,release}.apk

📌 新手记住:要加一个「只在国内某平台出现」的功能,往 src/dcddif/src/xcddif/ 放;「国内通用、海外不要」的功能往 src/region/cn/ 放;「海外专属」的功能往 src/region/global/ 放或者直接进 settingsPage/globalOnly 模块。「所有平台都要」就老老实实放 src/main/


六、签名机制

6.1 平台签名

app/build.gradle:26-39signingConfigs 把 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_STATEDUMPCONTROL_DISPLAY_UNITS 等大量系统权限)。

这两个特权只有用 platform.keystore 签名才生效。这也是 MiCarSettings 能直接调用车机隐藏 API、Car 服务、修改系统设置的根本原因。

📌 新手记住:本地 debug 包打出来直接装到车机上就行,因为它和系统共享 platform 签名。但如果你换了一台没有把这个应用预置签名白名单的车机,可能装不上——这是平台签名机制的约束,不是你代码的问题。


七、依赖体系

7.1 三套 framework.jar / car.jar 的差异

依赖项dcd 注入到xcd 注入到global 注入到
framework.jardcdCompileOnly (dcd_framework_lib_path = watt)xcdCompileOnly (xcd_framework_lib_path = 1129)globalCompileOnly (xcd_framework_lib_path = 1129)
car.jardcdCompileOnly (watt)xcdCompileOnly (1129)globalCompileOnly (1129)
wifi-trackerdcdCompileOnly (classes_8295)xcdCompileOnly (xcd_1129)globalCompileOnly (xcd_1129)
CarHelperLib不注入xcdCompileOnlyglobalCompileOnly
setupwizardcompileOnly(全 flavor 共用)compileOnlycompileOnly
CarUserLib (maven)compileOnlycompileOnlycompileOnly
dfx_sdk 0.0.6implementationimplementationimplementation

7.2 为什么用 compileOnly 而不是 implementation

所有平台 jar 都用 xxxCompileOnly,意味着这些 jar 只在编译期可见、不打进 APK。原因:这些类在车机系统镜像里已经存在/system/framework/framework.jar 等),运行时由系统 classloader 提供。如果再 implementation 打进去,会和系统版本冲突,引发 ClassCastExceptionLinkageError

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.xmlsettingsCommon/tools/checkstyle/checkstyle.xmlJava 代码规范规则
checkstyle-suppression.xmlsettingsCommon/tools/checkstyle/checkstyle-suppression.xmlcheckstyle 豁免清单
detekt.ymlsettingsCommon/tools/checkstyle/detekt.ymlKotlin 代码规范规则

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.javaBuildConfig.java
  • detekt:扫 Kotlin + Java 源码(含 test),输出 html + txt 报告。
  • checkStyleCode:聚合任务,dependsOn javaCheckstyle + detekt,是日常用的入口。

8.3 增量检查

checkstyle.gradle:64-87resolveChangedFiles(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:67allprojects { 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-byChange-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-219artifactoryPublish 会从 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 CarSettingsAOSP 源码树内编译(非 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.FoosettingsCommon/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-Idcommit 时没加 -s 或 message 格式不对git commit --amend -s 补签名,重写 message 加 Change-Id:Test: 字段
装不上车机,INSTALL_FAILED_SHARED_USER_INCOMPATIBLE包没用 platform 签名确认 app/build.gradlesigningConfigs 指向 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.gradleafterEvaluate 自动注入到每个子模块的 dcdCompileOnly / xcdCompileOnly / globalCompileOnly,再由 gradle.projectsEvaluated 把 framework.jar 前置到 JavaCompile.bootstrapClasspath——这两段钩子是理解整个构建的钥匙。

掌握本篇后,你应该能:定位一个文件该放哪一层、知道为什么需要平台 jar、能在本地用一行 Gradle 命令编出 APK、并理解 push/上传时门禁为什么会对你的代码和 commit message 这么严格。下一篇《02-页面开发与 PreferenceController 范式》会带你深入到 page 模块内部,看一个具体设置页是怎么用 Fragment + PreferenceController 写出来的。