目录
  1. 1. 要点摘要
  2. 2. 进程与职责总览
  3. 3. 谁启动 SystemUI:systemReady → startSystemUi
    1. 3.1. 调用时机
    2. 3.2. 函数本体
    3. 3.3. onSystemUiStarted:绑定锁屏服务
    4. 3.4. 与 BOOT_COMPLETED 的关系
  4. 4. 进程起来之后:Dagger 图在哪里建
    1. 4.1. Manifest 的三个关键属性
    2. 4.2. Dagger 图在哪一刻建立
    3. 4.3. 替换 Initializer 的入口:按版本区分
    4. 4.4. 系统用户与次用户分叉
  5. 5. 常驻入口:SystemUIService → CoreStartable
    1. 5.1. SystemUIService.onCreate
    2. 5.2. 启动哪些 Startable
    3. 5.3. 每个 Startable 怎么启动
    4. 5.4. CoreStartable 的版本演进
  6. 6. 状态栏如何变得可见
    1. 6.1. 新路径的三个 Startable
    2. 6.2. 注册:CommandQueueInitializer
    3. 6.3. 加窗:MultiDisplayStatusBarStarter → StatusBarOrchestrator
    4. 6.4. 根视图:StatusBarInitializer 的 Compose 路径
    5. 6.5. 老路径(16 首发默认、13–15)
    6. 6.6. 从 startSystemUi 到状态栏可见的时序(16-qpr2 新路径)
    7. 6.7. 如何测量「状态栏比桌面晚出现」
  7. 7. 其他表面:同一进程,各自是 Startable
  8. 8. 公开扩展点(基于 AOSP 源码,可在模拟器复现)
    1. 8.1. 用 RRO 改状态栏布局和高度
    2. 8.2. 换 Initializer,增加或替换 Startable
    3. 8.3. 裁剪不需要的 Startable
    4. 8.4. 多用户
  9. 9. 动手验证:在模拟器上走一遍这条链
    1. 9.1. 用 logcat 看启动顺序
    2. 9.2. 用 Perfetto 看耗时分布
    3. 9.3. 观察 persistent 进程被拉回
    4. 9.4. (可选)验证拓扑启动的依赖检查
  10. 10. 排障清单:状态栏不显示,按链路逐段查
  11. 11. 从 13–15 迁移到 16:旧代码要改什么
  12. 12. 常见误区
  13. 13. 关键路径速查(AOSP android16-qpr2-release)
  14. 14. 【面试口述骨架】(≤3 分钟)
  15. 15. 17 的变化
SystemUI 系列(一):启动与 CoreStartable

要点摘要

  • 问题:开机时状态栏由谁拉起?为什么它不等 BOOT_COMPLETED?定制后状态栏不显示,该从哪一段查起?
  • 结论:SystemUI 是 persistent 的系统进程。AMS systemReady 先通过 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):

  1. startPersistentApps(PackageManager.MATCH_DIRECT_BOOT_AWARE):对每个 persistent 且 directBootAware 的应用调用 addAppLocked;进程未运行时,以 hostingType added application 发起进程启动。SystemUI 两个属性都具备。
  2. 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),节选
PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class);
Intent intent = new Intent();
intent.setComponent(pm.getSystemUiServiceComponent());
intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
context.startServiceAsUser(intent, UserHandle.SYSTEM);
windowManager.onSystemUiStarted();

要点:

  • 组件名不是写死的,而是取自 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 的实际机制如下:

  1. 系统通过 appComponentFactory 实例化 Application 时,进入 SystemUIAppComponentFactoryBase.instantiateApplicationCompat。16-qpr2 要求 Application 实现 ApplicationContextInitializer,否则抛出「App must implement ApplicationContextInitializer」;随后为它设置 ApplicationContextAvailableCallback。

  2. 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。

  3. 回调内部调用子类实现的 createSystemUIInitializer(context),再执行 initializer.init(false)。结果缓存在静态字段中,整个进程只建一次。

  4. SystemUIInitializer.init 先建 GlobalRootComponent,再建 WMComponent(WM Shell),把 Shell 提供的 Pip、SplitScreen、Bubbles 等接口传给 SysUIComponent.Builder,建出 SysUIComponent,最后调用 Dependency.start()。

  5. 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

启动方法合并三类来源:

  1. 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」一节)。
  2. mSysUIComponent.getPerUserStartables():带 @PerUser 限定的子集,次用户也会启动。
  3. 可选的厂商组件: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):

  1. 把所有未启动的 Startable 按 TreeMap 顺序放进队列。
  2. 依次取出:依赖都已启动(或没有依赖)的立即启动,否则放进下一轮队列。
  3. 本轮有进展且下一轮队列非空,就继续循环。
  4. 结束时若仍有未启动的,逐个打印缺失的依赖,再抛出 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() 做三件事:

  1. barService.registerStatusBarForAllDisplays(commandQueue):一次把 CommandQueue 注册给 SBMS,返回 Map<String, RegisterStatusBarResult>,key 是显示屏 ID。
  2. 对默认显示屏:创建导航栏,按结果中的 mIcons 逐个调用 commandQueue.setIcon(...) 恢复图标。
  3. 对每块显示屏:恢复瞬态栏、系统栏外观和 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 发出当前值,之后每出现新的显示屏,依次:

  1. multiDisplayStatusBarOrchestratorStore.forDisplay(id)?.start()
  2. statusBarInitializerStore.forDisplay(id)?.start()
  3. 非默认屏启动隐私指示点窗口;创建该屏的 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() 负责:

  1. mBarService.registerStatusBar(mCommandQueue),取回单个 RegisterStatusBarResult。16 的源码把这句包在 if (!StatusBarConnectedDisplays.isEnabled()) 中。
  2. createAndAddWindows(result):makeStatusBarView 调用 StatusBarInitializer.initializeStatusBar() 提交 CollapsedStatusBarFragment 事务;随后 attach 通知面板窗口和状态栏窗口(16 写作 mStatusBarWindowControllerStore.getDefaultDisplay().attach(),15 是 mStatusBarWindowController.attach())。
  3. 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 图和主线程。

这带来两个后果:

  1. 一处慢,处处慢。所有 start() 在主线程串行执行(见「每个 Startable 怎么启动」一节),任何一个 Startable 做重活,都会推迟其后所有表面,包括状态栏。
  2. 一处崩,全部消失。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 下:

  1. SystemUIApplication created.:已进入 Application.onCreate。这条在 super.onCreate() 之后、DependencyInjection 之前打印,不代表 Dagger 图在此刻才建。
  2. Starting SystemUI services for user 0.:SystemUIService 已触发启动方法,开始逐个启动 Startable。
  3. 15 起还有一条 Topological CoreStartables completed in N iterations,N 是拓扑循环的轮数。
  4. 若有组件超过 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 起支持):

  1. 写一份文本格式的配置 boottrace.pbtxt:在 linux.ftrace 数据源中,atrace_categories 加 am、wm、view、gfx,atrace_apps 加 com.android.systemui,duration_ms 设为足以覆盖开机的值。
  2. adb push boottrace.pbtxt /data/misc/perfetto-configs/boottrace.pbtxt
  3. adb shell setprop persist.debug.perfetto.boottrace 1,然后 adb reboot。
  4. 录制结束后执行 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 商店的镜像不行。

  1. adb root,再 adb wait-for-device(adb root 会重启 adbd)。
  2. adb shell pidof com.android.systemui,记下 PID。
  3. 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 后状态栏不出来」,按启动链从前往后排查,不要一上来就看布局:

  1. **system_server 是否走到 startSystemUi**:看 system_server 的日志和开机 trace。如果 systemReady 本身卡住,问题不在 SystemUI。
  2. 组件名是否正确:确认 config_systemUIServiceComponent 没有被 overlay 改成不存在的组件。组件不存在时 Service 起不来。
  3. 进程是否启动:pidof com.android.systemui 有没有输出?有输出但 PID 一直在变,说明进程在反复崩溃,去看 crash 日志。
  4. Dagger 图是否建好:崩溃栈落在 SystemUIInitializer.init 或 DependencyInjection 段,多半是自定义 Initializer 或 module 有问题。
  5. Startable 是否全部启动:看 Starting SystemUI services 之后有无报错;15 起重点看 Missing dependencies。
  6. 状态栏是否注册:用 adb logcat -b system -s StatusBarManagerService 查。新路径的日志是 registerStatusBarForAllDisplays bar=...,老路径是 registerStatusBar bar=...。两者都没有,就查负责注册的 Startable 是否被裁掉,或被 flag 换成了 NOP(见「新路径的三个 Startable」一节)。
  7. 窗口是否加入:用 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 源码层面的差异。

  1. 状态栏启动换了负责方。以前在 CentralSurfacesImpl.start() 中注册、加窗、提交 Fragment;16-qpr2 的新路径拆给 CommandQueueInitializer、MultiDisplayStatusBarStarter、StatusBarOrchestrator、StatusBarInitializerImpl(见「状态栏如何变得可见」一节)。在 CentralSurfacesImpl 中插过代码的定制,要找到新的落点。
  2. 从单屏到多屏。注册改为 registerStatusBarForAllDisplays,返回按显示屏划分的结果;StatusBarInitializer、StatusBarOrchestrator、StatusBarWindowController 都改为按显示屏创建,通过各自的 Store 获取(如 StatusBarWindowControllerStore、StatusBarInitializerStore)。假设「只有一块屏、一个状态栏窗口」的代码需要复查。
  3. Fragment 让位给 Compose。StatusBarRootModernization 开启时没有 CollapsedStatusBarFragment,依赖 Fragment 生命周期回调的定制会失效;视图回调改走 HomeStatusBarComponent 和 OnStatusBarViewUpdatedListener。
  4. 类搬家。15 的 statusbar/window/StatusBarWindowController.java 在 16 变为 .kt 接口加 StatusBarWindowControllerImpl.java;16-qpr2 的 Application 是 application/impl/SystemUIApplicationImpl.java(抽象父类 application/SystemUIApplication.kt);CoreStartable 移到 packages/SystemUI/pods/。反射或测试中写死的类名和路径要一并修改。
  5. flag 双路径要两条都测。同一份代码在不同 release config 下走不同路径。验收前先确认目标构建中两个 flag 的取值,再按对应路径测试。
  6. 13–15 内部的变化依然有效:13 起清单由 Dagger Map 决定;14 CoreStartable 变为接口,mContext 被移除;15 引入 @Dependencies 拓扑启动,onConfigurationChanged 从 CoreStartable 移除(需注入 ConfigurationController 注册监听);启动方法 14 是 startServicesIfNeeded,15 起是 startSystemUserServicesIfNeeded。12 的组件继承 SystemUI、清单在 config_systemUIServiceComponents 数组中,仅在对比时提及。

迁移完成后用「动手验证」一节的方法验收:日志顺序是否正确,trace 中有无新增的慢组件,状态栏、通知、锁屏的冒烟测试是否通过。


常见误区

  1. 「SystemUI 等开机完成广播才启动」:错。进程在 systemReady 阶段拉起,SystemUIService 在 systemReady 返回后启动,开机完成后只是补调 onBootCompleted()。另外,「startSystemUi 在 systemReady 回调里」是 11 及以前的写法,12 到 17 都在回调外。
  2. 「Dagger 图在 SystemUIService 里建」:错。图在进程初始化阶段由 AppComponentFactory 设下的回调和 Initializer 建好,Service 只负责触发启动。
  3. 「14 的 Startable 按依赖启动」:错。14 按类名排序逐个启动;依赖声明从 15 才开始。
  4. 「config_systemUIFactoryComponent 在 14 就没用了」:错。14 的默认 Factory 仍读取它(已弃用),15 才删除。
  5. 「CommandQueue 是 SystemUI 这边的 Binder 客户端」:错。它是 IStatusBar.Stub,是服务端;客户端是 SBMS 持有的 proxy。
  6. 「状态栏一定由 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 分钟)

  1. 定位(15 秒):SystemUI 是常驻、支持 Direct Boot 的系统进程,不是用户点开的 App,由 system_server 主动拉起。
  2. 谁拉起(35 秒):AMS 的 systemReady 先拉起常驻应用,SystemUI 进程通常这时就起来了,接着请求启动桌面。systemReady 返回后,SystemServer 调用 startSystemUi 启动 SystemUIService,再让窗口策略绑定锁屏。所以桌面可能先出来。
  3. 进程内初始化(35 秒):AppComponentFactory 给 Application 和 Provider 设了同一个回调。Provider 安装早于 onCreate,所以由 Provider 先触发建 Dagger 图,分 Root、WM Shell、SysUI 三层,只建一次。次用户进程只启动 PerUser 那部分。
  4. CoreStartable(40 秒):SystemUIService 在主线程串行启动所有 Startable,一个慢全都慢。演进:13 换成 Dagger Map,14 变接口,15 拓扑启动,16 加 NOP,让 flag 切换实现。
  5. 状态栏可见(35 秒):16 QPR2 上,CommandQueueInitializer 把 CommandQueue 这个 Binder 服务端按屏注册给 SBMS;StatusBarOrchestrator 以 TYPE_STATUS_BAR 加窗,根视图是 Compose。老版本这些都在 CentralSurfaces 里。
  6. 收尾(20 秒):先 start,后 onBootCompleted。排查时按 Start proc 日志、SystemUIBootTiming 耗时日志和 Perfetto 切片逐段定位。

追问清单(每条都能在正文找到依据)

  1. 为什么桌面可能先于状态栏出来? systemReady 中 startHomeOnAllDisplays 早于 startSystemUi;SystemUI 还要建图、串行启动全部 Startable,再由 StatusBarOrchestrator 加窗。(见「调用时机」「从 startSystemUi 到状态栏可见的时序」)
  2. SystemUI 进程是被 Service 拉起的吗? 通常不是。它是 persistent + directBootAware,startPersistentApps 会为它发起进程启动;是否首次 fork 在这一步,看 Start proc 日志的 hosting 类型验证。(见「调用时机」「用 logcat 看启动顺序」)
  3. Dagger 图只在 Application.onCreate 里建吗? 不是。Provider 安装早于 onCreate,16-qpr2 的 CustomizationProvider.attachInfo、KeyguardSliceProvider 都会触发同一个回调,图只建一次。(见「Dagger 图在哪一刻建立」)
  4. 14 上换 Initializer 改哪里?15 呢? 14 可改 config_systemUIFactoryComponent(已弃用)或 appComponentFactory;15 只能改 appComponentFactory。(见「替换 Initializer 的入口」)
  5. CommandQueue 在 Binder 里是什么角色?16 由谁注册它? 它是 IStatusBar.Stub,即服务端;16-qpr2 由 CommandQueueInitializer 调用 registerStatusBarForAllDisplays,按显示屏取回初始状态。SBMS 对 bar 调用 linkToDeath,SystemUI 死亡后把 mBar 置空并通知 attach 状态变化。(见「注册:CommandQueueInitializer」)
  6. 12 的组件纯靠反射创建吗? 不是。先从 SystemUIBinder 的 Dagger Map 取,取不到才反射;13 的变化是清单来源换成 Dagger Map。(见「CoreStartable 的版本演进」)
  7. 某个 Startable 启动慢,怎么找? 看 Initialization of ... took 警告、SystemUIBootTiming 的 took to complete 日志,以及 Provider<...>.get() 和 .start() 两段切片,判断慢在构造还是初始化。(见「每个 Startable 怎么启动」「动手验证」)
  8. 为什么 15 要引入拓扑启动? 14 及以前顺序只由类名决定,依赖靠碰巧;15 用 @Dependencies 显式声明,缺依赖时启动失败并打印缺了什么。(见「CoreStartable 的版本演进」)
  9. 改状态栏高度为什么不生效? 要改 android 包的 status_bar_height_default,不是改 status_bar_height 这个别名,也不是改 SystemUI 包的资源。(见「用 RRO 改状态栏布局和高度」)
  10. 锁屏和状态栏谁主动?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 的新变化。

  1. 未变的部分:startSystemUi 的时机和函数体、AMS systemReady 的顺序、config_systemUIServiceComponent、PhoneWindowManager.bindKeyguard。
  2. Application 拆分延续:16-qpr2 起的抽象类加实现类(见「Manifest 的三个关键属性」一节)、ApplicationContextInitializer / ContentProviderContextInitializer 两个接口(见「Dagger 图在哪一刻建立」一节),在 17 保持不变。
  3. CoreStartable 改为 Kotlin:packages/SystemUI/pods/src/api/CoreStartable.kt,fun interface CoreStartable : Dumpable,带 NOP,包名仍是 com.android.systemui。
  4. 拓扑启动逻辑不变,mServices、mServicesStarted 改为原子类型。新增 flag handleBootCompletedOnSeparateThread:开启时在后台线程接收开机完成广播,onBootCompleted() 再回到主线程派发。flag 取值:android17-release 的 platform/build/release 中,trunk_staging 是 ENABLED(READ_WRITE);cp1a、cp2a 没有值文件,构建时取 aconfig 默认值 DISABLED。
  5. 状态栏只剩新路径:两个状态栏 flag 已从代码中删除;CommandQueueInitializer 无条件调用 registerStatusBarForAllDisplays;StatusBarInitializerImpl 只保留 Compose 路径,源码注释写明不再使用 Fragment 事务;CentralSurfacesImpl 中已没有 registerStatusBar。
  6. 状态栏 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 条目。
  7. 工厂类搬家:PhoneSystemUIAppComponentFactory 移到 packages/SystemUI/application/src/。
  8. 排障命令随之变化:SBMS 日志看 registerStatusBarForAllDisplays bar=;多屏时窗口标题为 StatusBar(displayId=N)。
文章作者: Leo·Cheung
文章链接: http://tufusi.com/systemui/01-startup/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ONE·PIECE!
打赏
  • 微信
  • 支付宝

评论
简