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、BluetoothTileQSTileHost.java— 负责管理所有Tile的创建和生命周期
这里有个坑,我必须要提醒你:
避坑指南:我曾经在添加自定义Tile时,直接继承了QSTile,但忘了在QSTileHost中注册。结果Tile死活不显示。后来才发现,所有Tile必须通过QSTileHost的createTile方法创建,不能自己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全量编译。太慢了。我一般用mmm加adb 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_libs和shared_libs。 - 安装失败「INSTALL_FAILED_UPDATE_INCOMPATIBLE」:SystemUI是系统应用,不能直接用
adb install覆盖。要用adb remount后,把APK推到/system/priv-app/目录。 - 运行时崩溃「ClassNotFoundException」:检查ProGuard混淆规则。SystemUI默认不开启混淆,但如果你手动开了,记得配置keep规则。
好了,这一章的内容就到这里。我们梳理了SystemUI的目录结构、关键模块,以及怎么编译它。下一章,我会带你深入StatusBar的启动流程,看看SystemUI是怎么从无到有,一步步构建出我们看到的界面的。