2. SystemUI源码结构:源码目录结构、关键模块介绍、编译与构建

好,咱们直接进入正题。上一章我们聊了SystemUI是什么,这一章我带你看看它的「骨架」——源码结构。

说实话,我第一次打开SystemUI的源码目录时,也有点懵。文件夹太多了,光顶层就有几十个。但别怕,摸清楚规律后,你会发现它其实很有条理。

2.1 源码目录结构

SystemUI的代码,主要躺在AOSP源码树的这个位置:

frameworks/base/packages/SystemUI/

嗯,就是这个路径。我当年找它花了好一会儿,因为一开始我总以为它在packages/apps/下面。实际上,它作为系统级应用,被放在了framework层。

我们看看顶层目录长什么样:

SystemUI/
├── src/                    # 核心Java/Kotlin源码
│   ├── com/android/systemui/
│   │   ├── statusbar/      # 状态栏相关
│   │   ├── navigationbar/  # 导航栏相关
│   │   ├── volume/         # 音量面板
│   │   ├── power/          # 电源菜单
│   │   ├── qs/             # 快捷设置面板
│   │   ├── screenshot/     # 截图功能
│   │   ├── tv/             # TV版专用
│   │   └── ...             # 其他模块
│   └── ...
├── res/                    # 资源文件
│   ├── layout/             # 布局XML
│   ├── drawable/           # 图片资源
│   ├── values/             # 字符串、颜色等
│   └── ...
├── tests/                  # 单元测试
├── Android.bp              # 构建配置文件
├── AndroidManifest.xml     # 清单文件
└── ...

这个结构,说白了就是按功能模块划分的。每个核心功能,都对应一个独立的包。我个人习惯,看源码时会先扫一遍src/com/android/systemui/下面的子包名,基本就能猜出SystemUI支持哪些功能了。

核心要点:SystemUI的源码组织方式,是「功能模块化」的。每个模块内部,又遵循MVP或MVVM模式。你想想看,这样设计的好处是什么?——方便解耦,也方便不同团队并行开发。

2.2 关键模块介绍

这里我挑几个最核心的模块,跟你详细说说。这些模块,你在日常开发中几乎天天都会打交道。

2.2.1 StatusBar(状态栏)

状态栏,就是手机顶部显示时间、信号、电量那一栏。它可不是一个简单的View,背后是一整套复杂的架构。

核心类:

  • StatusBar.java — 状态栏的主控制器,负责生命周期管理
  • PhoneStatusBarView.java — 状态栏的视图层
  • StatusBarIconController.java — 图标管理,比如控制哪些图标显示、怎么显示

我记得有一次,客户反馈说状态栏的电池图标在低电量时不闪烁了。我排查了半天,最后发现是StatusBarIconController里一个动画状态没重置。嗯,这种细节问题,往往就藏在控制器里。

2.2.2 NavigationBar(导航栏)

导航栏,就是屏幕底部的返回键、Home键、最近任务键。现在很多手机用全面屏手势了,但代码里依然保留着导航栏的实现。

核心类:

  • NavigationBarView.java — 导航栏的视图
  • NavigationBarController.java — 导航栏的控制器
  • NavigationBarInflaterView.java — 负责从布局文件加载导航栏

小技巧:如果你想自定义导航栏的按键布局,重点关注NavigationBarInflaterView这个类。它决定了哪些按键显示、按键的顺序。我曾经通过修改它的布局文件,给客户定制了一个「一键截屏」的虚拟按键。

2.2.3 QuickSettings(快捷设置面板)

下拉状态栏出现的那个面板,就是QuickSettings。WiFi开关、蓝牙开关、手电筒,都在这里。

核心类:

  • QSPanel.java — 快捷设置的主面板
  • QSTile.java — 每个快捷开关的基类,比如WiFiTile、BluetoothTile
  • QSTileHost.java — 负责管理所有Tile的创建和生命周期

这里有个坑,我必须要提醒你:

避坑指南:我曾经在添加自定义Tile时,直接继承了QSTile,但忘了在QSTileHost中注册。结果Tile死活不显示。后来才发现,所有Tile必须通过QSTileHostcreateTile方法创建,不能自己new。

2.2.4 其他重要模块

模块 核心类 功能说明
音量面板 VolumeDialogImpl.java 按音量键时弹出的面板
电源菜单 GlobalActionsImpl.java 长按电源键弹出的菜单
截图 ScreenshotController.java 处理截图逻辑和动画
锁屏 KeyguardViewMediator.java 锁屏界面的核心控制器
通知管理 NotificationListener.java 监听系统通知,更新状态栏

这些模块,每个单独拿出来都能讲一节课。但今天我们先有个整体印象就好。

2.3 编译与构建

好,源码看完了,怎么把它跑起来呢?SystemUI的编译,和普通App不太一样。它属于系统级应用,必须跟着整个系统一起编译。

2.3.1 构建文件

SystemUI的构建配置,主要看Android.bp文件。这是Android新一代的构建系统,取代了旧的Android.mk

核心内容大致如下:

android_app {
    name: "SystemUI",

    srcs: [
        "src/**/*.java",
        "src/**/*.kt",
    ],

    resource_dirs: ["res"],

    platform_apis: true,

    required: [
        "privapp_whitelist_com.android.systemui",
    ],

    // 依赖的静态库
    static_libs: [
        "androidx-constraintlayout_constraintlayout",
        "androidx-recyclerview_recyclerview",
        // ... 很多其他依赖
    ],
}

注意看platform_apis: true这一行。这意味着SystemUI可以使用系统隐藏API。普通App可没这个权限。这也是为什么SystemUI能做很多普通App做不了的事。

2.3.2 编译命令

如果你有完整的AOSP源码环境,编译SystemUI很简单:

# 单独编译SystemUI模块
make SystemUI

# 或者用mmm命令(更推荐,速度快)
mmm frameworks/base/packages/SystemUI/

编译完成后,输出文件在:

out/target/product/你的设备名/system/priv-app/SystemUI/SystemUI.apk

个人经验:我平时调试SystemUI,很少用make全量编译。太慢了。我一般用mmmadb install -r的方式。改一行代码,编译加推送,30秒搞定。但要注意,有些修改必须重启SystemUI进程才能生效,可以用adb shell pkill -f com.android.systemui来杀掉进程,系统会自动重启它。

2.3.3 增量编译与热替换

说到调试,这里有个更高级的技巧——热替换。

Android 10之后,SystemUI支持了「实时资源替换」。你修改了布局或资源文件后,不需要重新编译整个APK,只需要:

# 编译资源
make SystemUI-res

# 推送资源到设备
adb push out/target/product/xxx/system/priv-app/SystemUI/SystemUI.apk /system/priv-app/SystemUI/

# 重启SystemUI
adb shell pkill -f com.android.systemui

嗯,这个方法我用了好几年。尤其是调试UI布局时,改一个dimens.xml或者layout.xml,几秒钟就能看到效果,非常爽。

2.3.4 常见编译问题

最后,我列几个我踩过的坑:

  • 编译报错「找不到符号」:多半是依赖没加全。检查Android.bp里的static_libsshared_libs
  • 安装失败「INSTALL_FAILED_UPDATE_INCOMPATIBLE」:SystemUI是系统应用,不能直接用adb install覆盖。要用adb remount后,把APK推到/system/priv-app/目录。
  • 运行时崩溃「ClassNotFoundException」:检查ProGuard混淆规则。SystemUI默认不开启混淆,但如果你手动开了,记得配置keep规则。

好了,这一章的内容就到这里。我们梳理了SystemUI的目录结构、关键模块,以及怎么编译它。下一章,我会带你深入StatusBar的启动流程,看看SystemUI是怎么从无到有,一步步构建出我们看到的界面的。