在安卓R(Android 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,收到occluded=true时,状态机暂时保持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.setOccluded;KeyguardViewController更新锁屏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承载的,它对应一个ViewRootImpl,WMS侧的类型通常是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|StatusBar"4.4 场景四:多用户切换时的状态残留
Android R的多用户切换会触发SystemUI重建,而系统在切换完成前会做一轮锁屏状态重置。如果切换期间occluded状态没有被正确还原,新用户进入桌面时会遇到“锁屏消失了但状态栏还显示锁屏图标”的诡异情况。
根因通常是UserSwitch通知与setOccluded(false)时序交错:occluded的清理回调落在用户切换广播之前,导致KeyguardOcclusionController以为还在遮挡状态,新用户自然看不到锁屏。
解决方案是在BootReceiver或UserController的onUserSwitchComplete回调里主动调用一次setOccluded(false),强制复位。
5. 常见问题速查与调试技巧
5.1 常见问题对照表
| 现象 | 可能原因 | 排查命令/思考方向 |
|---|---|---|
| 来电全屏界面卡白屏 | occlusion和dismiss动画冲突 | 检查WindowManager转场动画类型 |
| occluded=false但锁屏不恢复 | 用户已解锁或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的运作方式都会有更深的理解。