
在安卓RAndroid 11上啃SystemUI源码有一个绕不开的状态机Keyguard的occluded被遮挡变化流程。平时大家关注比较多的是Keyguard怎么显示、怎么dismiss很少有人专门把occluded这条链路单独拉出来讲。但这个状态恰恰是很多疑难杂症的源头比如来电全屏界面盖住锁屏、某个应用希望隐藏锁屏但又不希望解锁、以及锁屏截图黑屏这些奇奇怪怪的问题追根溯源都指向occluded的变化逻辑。我花了不少时间把这套流程从API入口到SystemUI内部通知、再到WindowManager侧的旗标变更完整捋了一遍发现它比想象中更有意思也更容易踩坑。这里把整个流转过程整理出来配合一些实测经验和排查思路给同样在Android R上做系统定制或SystemUI二次开发的朋友做个参考。1. occluded到底是什么意思1.1 先从使用场景理解这个概念occluded在英文里的意思是“被遮挡的”在锁屏的场景下很好理解当某个全屏界面最常见的例子是来电界面覆盖在锁屏之上时当前锁屏虽然还在底层但实际上用户已经看不到它了这时候Keyguard就处于occluded状态。有一点必须强调occluded不等于dismissed。dismiss意味着锁屏被解除用户进入桌面而occluded只是被别的窗口盖住了锁屏本身还“活着”只是看不见。这个区别在后文分析状态回切时会反复出现。实际开发中occluded最常见的入口有两个第三方应用申请隐藏锁屏比如视频播放器在全屏播放时申请隐藏锁屏让用户能直接操作返回键而非先解锁。Telephony层来电全屏界面通话过程中不需要用户先解锁来电界面本身就是全屏且优先级更高此时锁屏要进入occluded状态。1.2 为什么需要专门维护一个occluded状态锁屏不仅仅是画了个界面它背后牵扯到安全策略、指纹/人脸解锁流程、通知栏展开策略、window flag输出给WindowManager的联动逻辑等。如果只是简单地把锁屏View盖住而不通知其他模块状态变化就会出现各种冲突锁屏还认为自己在前台去抢焦点或拦截触摸事件通知栏逻辑不知道锁屏已被遮挡下拉时出现重叠WindowManager侧不知道该不该给窗口加锁屏相关的flag。所以occluded需要作为Keyguard状态机的一部分被系统性地管理。简单说这个状态承载的是“锁屏还在但被更高层级的界面完全盖住”的核心语义。2. SystemUI侧的核心链路拆解2.1 谁是这套流程的主心骨在Android R里Keyguard的状态管理主要围绕几个核心类转KeyguardViewController负责KeyguardView的创建、显示、与WindowManager的绑定KeyguardViewMediator全局状态流转的总控制任何状态变化都会经过它做仲裁KeyguardOcclusionController专门维护occluded状态的控制器监听来源变化并分发结果NotificationShadeWindowController/StatusBarWindowController负责和WindowManager层沟通窗口状态、flag的写入方。occluded变化流程大体可以描述为外部触发者比如Telephony或者第三方应用发起请求 → 通知到KeyguardViewMediator → KeyguardViewController和KeyguardOcclusionController联动更新 → 通过NotificationShadeWindowController把最终状态同步到WindowManager。2.2 从API入口看一次调用最终去了哪里在Android R源码中Keyguard控制的主要对外接口是StatusBarManagerService或PhoneWindowManager间接调用进去的。以setOccluded为例最终会走到KeyguardViewMediator.setOccluded(boolean isOccluded, boolean animate)。这里要注意setOccluded有两个重载版本区别在于是否带动画。系统内的来电界面通常希望锁屏消失或出现时带动画铺满而纯状态切换时则不带动画。这个动画开关会影响到后续NotificationShadeWindowController内部对StatusBarState和窗口转场动画的处理。流程内部会调用// KeyguardViewMediator内部 mKeyguardViewController.setOccluded(occluded, animate);KeyguardViewController携带occluded信息更新KeyguardOcclusionController里的状态值同时通知NotificationShadeWindowController做同步例如setKeyguardOccluded的调用。2.3 StateController在其中的角色Android R的SystemUI里有一个统一的状态机叫StatusBarStateController部分版本是CentralSurfaces负责它维护了KEYGUARD、SHADE、SHADE_LOCKED几个状态。occluded变化会引发状态机的切换举个常见的组合当前状态是KEYGUARD收到occludedtrue时状态机暂时保持KEYGUARD但所有与用户交互相关的策略进入“挂起”状态如果occluded变化的同时发生了解锁KEYGUARD会切到SHADE并立即dismiss锁屏occluded随之被清理为false。代码层面occluded对状态机的影响往往体现在mStatusBarStateController.setKeyguardOccluded(occluded);这一步会回调给所有注册了StateListener的组件包括通知面板、快捷开关面板、导航栏。这些组件收到回调后自行调整自己的可交互状态。比如通知面板在锁屏遮挡时不应该响应用户下拉手势此时它收到回调后会把NotificationsPanelView的触摸响应给裁剪掉。2.4 一次典型的变化时序以第三方应用申请occlude为例完整时序可以归纳如下应用通过StatusBarManager.disable或WindowManager.LayoutParams的FLAG_SHOW_WHEN_LOCKED请求全屏显示PhoneWindowManager根据窗口属性判断需要通知SystemUI调用KeyguardViewMediator.setOccludedKeyguardViewController更新锁屏View的可见性策略同时调用KeyguardOcclusionController保存最新的occluded值StatusBarStateController广播状态到通知栏、快捷设置等组件NotificationShadeWindowController将occluded语义翻译成WindowManager能识别的窗口flag通过mWindowManager.updateViewLayout()提交。最终在WMS层面锁屏窗口会带上类似SYSTEM_FLAG_ALWAYS_ON_TOP之类的属性但真正让它被盖住的是上层全屏窗口的存在occluded只是系统性确认这个事实。3. 那些容易忽略的内部细节3.1 KeyguardOcclusionController的存续期小陷阱排查问题期间发现一个比较邪门的地方KeyguardOcclusionController的实例在很多定制ROM里会被重复创建。一旦重复创建注册的listener可能出现错乱导致occluded回调被重复派发甚至出现“明明已经取消遮挡锁屏还处于隐藏态”的诡异现象。原因是Android R在KeyguardViewController初始化和配置变更比如屏幕旋转时都会触发现有实例的销毁重建。如果依赖注入关系没有梳理干净listener没有在onDestroy时及时解绑后续就会连环踩雷。实测建议在定制代码里新增对occluded状态监听的模块时务必在onStart里注册、onStop里反注册并且使用LifecycleBoundObserver或等效机制避免重复注册回调。3.2 WindowManager侧的表现方式occluded状态从SystemUI传递到WindowManager之后WindowManager并不会直接感知“锁屏被遮挡”这个概念而是通过窗口顺序和FLAG_SHOW_WHEN_LOCKED等机制配合实现。在R上锁屏窗口本身是通过StatusBarWindowView承载的它对应一个ViewRootImplWMS侧的类型通常是TYPE_STATUS_BAR或TYPE_NAVIGATION_BAR这些系统级窗口。来电这类全屏窗口类型级别更高从而在层级上天然压住锁屏。occluded在这里的意义更多是决策依据WindowManager知道“既然锁屏被全屏窗口遮挡了那么系统返回键和某些焦点事件可以直接穿过”避免不必要的触摸重定向。3.3 occluded与OffScreen的相互作用在Android 11里绕不开的一个话题是offScreen与occluded的区别。简单说offScreen表示锁屏窗口被移到了屏幕外比如用户解锁后锁屏从当前显示移除occluded表示锁屏窗口仍然在屏幕上但被全屏窗口挡住。这两个状态在某些情况下会互相转化。典型场景用户正在通话中锁屏被来电界面occlude通话挂断后全屏界面退出occluded返回false但此时锁屏不一定重新可见——如果在此之前用户已经完成了生物识别解锁系统会直接走onKeyguardDismissed路径把锁屏彻底关闭。这个转化过程中最常踩的坑是只监听setOccluded(false)就想当然认为锁屏会重新显示结果发现KeyguardView还在锁屏却黑屏了。正确的做法是根据KeyguardViewMediator里当前mShowing、mHiding和用户解锁状态综合判断不要只盯着occluded一个变量。4. 实战中的特定场景解析4.1 场景一来电界面遮挡锁屏这是最经典的occluded场景也是最容易复现问题的测试路径。正常流程是系统收到来电 → TelephonyRegistry通知SystemUI → 系统UI通过PhoneWindowManager内部逻辑让来电界面显示在最高层级 → 触发setOccluded(true)。在Android R上来电界面走的是CallNotification配合SystemUI内部的StatusBar策略。这里有个细节值得说来电界面并不是直接调setOccluded的而是通过启动一个全屏的Activity如InCallActivity这个Activity的window在PhoneWindowManager里被判为canShowWhenLocked属性从而回调给Keyguard状态机。我的实测经验是这个场景下最容易出现的问题是动画冲突。如果锁屏正在做指纹解锁动画同时来电界面请求occluded两个动画会抢资源出现解锁动画卡在半路、来电界面白屏几秒的情况。排查方向是检查WindowManager侧的转场动画是否被设置为TRANSIT_OLD_WALLPAPER这种高开销动画。4.2 场景二视频应用申请沉浸式隐藏锁屏很多视频App在播放时会设置FLAG_SHOW_WHEN_LOCKED来避免锁屏打断观影体验。开发者申请这个flag时锁屏会走occluded流程。这个场景里真正的风险在“取消遮挡”的一刻。视频App退出全屏或播放结束时如果应用直接清了flag但系统UI还没来得及把锁屏恢复显示用户会看到一个短暂的黑屏或桌面闪烁。Android R的优化是让KeyguardViewMediator在setOccluded(false)时判断当前是否处于屏保状态如果是则延迟恢复锁屏避免闪烁。开发者在方案设计时建议预留至少300毫秒的重叠时间让锁屏View的alpha先恢复再让全屏窗口退场能有效减少视觉突兀感。4.3 场景三锁屏截图黑屏问题很多人遇到过在occluded状态下按电源键截图截出来是黑屏。原因是锁屏层被遮挡后截屏的buffer抓的是最上层全屏窗口内容但这个过程有时会出现窗口同步问题导致抓到的是被遮挡中的锁屏窗口而锁屏窗口为了省电可能已经停止渲染。Android R引入了一个补丁逻辑在occluded true时锁屏View会跳过surface更新直接复用之前的buffer。这本来是为了省电但如果上层窗口也是全黑或者还没准备就绪就会截到黑图。排查方法很直接抓取dumpsys window看锁屏窗口的mHasSurface标志。如果为false基本就是渲染被挂起导致。复现时可用命令缩短周期adb shell dumpsys window | grep -E mHasSurface|occluded|StatusBar4.4 场景四多用户切换时的状态残留Android R的多用户切换会触发SystemUI重建而系统在切换完成前会做一轮锁屏状态重置。如果切换期间occluded状态没有被正确还原新用户进入桌面时会遇到“锁屏消失了但状态栏还显示锁屏图标”的诡异情况。根因通常是UserSwitch通知与setOccluded(false)时序交错occluded的清理回调落在用户切换广播之前导致KeyguardOcclusionController以为还在遮挡状态新用户自然看不到锁屏。解决方案是在BootReceiver或UserController的onUserSwitchComplete回调里主动调用一次setOccluded(false)强制复位。5. 常见问题速查与调试技巧5.1 常见问题对照表现象可能原因排查命令/思考方向来电全屏界面卡白屏occlusion和dismiss动画冲突检查WindowManager转场动画类型occludedfalse但锁屏不恢复用户已解锁或dismiss流程已走完看mShowing、mHiding和生物识别状态锁屏截图黑屏锁屏surface渲染挂起dumpsys window查mHasSurface通知栏在锁屏遮挡时还能下拉occluded回调没有传到状态机检查listener是否重复注册/漏注册多用户切换后锁屏状态错乱occluded清理时序早于用户切换在onUserSwitchComplete里强制复位取消遮挡时屏幕闪烁全屏窗口退场和锁屏恢复不同步考虑alpha渐变叠加或延迟恢复5.2 高效调试指令调试这套流程我最常用的命令是# 查看锁屏状态和遮挡状态 adb shell dumpsys statusbar | grep -E Keyguard|occluded # 查看WindowManager侧的窗口信息 adb shell dumpsys window windows | grep -E Window|mCurrentFocus|mObscured # 查看SystemUI状态 adb shell dumpsys activity service com.android.systemui/.SystemUIService | grep -i keyguard如果怀疑是动画问题可以临时关闭动画来快速验证adb shell settings put global window_animation_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global animator_duration_scale 05.3 一个值得养成的习惯强烈建议在KeyguardViewMediator的setOccluded方法入口处加自定义日志把调用来源、当前mState、mShowing、mHiding、occluded变化值全部打出来。这个入口几乎是所有锁屏遮挡问题的交汇点日志一开很多问题直接水落石出。我自己实际用下来只要在这里加一行Log.d(TAG, setOccluded: occluded occluded animate animate state mState showing mShowing hiding mHiding);配合系统的dumpsys statusbar大部分occluded相关的问题都能在十分钟内定位到具体模块。6. 从Android R延伸到更高版本的差异速览Android R之后的S、T、U系列版本occluded的状态管理有了不少变化这里罗列几个影响较大的点供参考。6.1 从StatusBarStateController到CentralSurfaces在Android 12之后SystemUI做了大规模重构原先的StatusBarStateController逐步被CentralSurfaces取代occluded状态的存储和回调也一并迁移。所以如果你的代码是基于R写的升级到更高版本时第一件事就是核对API类的包路径。6.2 引入OccludingLayer和自定义布局Android 12还引入了OccludingLayer的概念让锁屏支持被部分遮挡比如只有底部导航区域被某个全屏应用覆盖。这类变更让occluded不再是一个单纯的布尔值问题而是一个几何区域计算问题调试维度完全不同。6.3 安全策略增强更高版本在设备锁屏策略上对FLAG_SHOW_WHEN_LOCKED的限制更严格比如要求应用需要具备特定权限或用户明确授权后才能隐藏锁屏。开发者在定制ROM时要特别留意系统包对permission的校验时序。不过基础流程的原理并没有变外部来源触发 → KeyguardViewMediator仲裁 → KeyguardViewController刷新状态 → NotificationShadeWindowController同步给WindowManager。理解了R上的这条链路再往上走会轻松很多。7. 结束语与个人实操心得最后分享几个我在实际调试occluded流程时攒下的经验。第一动代码之前先把dumpsys statusbar的输出研究透。这个命令会给出当前keyguard的核心状态快照包括occluded是否生效、前后台状态、window state等能省下大量盲调时间。第二测试时一定要覆盖“快速连续变化”的场景。比如通话挂断又立刻来电、指纹解锁进行中来电这种并发场景很多时序bug都是在这种极限切换下爆出来的。自己写自动化测试脚本时优先模拟这些操作。第三多留意SystemUI日志里关于KeyguardOcclusionController和KeyguardViewController的ANR和warning。它们通常意味着Listener回调阻塞了主线程尤其是自定义ROM加了商用模块之后这个问题会被放大。对我来说Android R这套occluded流程最吸引人的地方在于它不是一个孤立的开关而是一个横跨WindowManager、SystemUI状态机、锁屏控制器、窗口转场动画的综合性状态协调机制。把这条链路吃透对整个Android系统UI的运作方式都会有更深的理解。