要点摘要
- 问题:开机时状态栏由谁拉起?为什么它不等
BOOT_COMPLETED?定制后状态栏不显示,该从哪一段查起? - 结论:SystemUI 是
persistent的系统进程。AMSsystemReady先通过startPersistentApps拉起各 persistent 应用;SystemUI 进程若尚未运行,会在这一步被发起启动。systemReady返回后,system_server在startOtherServices末尾调用startSystemUi,以SYSTEM用户启动SystemUIService。进程内,AppComponentFactory设下的回调负责建 Dagger 图;SystemUIService.onCreate再依次启动全部CoreStartable。在 16-qpr2 上,CommandQueueInitializer按显示屏向StatusBarManagerService注册CommandQueue,StatusBarOrchestrator把状态栏窗口加入 WMS,StatusBarInitializer挂上 Compose 根视图,状态栏随后可见。13–15 和 16 首发默认走老路径,这些步骤都在CentralSurfacesImpl.start()中完成。 - 版本:以 android16-qpr2-release 为主线。13 起启动清单由 Dagger Map 决定,基类是
abstract class CoreStartable;14 改为interface;15 引入@Dependencies,按依赖拓扑启动;16 拓扑启动不变,状态栏启动由 flag 在新旧两条路径间选择,android16-qpr2-release 的 release config 里新路径的 flag 是 ENABLED。17 的变化见「17 的变化」一节。 - 范围:只用 AOSP 公开源码和公开文档,不涉及任何厂商定制实现。
进程与职责总览
读源码前,先明确「谁在哪个进程、负责什么」。后文的排障和定制都以这张表为基础。
| 实体 | 所在进程 | 职责 |
|---|---|---|
SystemServer |
system_server |
AMS systemReady 返回后,在 startOtherServices 末尾调用 startSystemUi |
PackageManagerInternal.getSystemUiServiceComponent() |
system_server |
给出要启动的组件,默认取自 framework 资源 config_systemUIServiceComponent(com.android.systemui/com.android.systemui.SystemUIService) |
SystemUIAppComponentFactoryBase 的子类 |
com.android.systemui |
由 Manifest 的 appComponentFactory 指定;实例化 Application 和 ContentProvider 时设回调,回调负责创建 SystemUIInitializer 并建 Dagger 图 |
SystemUIApplicationImpl(16-qpr2;此前为 SystemUIApplication) |
同上 | 取得 SysUIComponent,区分系统用户与次用户;监听 LOCKED_BOOT_COMPLETED |
SystemUIService |
同上 | 常驻入口;onCreate 调用 Application 的启动方法 |
CoreStartable 集合(下文简称 Startable) |
同上 | 状态栏、导航栏、通知、锁屏等各界面表面(surface)的启动单元 |
CentralSurfacesImpl |
同上 | 通知面板(Shade)、锁屏等的协调中枢;老路径下还负责状态栏的注册和加窗 |
CommandQueueInitializer、MultiDisplayStatusBarStarter、StatusBarOrchestrator |
同上 | 16 新路径:按显示屏注册 CommandQueue、启动状态栏、加窗 |
CommandQueue |
同上 | IStatusBar.Stub 的实现,是 SystemUI 进程里的 Binder 服务端;StatusBarManagerService 持有它的 proxy,向 SystemUI 下发命令 |
StatusBarWindowController(16 起为接口,实现类 StatusBarWindowControllerImpl) |
同上 | 以 TYPE_STATUS_BAR 把状态栏窗口 addView 到 WMS |
定位:SystemUI 不是普通 App,而是 system_server 在服务就绪后主动拉起、常驻不退的系统 UI 进程。
谁启动 SystemUI:systemReady → startSystemUi
调用时机
入口在 frameworks/base/services/java/com/android/server/SystemServer.java。
12 到 17 的结构一致:startOtherServices 后段调用 mActivityManagerService.systemReady(() -> { ... }, t),回调里执行 PHASE_ACTIVITY_MANAGER_READY、PHASE_THIRD_PARTY_APPS_CAN_START 等启动阶段。回调返回之后,startOtherServices 末尾才以 t.traceBegin("StartSystemUI") 包裹调用 startSystemUi(context, windowManagerF)。
版本差异:Android 11 及以前,
startSystemUi写在systemReady的回调内;12 起移到回调外。不少网上资料仍是 11 的写法。
systemReady 执行完回调后,还会做两件与 SystemUI 相关的事(services/core/java/com/android/server/am/ActivityManagerService.java):
startPersistentApps(PackageManager.MATCH_DIRECT_BOOT_AWARE):对每个persistent且directBootAware的应用调用addAppLocked;进程未运行时,以 hostingTypeadded application发起进程启动。SystemUI 两个属性都具备。startHomeOnAllDisplays(...):请求启动桌面。
因此开机时有两条线:
- 进程线:
startPersistentApps会为 SystemUI 发起进程启动(若进程尚未运行),这是源码事实。推断:SystemUI 进程在这一步首次 fork,Application 和 Dagger 图也在这一阶段建好。不确定之处:本文没有排查systemReady之前是否有其他路径(例如对 SystemUI 组件的访问)先把进程拉起;若进程已在运行,addAppLocked不会再次启动它。「用 logcat 看启动顺序」一节给出用Start proc日志验证的方法。 - Service 线:
systemReady返回后,startSystemUi才调用startServiceAsUser。若进程已在运行,它只触发SystemUIService.onCreate,状态栏等表面从这里开始启动。
这也解释了面试常问的「为什么桌面可能比状态栏先出来」:桌面的启动请求早于 startSystemUi 发出。
函数本体
startSystemUi 的逻辑只有四步:
// AOSP frameworks/base, SystemServer.java (Apache-2.0),节选 |
要点:
- 组件名不是写死的,而是取自 framework 资源
config_systemUIServiceComponent。该值可以用 overlay 覆盖,OEM 可借此换成自己的 SystemUI 包。 - 调用的是
startServiceAsUser(..., UserHandle.SYSTEM),即以系统用户启动一个 Service,不涉及广播,也不走反射。 - 启动后调用 WMS 的
onSystemUiStarted(),见「onSystemUiStarted:绑定锁屏服务」一节。 - 12 到 17 的函数体一致。
onSystemUiStarted:绑定锁屏服务
以 16 的源码为准,WindowManagerService.onSystemUiStarted() 只有一行,转交窗口策略 mPolicy.onSystemUiStarted()。手机上的策略实现是 PhoneWindowManager,它在这里调用 bindKeyguard();bindKeyguard() 用 mKeyguardBound 标志保证只绑定一次,再通过 KeyguardServiceDelegate.bindService 绑定 SystemUI 进程里的锁屏服务。
可见锁屏和状态栏的建立方向相反:
- 状态栏:SystemUI 主动注册。16-qpr2 由
CommandQueueInitializer调用registerStatusBarForAllDisplays(老路径由CentralSurfacesImpl调用registerStatusBar)向 Framework 注册。 - 锁屏:Framework 主动绑定。
system_server的窗口策略去绑定 SystemUI 暴露的锁屏服务。
面试时讲清这两个方向,比笼统地说「SystemUI 管锁屏」更有说服力。
与 BOOT_COMPLETED 的关系
这是面试中最容易说反的一点:
- SystemUI 的启动不等
BOOT_COMPLETED。进程在systemReady阶段被拉起,SystemUIService在systemReady返回后启动,二者都早于开机完成广播。 - SystemUI 内部有一套开机完成通知:
SystemUIApplication.onCreate在系统用户进程里注册ACTION_LOCKED_BOOT_COMPLETED接收器。收到广播后,把状态记入BootCompleteCache,再对已启动的每个CoreStartable调用onBootCompleted()。 - 如果 SystemUI 启动较晚、开机完成广播已经发出,启动流程中的
startServicesIfNeeded会读取系统属性sys.boot_completed,补记开机完成。
记法:**start() 早于开机完成,onBootCompleted() 晚于 start()**。CoreStartable 的接口注释也写明:onBootCompleted() 不会早于 start() 被调用。
进程起来之后:Dagger 图在哪里建
Manifest 的三个关键属性
frameworks/base/packages/SystemUI/AndroidManifest.xml 的 <application> 上有三个属性值得记住:
android:persistent="true":常驻进程,被杀后由系统拉回。android:directBootAware="true":用户解锁前(Direct Boot 阶段)也能运行,因此锁屏界面能在首次解锁前出现。android:appComponentFactory:14 为.SystemUIAppComponentFactory,15、16、16-qpr2 为.PhoneSystemUIAppComponentFactory。
此外,16-qpr2 起 <application> 的 android:name 是 .application.impl.SystemUIApplicationImpl(此前为 .SystemUIApplication),抽象父类在 application/SystemUIApplication.kt。源码注释说明,拆分是为了把初始化代码放进单独的 build target。下文提到 SystemUIApplication 的启动逻辑和 onCreate 时,在 16-qpr2 上均指实现类 SystemUIApplicationImpl。
Dagger 图在哪一刻建立
常见说法是「SystemUIApplication.onCreate 里建 Dagger 图」,这不够准确。14 到 16-qpr2 的实际机制如下:
系统通过
appComponentFactory实例化 Application 时,进入SystemUIAppComponentFactoryBase.instantiateApplicationCompat。16-qpr2 要求 Application 实现ApplicationContextInitializer,否则抛出「App must implement ApplicationContextInitializer」;随后为它设置ApplicationContextAvailableCallback。instantiateProviderCompat为实现了ContentProviderContextInitializer的 ContentProvider 设置ContentProviderContextAvailableCallback,回调中还会把 Provider 注入SysUIComponent。两个回调最终调用同一个创建 Initializer 的方法。16-qpr2 主进程里的KeyguardSliceProvider、CustomizationProvider、PeopleProvider都实现了该接口:KeyguardSliceProvider.onCreateSliceProvider()第一句调用回调,CustomizationProvider.attachInfo()在调用super.attachInfo()之前调用回调。版本差异:16-qpr2 起接口拆为
ApplicationContextInitializer和ContentProviderContextInitializer;13–16 首发是同一个ContextInitializer,回调类型为ContextAvailableCallback。回调内部调用子类实现的
createSystemUIInitializer(context),再执行initializer.init(false)。结果缓存在静态字段中,整个进程只建一次。SystemUIInitializer.init先建GlobalRootComponent,再建WMComponent(WM Shell),把 Shell 提供的 Pip、SplitScreen、Bubbles 等接口传给SysUIComponent.Builder,建出SysUIComponent,最后调用Dependency.start()。SystemUIApplicationImpl.onCreate开头同样调用回调:mInitializer = mContextAvailableCallback.onContextAvailable(this)。源码注释要求这一行保持在方法顶部。
首个触发点是 Provider 安装,而不是 onCreate。 16-qpr2 的源码链路是:
- AMS 在
attachApplicationLocked中,对 persistent 应用(isAllowedWhileBooting)即使在mProcessesReady之前也走正常模式,由ContentProviderHelper.generateApplicationProvidersLocked生成该进程的 Provider 列表,随bindApplication下发。 ActivityThread.handleBindApplication先makeApplicationInner创建 Application,再installContentProviders,最后callApplicationOnCreate。installContentProviders会调用每个 Provider 的attachInfo;CustomizationProvider.attachInfo先触发回调,建出 Dagger 图。
所以在 16-qpr2 上,主进程正常启动时(非受限备份模式),Dagger 图在 Provider 安装阶段就已建好,onCreate 第一行拿到的是缓存。准确的说法是:Dagger 图由 AppComponentFactory 设下的回调建立,首个触发点是主进程的 Provider 安装。 onCreate 里那段 trace 切片名为 DependencyInjection,耗时可能很小;要看建图的真实耗时,需结合进程的 bindApplication 切片(见「用 Perfetto 看耗时分布」一节)。
补充:
SystemUIService本身也由 Dagger 构造:构造函数带@Inject,由instantiateServiceCompat通过组件辅助类创建。Dependency.start()说明旧的全局Dependency.get()仍在(16、17 都在)。这是过渡期,新旧两套取依赖方式并存。- 16 起,Application 构造函数在非子进程中调用
Trace.registerWithPerfetto()。 - 16、16-qpr2、17 的
KeyguardSliceProvider.onCreateSliceProvider()第一句都是mContextAvailableCallback.onContextAvailable(getContext());16、16-qpr2、17 的CustomizationProvider都在attachInfo()中先调用回调。
替换 Initializer 的入口:按版本区分
旧资料常说「改 config_systemUIFactoryComponent 换 Factory」。按版本核对如下:
- 12、13:
packages/SystemUI/res/values/config.xml中config_systemUIFactoryComponent的值都是com.android.systemui.SystemUIFactory,换工厂改这个资源即可。 - 14:过渡版。Manifest 默认的
appComponentFactory是.SystemUIAppComponentFactory(已标@Deprecated)。它的createSystemUIInitializer调用SystemUIInitializerFactory.createWithContext(context),后者用context.getString(R.string.config_systemUIFactoryComponent)取类名(默认com.android.systemui.SystemUIInitializerImpl),再反射调用(Context)构造函数。因此 14 上改这个 config 仍然有效,只是已弃用;注释建议改为自定义SystemUIAppComponentFactoryBase子类。 - 15、16:Manifest 换成
.PhoneSystemUIAppComponentFactory,直接返回SystemUIInitializerImpl(context)。config 资源和SystemUIInitializerFactory一并删除,只剩appComponentFactory这一个入口。
一句话:14 是过渡版,新入口已有,旧 config 仍可用;15 起只认 appComponentFactory。
系统用户与次用户分叉
SystemUIApplication.onCreate 建完图后分叉:
- 系统用户进程:注册开机完成和语言变化的接收器,不启动任何表面,等待
SystemUIService触发。 - 非系统用户进程:如果是子进程(16-qpr2 的判断是进程名含冒号,例如
com.android.systemui:screenshot),直接返回,不启动任何表面;否则直接调用startSecondaryUserServicesIfNeeded(),只启动@PerUser标注的那部分 Startable。源码注释给出的原因是:次用户进程收不到开机完成广播,因为该广播在主用户启动时已经发过。
常驻入口:SystemUIService → CoreStartable
SystemUIService.onCreate
路径:frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIService.java
onCreate 的第一件事是调用 Application 的启动方法:
- 14:
((SystemUIApplication) getApplication()).startServicesIfNeeded() - 15 起:改名为
startSystemUserServicesIfNeeded(),在非系统用户上调用时记一条wtf日志后返回。16-qpr2 把getApplication()转为新的抽象类com.android.systemui.application.SystemUIApplication再调用。
其余是调试和兜底逻辑:挂 LogBuffer、初始化 dump(14:mDumpHandler.init())、为未捕获异常加 LogBuffer 记录钩子(15 起:mUncaughtExceptionPreHandlerManager.registerHandler)、按配置监听未知电池状态等。真正的业务都在 Startable 里。
启动哪些 Startable
启动方法合并三类来源:
mSysUIComponent.getStartables():所有通过@IntoMap @ClassKey(...)(@Binds或@Provides)绑定到CoreStartable的实现。主要绑定在dagger/SystemUICoreStartableModule.kt,另有分散在各模块的。例如CentralSurfaces在statusbar/dagger/StartCentralSurfacesModule.kt中以@ClassKey(CentralSurfaces::class)绑定(Map 的 key 是接口,不是 Impl);15 的statusbar/dagger/StatusBarModule.kt还绑定了OngoingCallController、LightBarController;16 的状态栏新路径绑定在statusbar/phone/dagger/StatusBarPhoneModule.kt(见「新路径的三个 Startable」一节)。mSysUIComponent.getPerUserStartables():带@PerUser限定的子集,次用户也会启动。- 可选的厂商组件:
config_systemUIVendorServiceComponent,默认是com.android.systemui.VendorServices,通过反射调用无参构造函数,最后启动。
前两类放进以 key 的类名(Class::getName)排序的 TreeMap。排序的目的是每次顺序一致,并不表达语义上的依赖;但对没有声明依赖的 Startable 来说,它就是实际的启动先后(「根视图:StatusBarInitializer 的 Compose 路径」一节会用到这一点)。
每个 Startable 怎么启动
单个 Startable 的启动分两步,trace 中各有一个切片:
Provider<类名>.get():Dagger 构造对象。构造函数依赖多、做事多,耗时就记在这里。类名.start():执行真正的初始化。
所有 start() 都在主线程串行执行:SystemUIApplication.startSystemUserServicesIfNeeded() 的注释写明「This method must only be called from the main thread.」,调用方 SystemUIService.onCreate 也运行在主线程。
外层还包了一层 timeInitialization:单个组件超过 1000ms,会打一条 Initialization of ... took ... ms 警告。这是排查「哪个表面拖慢了 SystemUI」最直接的日志线索。
全部启动完成后:若已开机完成,逐个调用 onBootCompleted();把每个 Startable 注册到 DumpManager(15 起区分关键和普通两类);再执行 InitController 的延后任务。
CoreStartable 的版本演进
以 android13 到 16-qpr2 的 release 分支逐一核对(12 仅作对比):
| 版本 | 基类形态 | 启动列表来源 | 顺序 |
|---|---|---|---|
| 12 | abstract class SystemUI |
config_systemUIServiceComponents 字符串数组决定清单和顺序;实例优先从 Dagger 的 SystemUI Map(dagger/SystemUIBinder.java)取,取不到再反射调用 (Context) 构造函数 |
数组顺序 |
| 13 | abstract class CoreStartable,构造函数带 Context |
清单改由 Dagger Map<Class<?>, Provider<CoreStartable>> 决定,另有 @PerUser 子集 |
按类名排序 |
| 14 | interface CoreStartable extends Dumpable |
同 13 | 按类名排序,逐个启动 |
| 15 | 同 14,新增 isDumpCritical(),移除 onConfigurationChanged |
同 13,另有 @Dependencies 依赖表 getStartableDependencies() |
拓扑启动 |
| 16 | 同 15,新增 CoreStartable.NOP 空实现(供 flag 切换时让位) |
同 15 | 拓扑启动,逻辑与 15 逐行相同 |
| 16-qpr2 | 同 16,文件移到 packages/SystemUI/pods/src/com/android/systemui/CoreStartable.java |
同 15;拓扑启动代码在 application/impl/SystemUIApplicationImpl.java |
同 16 |
15 起的拓扑启动逻辑写在 Application 里(16-qpr2 为 SystemUIApplicationImpl):
- 把所有未启动的 Startable 按
TreeMap顺序放进队列。 - 依次取出:依赖都已启动(或没有依赖)的立即启动,否则放进下一轮队列。
- 本轮有进展且下一轮队列非空,就继续循环。
- 结束时若仍有未启动的,逐个打印缺失的依赖,再抛出
RuntimeException("Failed to start all CoreStartables. Check logcat!")。
依赖表本身是一个带 @Dependencies 限定符的 Dagger Map:SysUIComponent.getStartableDependencies() 返回 Map<Class<?>, Set<Class<? extends CoreStartable>>>,SystemUIModule 用 @Multibinds 声明它,各模块以 @Provides @IntoMap @Dependencies @ClassKey(...) 添加条目。源码注释说明这个排序没有做优化,因为大多数 Startable 没有依赖,两三轮就能结束。
为什么要改:14 及以前顺序只由类名决定。A 要求 B 先 start(),只能靠类名碰巧排在后面,或在代码里绕开。15 让依赖显式化,缺依赖时启动失败并直接报出缺了什么。
状态栏如何变得可见
16 的状态栏启动有新旧两条路径,由两个 aconfig flag 选择:
StatusBarConnectedDisplays(com.android.systemui.shared.status_bar_connected_displays):决定注册和加窗是否从CentralSurfacesImpl中拆出。StatusBarRootModernization(com.android.systemui.status_bar_root_modernization):决定状态栏根视图用 Compose 还是CollapsedStatusBarFragment。
platform/build/release 的取值:android16-qpr2-release 分支里,status_bar_root_modernization 在 bp3a 是 ENABLED(READ_ONLY),status_bar_connected_displays 在 bp4a 是 ENABLED(READ_ONLY),bp4a 继承 bp3a。android16-release 分支里这两个 flag 都没有值文件;没有值文件的 flag,构建时由 aconfig 工具取默认值 DISABLED(platform/build 的 tools/aconfig/aconfig/src/commands.rs 中 DEFAULT_FLAG_STATE)。本文以 16-qpr2 为主线,正文讲新路径;老路径见「老路径」和「从 13–15 迁移到 16」两节。
新路径的三个 Startable
三者都在 frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/core/ 下,统一在 statusbar/phone/dagger/StatusBarPhoneModule.kt 中以 @Provides @IntoMap @ClassKey(...) 绑定为 CoreStartable。绑定函数按 flag 返回真实对象或 CoreStartable.NOP(16 新增的空实现):
@ClassKey |
StatusBarConnectedDisplays 开 |
关 |
|---|---|---|
CommandQueueInitializer |
真实对象 | NOP |
MultiDisplayStatusBarStarter |
真实对象 | NOP |
StatusBarInitializer |
NOP(源码注释:由 MultiDisplayStatusBarStarter 启动) |
StatusBarRootModernization 开则用默认显示屏的 Impl,否则 NOP(由 CentralSurfaces 启动) |
这种写法本身就是面试素材:同一个 @ClassKey 下按 flag 换实现,启动表结构不变,新旧路径靠 NOP 互相让位。
注册:CommandQueueInitializer
start() 做三件事:
barService.registerStatusBarForAllDisplays(commandQueue):一次把CommandQueue注册给 SBMS,返回Map<String, RegisterStatusBarResult>,key 是显示屏 ID。- 对默认显示屏:创建导航栏,按结果中的
mIcons逐个调用commandQueue.setIcon(...)恢复图标。 - 对每块显示屏:恢复瞬态栏、系统栏外观和 IME 状态;disable 标志通过
InitController的延后任务应用。
16 / 16-qpr2 的 start() 开头有 StatusBarConnectedDisplays.unsafeAssertInNewMode(),只允许在新模式下运行。
CommandQueue 本身没有变,仍是 IStatusBar.Stub,即 SystemUI 进程里的 Binder 服务端:
- Framework → SystemUI:
StatusBarManagerService持有CommandQueue的 proxy,调用它禁用状态栏、设置图标、展开或收起面板等。CommandQueue用主线程 Handler 把调用从 Binder 线程切到主线程,再分发给各Callbacks。 - SystemUI → Framework:SystemUI 通过
IStatusBarService调用 SBMS,注册走的就是这个方向。 - 死亡感知:16-qpr2 的 SBMS 在
registerStatusBar和registerStatusBarForAllDisplays中都会调用mDeathRecipient.linkToDeath()。SystemUI 死亡后,binderDied()会unlinkToDeath,把mBar置空,再调用notifyBarAttachChanged()。
加窗:MultiDisplayStatusBarStarter → StatusBarOrchestrator
MultiDisplayStatusBarStarter.start() 在应用级协程(applicationScope)中收集 displayRepository.displayIdsWithSystemDecorations,先通过 onStart 发出当前值,之后每出现新的显示屏,依次:
multiDisplayStatusBarOrchestratorStore.forDisplay(id)?.start()statusBarInitializerStore.forDisplay(id)?.start()- 非默认屏启动隐私指示点窗口;创建该屏的
LightBarController。
StatusBarOrchestrator 的类注释称它是「临时类,用来把状态栏逻辑从 CentralSurfacesImpl 中拆出来」。它的 start() 先在主线程协程里挂上一组状态收集(锁屏 bouncer、状态栏模式、自动隐藏等),然后在 start() 内同步调用 createAndAddWindow():先给 StatusBarInitializer 设置视图回调,再调用 statusBarWindowController.attach()。
StatusBarWindowController 在 16 改为 Kotlin 接口,实现类是 StatusBarWindowControllerImpl.java。attach() 仍是 WindowManager.addView(...),窗口类型 TYPE_STATUS_BAR,使用该显示屏自己的 WindowManager。16-qpr2 的窗口标题是 "StatusBar" + 后缀:默认屏无后缀,其他屏为 (displayId=N)。
根视图:StatusBarInitializer 的 Compose 路径
StatusBarInitializer 从 13 起就在 statusbar/core/StatusBarInitializer.kt。16 起它是 interface StatusBarInitializer : CoreStartable,实现类 StatusBarInitializerImpl 用 @AssistedInject 按显示屏创建。start() → doStart(),按 StatusBarRootModernization.isEnabled 二选一:
doComposeStart():创建 Compose 状态栏根视图,加到窗口的背景视图中,不使用 Fragment。doLegacyStart():通过Provider<CollapsedStatusBarFragment>提交 Fragment 事务(老路径)。
视图建好后,通过回调把 PhoneStatusBarViewController 和 PhoneStatusBarTransitions 交给 StatusBarOrchestrator。
与老路径顺序相反:新路径先由 StatusBarOrchestrator 把窗口加入 WMS,再由 StatusBarInitializerImpl 把 Compose 视图挂进窗口。挂视图是普通的 ViewGroup.addView,不是 WMS 加窗。老路径是先提交 Fragment 事务,再 attach 窗口(见「老路径」一节)。
注册与加窗的先后(源码可证):
- 在 android16-qpr2-release 的
packages/SystemUI已查范围内,没有约束CommandQueueInitializer与MultiDisplayStatusBarStarter(以及StatusBarOrchestrator、StatusBarInitializer)先后的@Dependencies条目;已查到的唯一条目是ReferenceSystemUIModule中SysuiStatusBarStateController依赖CentralSurfaces。 - 因此两者都在拓扑启动的第一轮,按
TreeMap的类名顺序启动:com.android.systemui.statusbar.core.CommandQueueInitializer排在com.android.systemui.statusbar.core.MultiDisplayStatusBarStarter之前。 registerStatusBarForAllDisplays有返回值,是同步 Binder 调用。所以在MultiDisplayStatusBarStarter.start()开始之前,注册已经完成。
推断:默认屏加窗在 MultiDisplayStatusBarStarter.start() 返回前同步完成。 依据:applicationScope 使用 @Main 的 CoroutineContext,flag do_not_use_immediate_coroutine_dispatcher 关闭时是 Dispatchers.Main.immediate,而 android16-qpr2-release 的 release config 里没有该 flag 的值文件;start() 在主线程调用,onStart 同步发出当前显示屏集合,StatusBarOrchestrator.start() 内同步调用 attach()。不确定之处:launchTraced 和 DisplayRepository 接口位于 packages/SystemUI 之外,本文未核;该 flag 运行时是否被覆盖、默认屏是否已在初始集合中,也以设备实际为准。
老路径(16 首发默认、13–15)
两个 flag 都关闭时,状态栏由 CentralSurfacesImpl.start() 负责:
mBarService.registerStatusBar(mCommandQueue),取回单个RegisterStatusBarResult。16 的源码把这句包在if (!StatusBarConnectedDisplays.isEnabled())中。createAndAddWindows(result):makeStatusBarView调用StatusBarInitializer.initializeStatusBar()提交CollapsedStatusBarFragment事务;随后 attach 通知面板窗口和状态栏窗口(16 写作mStatusBarWindowControllerStore.getDefaultDisplay().attach(),15 是mStatusBarWindowController.attach())。setUpPresenter(),源码注释要求它在CommandQueue.disable()之前完成。
**Fragment 视图晚于 addView**(以 android16-release 和 android16-qpr2-release 源码为准):doLegacyStart() 用的是 android.app.FragmentManager 的 commit();FragmentManager.scheduleCommit() 把执行任务 post 到宿主 Handler,FragmentHostManager 的 Handler 绑定主线程 Looper。CentralSurfacesImpl.start() 本身在主线程上运行,attach() 在同一次调用中同步执行,所以事务只能在这之后执行,Fragment 视图也在 addView 之后创建。
从 startSystemUi 到状态栏可见的时序(16-qpr2 新路径)
| 序 | 进程 | 动作 | 关键类 |
|---|---|---|---|
| 1 | system_server |
AMS systemReady:执行回调(各启动阶段)→ startPersistentApps → startHomeOnAllDisplays |
ActivityManagerService |
| 2 | Zygote → com.android.systemui |
startPersistentApps 为 SystemUI 发起进程启动(若未运行);推断这是首次 fork,以 Start proc 日志为准 |
ProcessList |
| 3 | com.android.systemui |
appComponentFactory 实例化 Application 并设回调;bindApplication 安装 Provider 时首次触发回调 → SystemUIInitializer.init() 建 Root、WM、SysUI 三层图 |
SystemUIAppComponentFactoryBase、SystemUIInitializer |
| 4 | 同上 | Application.onCreate 取缓存的图;系统用户分支注册 LOCKED_BOOT_COMPLETED 接收器 |
SystemUIApplicationImpl |
| 5 | system_server |
systemReady 返回 → startSystemUi → startServiceAsUser(SYSTEM) → onSystemUiStarted → 绑定锁屏 |
SystemServer、PhoneWindowManager |
| 6 | com.android.systemui |
SystemUIService.onCreate → 主线程拓扑启动全部 Startable |
SystemUIService、CoreStartable |
| 7 | 同上 ↔ system_server |
CommandQueueInitializer.start → registerStatusBarForAllDisplays(CommandQueue) |
CommandQueueInitializer、StatusBarManagerService |
| 8 | 同上 ↔ system_server |
MultiDisplayStatusBarStarter 按屏启动 StatusBarOrchestrator(addView(TYPE_STATUS_BAR))和 StatusBarInitializer(Compose 根视图)→ 首帧,状态栏可见 |
StatusBarOrchestrator、StatusBarInitializerImpl、StatusBarWindowControllerImpl |
| 9 | 同上 | 开机完成广播到达 → 逐个 onBootCompleted() |
BootCompleteCache |
7 先于 8 有源码依据;8 在启动循环内同步完成是推断(均见「根视图:StatusBarInitializer 的 Compose 路径」一节)。
如何测量「状态栏比桌面晚出现」
按「用 Perfetto 看耗时分布」一节的方法抓一段开机 Perfetto trace,在 com.android.systemui 进程主线程上找 bindApplication、DependencyInjection、StartServices 和各 StartServices <类名> 切片,再找 StatusBar 窗口的首帧,与 Launcher 进程的首帧比较先后。结论用切片的时间差表述,数字需在自己的模拟器上实测。
其他表面:同一进程,各自是 Startable
导航栏、通知、锁屏、快捷设置、音量、截图等,大多以自己的 CoreStartable 或 CentralSurfaces 下的控制器就位,共享同一个 Dagger 图和主线程。
这带来两个后果:
- 一处慢,处处慢。所有
start()在主线程串行执行(见「每个 Startable 怎么启动」一节),任何一个 Startable 做重活,都会推迟其后所有表面,包括状态栏。 - 一处崩,全部消失。SystemUI 进程崩溃时,状态栏、通知、锁屏一起消失,再由
persistent机制拉回。面试谈稳定性时可以从这一点切入。
公开扩展点(基于 AOSP 源码,可在模拟器复现)
用 RRO 改状态栏布局和高度
- 栏高:14 到 16-qpr2 的状态栏窗口高度来自
SystemBarUtils.getStatusBarHeightForRotation,读取的是 framework(android包) 的status_bar_height_default,再按挖孔修正。framework(core/res/res/values/dimens.xml)中的status_bar_height只是status_bar_height_portrait的别名,SystemUI 包里没有栏高资源。所以改栏高的 RRO 要以android为 target,改status_bar_height_default,必要时连同 portrait、landscape 一起改。 - 布局:信号、电池、时钟区域的排布,可以用以 SystemUI 为 target 的 RRO 改 layout 和 dimen,少动 Java 代码。
- 好处:不改源码,升级合并时冲突小。
- 风险:栏高与触控热区、挖孔安全区不一致,会造成遮挡或误触。
- 验证:
adb shell cmd overlay list确认 overlay 已启用;adb shell dumpsys window windows查看StatusBar窗口的 frame。效果以实测为准。
换 Initializer,增加或替换 Startable
- 推荐入口:自定义
appComponentFactory子类,覆盖createSystemUIInitializer,返回自己的 Initializer,在它构建的 Dagger 组件中增加、替换或去掉CoreStartable绑定。15 起只有这一个入口;14 上改config_systemUIFactoryComponent仍有效但已弃用(见「替换 Initializer 的入口」一节)。 - 最轻量的入口:覆盖
config_systemUIVendorServiceComponent,挂一个附加的 Startable。它通过反射调用无参构造函数,最后启动,适合放与其他表面无依赖的逻辑。 - 风险:类名或构造函数不对,反射失败会抛
RuntimeException,进程直接崩溃。15 起漏声明依赖会导致拓扑启动失败,logcat 中的Failed to start ... Missing dependencies可直接定位。
裁剪不需要的 Startable
- 做法:在自己的 Dagger module 中不绑定某些表面,或换成空实现。
- 风险:被依赖项裁掉后,14 上可能表现为空指针或半初始化;15 起声明了依赖的会直接启动失败,问题更容易暴露。
- 验收:对比改前改后的 Startable 列表(日志,或
adb shell dumpsys activity service com.android.systemui/.SystemUIService的输出,以实际输出为准)、开机 trace 中StartServices的总时长,以及状态栏、通知、锁屏的冒烟测试。
多用户
- 次用户的 SystemUI 进程只启动
@PerUser子集,入口是startSecondaryUserServicesIfNeeded()。 - 新写的 Startable 如需每个用户各有一份,要放进
@PerUser映射;否则只在系统用户进程中运行。
动手验证:在模拟器上走一遍这条链
说明:「公开扩展点」「动手验证」「排障清单」三节的
adb命令均未在模拟器上实跑。需要 root 的命令只在 userdebug 镜像并执行adb root后可用。
以下步骤只用 AOSP 模拟器镜像和公开命令,读者可自行复现。本文不给实测数字,结果以你自己的环境为准。
用 logcat 看启动顺序
看进程在哪一步启动。 开机后执行 adb logcat -b all -d | grep "Start proc.*com.android.systemui"。日志中的 hosting 类型若是 added application,说明进程由 startPersistentApps 拉起;若是 service,说明由 startSystemUi 拉起。这一条用来验证「调用时机」一节的推断。开机较久后再抓,这条日志可能已被冲掉,可改看事件日志:adb logcat -b events -d | grep am_proc_start,其中也有 hostingType 字段。Windows 上把 grep 换成 findstr。
看 SystemUI 自己的日志。 执行 adb logcat -s SystemUIService。16-qpr2 的 SystemUIApplicationImpl 中 TAG 常量就是 SystemUIService,所以两个类的日志都在这个 tag 下:
SystemUIApplication created.:已进入Application.onCreate。这条在super.onCreate()之后、DependencyInjection之前打印,不代表 Dagger 图在此刻才建。Starting SystemUI services for user 0.:SystemUIService已触发启动方法,开始逐个启动 Startable。- 15 起还有一条
Topological CoreStartables completed in N iterations,N 是拓扑循环的轮数。 - 若有组件超过 1 秒,会出现
Initialization of 类名 took ... ms。
前两条是 Log.v,AOSP userdebug 模拟器上通常能看到,user 版本以实际为准。
不开 Perfetto 也能看分段耗时。 TimingsTraceLog.traceEnd 会用 Slog.v 把每段耗时写进 system 缓冲区:adb logcat -b system -s SystemUIBootTiming SystemServerTiming。可以看到 system_server 侧的 StartSystemUI took to complete,以及 SystemUI 侧各段的 took to complete。Slog.v 在 user 版本上可能看不到,建议用 userdebug 镜像。
用 Perfetto 看耗时分布
按 Perfetto 官方文档「Tracing Android boot」一节的方法(Android 13 起支持):
- 写一份文本格式的配置
boottrace.pbtxt:在linux.ftrace数据源中,atrace_categories加am、wm、view、gfx,atrace_apps加com.android.systemui,duration_ms设为足以覆盖开机的值。 adb push boottrace.pbtxt /data/misc/perfetto-configs/boottrace.pbtxtadb shell setprop persist.debug.perfetto.boottrace 1,然后adb reboot。- 录制结束后执行
adb pull /data/misc/perfetto-traces/boottrace.perfetto-trace,用 ui.perfetto.dev 打开。文件在duration_ms到期、录制停止后才生成。
注意:开机 trace 要等 /data 挂载、持久属性加载、traced 启动后才开始,最早期的阶段抓不到。
在 com.android.systemui 主线程上找这些切片(SystemUIBootTiming 只是日志 tag,不是切片名):
bindApplication:进程初始化,包含 Provider 安装触发的 Dagger 建图(见「Dagger 图在哪一刻建立」一节)。DependencyInjection:onCreate中那次回调调用。StartServices:全部 Startable 的总耗时,下面嵌套各个StartServices <全类名>;15 起还有Topologically start Core Startables。- 各类名下的
Provider<全类名>.get()和<简单类名>.start():区分慢在构造还是慢在初始化。 <简单类名>.onBootCompleted():开机完成后各组件的补充工作。
这张图有两个用处:一是看清 SystemUI 自身的启动时间花在哪里;二是对照 Launcher 首帧,解释「为什么用户先看到桌面,后看到状态栏」。
观察 persistent 进程被拉回
前提:userdebug 镜像和 adb root。AOSP 或 Google APIs 这类模拟器镜像可以,带 Play 商店的镜像不行。
adb root,再adb wait-for-device(adb root会重启 adbd)。adb shell pidof com.android.systemui,记下 PID。adb shell kill $(adb shell pidof com.android.systemui)
状态栏和导航栏会短暂消失。按 16-qpr2 源码:AMS 清理进程时,persistent 应用会被加入 mPersistentStartingProcesses 并重启;ActiveServices.killServicesLocked 对进程中正在运行的 Service 调用 scheduleServiceRestartLocked,persistent 应用不受崩溃次数上限约束;SystemUIService 没有覆盖 onStartCommand,在 mStartCompatibility 为假时默认返回 START_STICKY。因此 SystemUIService 会被安排重启。重启的实际时机,以及「用 logcat 看启动顺序」一节的日志是否完整再出现一遍,以实测为准。
注意:短时间内连续崩溃可能触发 RescueParty(系统救援机制),只在测试镜像上做。
(可选)验证拓扑启动的依赖检查
如果你能编译 SystemUI,可以写一个测试用 Startable,用 @Dependencies 声明它依赖一个没有绑定的类,再刷机启动。按 15 起的源码,logcat 会打出 Failed to start ... Missing dependencies: [...],随后抛出 RuntimeException,进程反复崩溃。这个实验只适合在自己的模拟器上做。
排障清单:状态栏不显示,按链路逐段查
遇到「开机后没有状态栏」或「改完 SystemUI 后状态栏不出来」,按启动链从前往后排查,不要一上来就看布局:
- **
system_server是否走到startSystemUi**:看system_server的日志和开机 trace。如果systemReady本身卡住,问题不在 SystemUI。 - 组件名是否正确:确认
config_systemUIServiceComponent没有被 overlay 改成不存在的组件。组件不存在时 Service 起不来。 - 进程是否启动:
pidof com.android.systemui有没有输出?有输出但 PID 一直在变,说明进程在反复崩溃,去看 crash 日志。 - Dagger 图是否建好:崩溃栈落在
SystemUIInitializer.init或DependencyInjection段,多半是自定义 Initializer 或 module 有问题。 - Startable 是否全部启动:看
Starting SystemUI services之后有无报错;15 起重点看Missing dependencies。 - 状态栏是否注册:用
adb logcat -b system -s StatusBarManagerService查。新路径的日志是registerStatusBarForAllDisplays bar=...,老路径是registerStatusBar bar=...。两者都没有,就查负责注册的 Startable 是否被裁掉,或被 flag 换成了NOP(见「新路径的三个 Startable」一节)。 - 窗口是否加入:用
adb shell dumpsys window windows查标题为StatusBar的窗口(16-qpr2 起非默认屏为StatusBar(displayId=N)),再看它的可见性和 frame。高度不对时,回头检查改栏高的 RRO 是否以android为 target(见「用 RRO 改状态栏布局和高度」一节)。
每一步都有对应的日志或命令,能把「状态栏不显示」缩小到一个具体环节。
从 13–15 迁移到 16:旧代码要改什么
把基于 13–15 的 SystemUI 定制升级到 16 / 16-qpr2 时,最常遇到下面几处。这里只描述 AOSP 源码层面的差异。
- 状态栏启动换了负责方。以前在
CentralSurfacesImpl.start()中注册、加窗、提交 Fragment;16-qpr2 的新路径拆给CommandQueueInitializer、MultiDisplayStatusBarStarter、StatusBarOrchestrator、StatusBarInitializerImpl(见「状态栏如何变得可见」一节)。在CentralSurfacesImpl中插过代码的定制,要找到新的落点。 - 从单屏到多屏。注册改为
registerStatusBarForAllDisplays,返回按显示屏划分的结果;StatusBarInitializer、StatusBarOrchestrator、StatusBarWindowController都改为按显示屏创建,通过各自的 Store 获取(如StatusBarWindowControllerStore、StatusBarInitializerStore)。假设「只有一块屏、一个状态栏窗口」的代码需要复查。 - Fragment 让位给 Compose。
StatusBarRootModernization开启时没有CollapsedStatusBarFragment,依赖 Fragment 生命周期回调的定制会失效;视图回调改走HomeStatusBarComponent和OnStatusBarViewUpdatedListener。 - 类搬家。15 的
statusbar/window/StatusBarWindowController.java在 16 变为.kt接口加StatusBarWindowControllerImpl.java;16-qpr2 的 Application 是application/impl/SystemUIApplicationImpl.java(抽象父类application/SystemUIApplication.kt);CoreStartable移到packages/SystemUI/pods/。反射或测试中写死的类名和路径要一并修改。 - flag 双路径要两条都测。同一份代码在不同 release config 下走不同路径。验收前先确认目标构建中两个 flag 的取值,再按对应路径测试。
- 13–15 内部的变化依然有效:13 起清单由 Dagger Map 决定;14
CoreStartable变为接口,mContext被移除;15 引入@Dependencies拓扑启动,onConfigurationChanged从CoreStartable移除(需注入ConfigurationController注册监听);启动方法 14 是startServicesIfNeeded,15 起是startSystemUserServicesIfNeeded。12 的组件继承SystemUI、清单在config_systemUIServiceComponents数组中,仅在对比时提及。
迁移完成后用「动手验证」一节的方法验收:日志顺序是否正确,trace 中有无新增的慢组件,状态栏、通知、锁屏的冒烟测试是否通过。
常见误区
- 「SystemUI 等开机完成广播才启动」:错。进程在
systemReady阶段拉起,SystemUIService在systemReady返回后启动,开机完成后只是补调onBootCompleted()。另外,「startSystemUi在systemReady回调里」是 11 及以前的写法,12 到 17 都在回调外。 - 「Dagger 图在
SystemUIService里建」:错。图在进程初始化阶段由AppComponentFactory设下的回调和 Initializer 建好,Service 只负责触发启动。 - 「14 的 Startable 按依赖启动」:错。14 按类名排序逐个启动;依赖声明从 15 才开始。
- 「
config_systemUIFactoryComponent在 14 就没用了」:错。14 的默认 Factory 仍读取它(已弃用),15 才删除。 - 「
CommandQueue是 SystemUI 这边的 Binder 客户端」:错。它是IStatusBar.Stub,是服务端;客户端是 SBMS 持有的 proxy。 - 「状态栏一定由
CentralSurfaces注册、由CollapsedStatusBarFragment绘制」:在 16-qpr2 的新路径上不成立。注册在CommandQueueInitializer,加窗在StatusBarOrchestrator,根视图是 Compose。
关键路径速查(AOSP android16-qpr2-release)
| 符号 | 路径(frameworks/base/ 下) |
|---|---|
startSystemUi |
services/java/com/android/server/SystemServer.java |
config_systemUIServiceComponent |
core/res/res/values/config.xml |
| Manifest | packages/SystemUI/AndroidManifest.xml |
SystemUIAppComponentFactoryBase |
packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt |
SystemUIInitializer |
packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java |
SystemUIApplication(抽象) |
packages/SystemUI/src/com/android/systemui/application/SystemUIApplication.kt |
SystemUIApplicationImpl |
packages/SystemUI/src/com/android/systemui/application/impl/SystemUIApplicationImpl.java |
SystemUIService |
packages/SystemUI/src/com/android/systemui/SystemUIService.java |
CoreStartable |
packages/SystemUI/pods/src/com/android/systemui/CoreStartable.java |
| Startable 绑定 | packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt |
| 状态栏新路径绑定 | packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt |
CommandQueueInitializer |
packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt |
MultiDisplayStatusBarStarter |
packages/SystemUI/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarStarter.kt |
StatusBarOrchestrator |
packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarOrchestrator.kt |
StatusBarInitializer |
packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializer.kt |
StatusBarWindowController |
packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt(实现 StatusBarWindowControllerImpl.java) |
CentralSurfacesImpl |
packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java |
CommandQueue |
packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java |
@Dependencies |
packages/SystemUI/src/com/android/systemui/startable/Dependencies.kt |
| 依赖表声明 | packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java(@Multibinds)、dagger/SysUIComponent.java(getStartableDependencies()) |
CustomizationProvider |
packages/SystemUI/src/com/android/systemui/keyguard/CustomizationProvider.kt |
@PerUser(16 起在 pods/) |
packages/SystemUI/pods/ 下的 dagger/qualifiers/PerUser.java |
startPersistentApps |
services/core/java/com/android/server/am/ActivityManagerService.java |
killServicesLocked |
services/core/java/com/android/server/am/ActiveServices.java |
StatusBarManagerService |
services/core/java/com/android/server/statusbar/StatusBarManagerService.java |
handleBindApplication |
core/java/android/app/ActivityThread.java |
SystemBarUtils(栏高) |
core/java/com/android/internal/policy/SystemBarUtils.java |
状态栏
core/下几个类、StatusBarPhoneModule.kt、Application 和CoreStartable的新位置,均已在 android16-qpr2-release 分支核对;其余路径沿用 13–16 的核对结果。13–15 的差异:Application 是SystemUIApplication.java,CoreStartable在src/下,StatusBarWindowController是单个.java文件;SystemUIInitializerFactory.kt只存在于 14。其他分支请自行复核。
【面试口述骨架】(≤3 分钟)
- 定位(15 秒):SystemUI 是常驻、支持 Direct Boot 的系统进程,不是用户点开的 App,由 system_server 主动拉起。
- 谁拉起(35 秒):AMS 的 systemReady 先拉起常驻应用,SystemUI 进程通常这时就起来了,接着请求启动桌面。systemReady 返回后,SystemServer 调用 startSystemUi 启动 SystemUIService,再让窗口策略绑定锁屏。所以桌面可能先出来。
- 进程内初始化(35 秒):AppComponentFactory 给 Application 和 Provider 设了同一个回调。Provider 安装早于 onCreate,所以由 Provider 先触发建 Dagger 图,分 Root、WM Shell、SysUI 三层,只建一次。次用户进程只启动 PerUser 那部分。
- CoreStartable(40 秒):SystemUIService 在主线程串行启动所有 Startable,一个慢全都慢。演进:13 换成 Dagger Map,14 变接口,15 拓扑启动,16 加 NOP,让 flag 切换实现。
- 状态栏可见(35 秒):16 QPR2 上,CommandQueueInitializer 把 CommandQueue 这个 Binder 服务端按屏注册给 SBMS;StatusBarOrchestrator 以 TYPE_STATUS_BAR 加窗,根视图是 Compose。老版本这些都在 CentralSurfaces 里。
- 收尾(20 秒):先 start,后 onBootCompleted。排查时按 Start proc 日志、SystemUIBootTiming 耗时日志和 Perfetto 切片逐段定位。
追问清单(每条都能在正文找到依据)
- 为什么桌面可能先于状态栏出来? systemReady 中
startHomeOnAllDisplays早于startSystemUi;SystemUI 还要建图、串行启动全部 Startable,再由StatusBarOrchestrator加窗。(见「调用时机」「从startSystemUi到状态栏可见的时序」) - SystemUI 进程是被 Service 拉起的吗? 通常不是。它是 persistent + directBootAware,
startPersistentApps会为它发起进程启动;是否首次 fork 在这一步,看Start proc日志的 hosting 类型验证。(见「调用时机」「用 logcat 看启动顺序」) - Dagger 图只在
Application.onCreate里建吗? 不是。Provider 安装早于onCreate,16-qpr2 的CustomizationProvider.attachInfo、KeyguardSliceProvider都会触发同一个回调,图只建一次。(见「Dagger 图在哪一刻建立」) - 14 上换 Initializer 改哪里?15 呢? 14 可改
config_systemUIFactoryComponent(已弃用)或appComponentFactory;15 只能改appComponentFactory。(见「替换 Initializer 的入口」) CommandQueue在 Binder 里是什么角色?16 由谁注册它? 它是IStatusBar.Stub,即服务端;16-qpr2 由CommandQueueInitializer调用registerStatusBarForAllDisplays,按显示屏取回初始状态。SBMS 对 bar 调用linkToDeath,SystemUI 死亡后把mBar置空并通知 attach 状态变化。(见「注册:CommandQueueInitializer」)- 12 的组件纯靠反射创建吗? 不是。先从
SystemUIBinder的 Dagger Map 取,取不到才反射;13 的变化是清单来源换成 Dagger Map。(见「CoreStartable的版本演进」) - 某个 Startable 启动慢,怎么找? 看
Initialization of ... took警告、SystemUIBootTiming的took to complete日志,以及Provider<...>.get()和.start()两段切片,判断慢在构造还是初始化。(见「每个 Startable 怎么启动」「动手验证」) - 为什么 15 要引入拓扑启动? 14 及以前顺序只由类名决定,依赖靠碰巧;15 用
@Dependencies显式声明,缺依赖时启动失败并打印缺了什么。(见「CoreStartable的版本演进」) - 改状态栏高度为什么不生效? 要改
android包的status_bar_height_default,不是改status_bar_height这个别名,也不是改 SystemUI 包的资源。(见「用 RRO 改状态栏布局和高度」) - 锁屏和状态栏谁主动?16 的新旧状态栏路径怎么切换,加窗顺序有何不同? 状态栏由 SystemUI 主动注册(16-qpr2 是
registerStatusBarForAllDisplays);锁屏由 Framework 的PhoneWindowManager.bindKeyguard主动绑定 SystemUI 的服务。新旧状态栏路径由两个 flag 选择,Dagger 在同一个@ClassKey下返回真实对象或CoreStartable.NOP。新路径先 WMS 加窗、再ViewGroup.addView挂 Compose 视图,与老路径相反。(见「onSystemUiStarted:绑定锁屏服务」「新路径的三个 Startable」「根视图:StatusBarInitializer的 Compose 路径」)
17 的变化
以下只写 android17-release 公开源码中核对过的事实。第 2 条是 16-qpr2 起延续到 17 的内容,其余是 17 的新变化。
- 未变的部分:
startSystemUi的时机和函数体、AMSsystemReady的顺序、config_systemUIServiceComponent、PhoneWindowManager.bindKeyguard。 - Application 拆分延续:16-qpr2 起的抽象类加实现类(见「Manifest 的三个关键属性」一节)、
ApplicationContextInitializer/ContentProviderContextInitializer两个接口(见「Dagger 图在哪一刻建立」一节),在 17 保持不变。 CoreStartable改为 Kotlin:packages/SystemUI/pods/src/api/CoreStartable.kt,fun interface CoreStartable : Dumpable,带NOP,包名仍是com.android.systemui。- 拓扑启动逻辑不变,
mServices、mServicesStarted改为原子类型。新增 flaghandleBootCompletedOnSeparateThread:开启时在后台线程接收开机完成广播,onBootCompleted()再回到主线程派发。flag 取值:android17-release 的platform/build/release中,trunk_staging 是 ENABLED(READ_WRITE);cp1a、cp2a 没有值文件,构建时取 aconfig 默认值 DISABLED。 - 状态栏只剩新路径:两个状态栏 flag 已从代码中删除;
CommandQueueInitializer无条件调用registerStatusBarForAllDisplays;StatusBarInitializerImpl只保留 Compose 路径,源码注释写明不再使用 Fragment 事务;CentralSurfacesImpl中已没有registerStatusBar。 - 状态栏 Startable 的绑定方式:仍在
packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt。MultiDisplayStatusBarStarter、CommandQueueInitializer改为@Binds @SysUISingleton @IntoMap @ClassKey(...)直接绑定,不再按 flag 返回NOP;StatusBarInitializer不再以@ClassKey进入启动表,只由MultiDisplayStatusBarStarter按屏启动。android17-release 的packages/SystemUI已查范围内,同样没有约束这几个类先后的@Dependencies条目。 - 工厂类搬家:
PhoneSystemUIAppComponentFactory移到packages/SystemUI/application/src/。 - 排障命令随之变化:SBMS 日志看
registerStatusBarForAllDisplays bar=;多屏时窗口标题为StatusBar(displayId=N)。

