news 2026/10/21 9:07:06

Android R SystemUI源码解析:Keyguard occluded状态流转全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android R SystemUI源码解析:Keyguard occluded状态流转全流程

在安卓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为例,完整时序可以归纳如下:

  1. 应用通过StatusBarManager.disable或WindowManager.LayoutParams的FLAG_SHOW_WHEN_LOCKED请求全屏显示;
  2. PhoneWindowManager根据窗口属性判断需要通知SystemUI,调用KeyguardViewMediator.setOccluded;
  3. KeyguardViewController更新锁屏View的可见性策略,同时调用KeyguardOcclusionController保存最新的occluded值;
  4. StatusBarStateController广播状态到通知栏、快捷设置等组件;
  5. 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 0

5.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的运作方式都会有更深的理解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 9:06:05

C++多态深度解析:虚表、CRTP与std::variant实战对比

如果让我挑一个最能体现C性格的特性,我会选多态。同一个调用,在不同对象上走出完全不同的执行路径;同一份接口声明,背后藏着无数具体实现。教科书喜欢把它包装成“面向对象三大特征”之一,但在真实项目里,它…

作者头像 李华
网站建设 2026/10/21 9:05:37

TCP/IP展厅智能中控:从串口到网络化控制的工程实践

简介:基于TCP/IP的展厅智能中控软件程序是一套面向展厅、会议及多媒体展项的集中控制方案,适用于集成商、运维人员或场馆管理者,用于统一管理灯光、投影、音响等设备。软件采用可视化、所见即所得的编辑方式,通过鼠标拖曳完成界面…

作者头像 李华
网站建设 2026/10/21 9:03:59

Leecode第68题:x的平方根

mySqrt 解题思路&#xff08;二分查找&#xff09;题目求非负整数 x 的整数平方根&#xff08;向下取整&#xff09;&#xff0c;即最大的整数 r 满足 r * r < x。不能使用内置 sqrt。例如&#xff1a;x 4 → 2x 8 → 2&#xff08;因为 2*24 < 8&#xff0c;3*39 >…

作者头像 李华
网站建设 2026/10/21 9:03:23

artcraft:从创意到成品的结构化工作流与个人资产库搭建指南

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是&#xff1a;这大概率是一个把“艺术&#xff08;art&#xff09;”和“手工艺&#xff08;craft&#xff09;”揉在一起的工具或项目。事…

作者头像 李华
网站建设 2026/10/21 9:03:01

diagram-design:用设计系统思维构建可复用图表模板

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次听到 diagram-design 这个词&#xff0c;很多人会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道&#xff0c;画图本身从来不是难点&#xff0c;难的是让图表在三个月…

作者头像 李华
网站建设 2026/10/21 9:02:58

STM32 HAL库驱动ADS124S08高精度ADC实战指南

简介&#xff1a;面向STM32嵌入式开发者&#xff0c;这份基于HAL库的ADS124s08驱动例程提供了一套完整的SPI通信与数据采集实现方案。ADS124s08是一款24位高精度、低噪声的Σ-Δ模数转换器&#xff0c;内置可编程增益放大器&#xff0c;能处理微伏级输入信号&#xff0c;配合ST…

作者头像 李华