
1. 先把灭屏和锁屏这两件事分开看安卓锁屏灭屏加载流程很多开发者第一次接触时都会把它当成一条链路来理解结果调试的时候越想越糊涂。真实情况是灭屏是电源与显示子系统的状态迁移锁屏是窗口管理与SystemUI侧视图状态的重置与准备两者是两条独立的状态机只是在电源键按下这一个动作上被强行耦合在一起。你在排查灭屏之后锁屏没准备好亮屏直接跳到桌面锁屏界面白一下才出来这类问题时如果脑子里只有一条链路就很难定位到底是哪一侧慢了。我见过不少同学在调这个问题时第一反应是去翻锁屏布局的inflate代码觉得灭屏时会重新加载一遍锁屏界面。这个认知偏差非常普遍但它恰恰是理解整条流程的最大障碍。实际实现里StatusBar 对应的窗口在SystemUI进程起来之后就长期存在了锁屏视图作为它的一部分在开机后第一次需要显示之前就已经被创建并常驻内存灭屏阶段做的主要是收尾、状态重置和下一次展示前的准备不是从零开始构建视图树。把这一点想明白后面所有关于时序的讨论才有意义。适合读这篇内容的人大致是三类第一类是做过SystemUI定制、想搞清楚灭屏亮屏时锁屏为什么会闪一下的开发者第二类是负责电源管理、需要确认应用侧申请的wakelock会不会打乱锁屏展示时机的工程师第三类是做稳定性测试、拿到了亮屏后短暂显示桌面这类问题单想从原理上判断是系统bug还是设计如此。三类人的关注点不一样但底层需要的知识是同一套。1.1 两个状态机为什么会互相纠缠电源侧的状态机核心是wakefulness取值从WAKEFULNESS_AWAKE到WAKEFULNESS_ASLEEP中间还有WAKEFULNESS_DOZING和WAKEFULNESS_DREAMING两个过渡态。锁屏侧的状态机核心是mShowing、mOccluded、mShowingLockscreen这几个布尔量外加一个屏幕是否正在关闭的中间态。两套状态机的驱动源不同前者由PowerManagerService的时间、电源键、传感器事件驱动后者由WindowManager的策略回调和SystemUI自己的状态同步驱动。纠缠点在于窗口管理层的策略层。当电源侧决定进入睡眠时它必须通知窗口管理层我要睡了因为窗口管理层需要在这个时间点决定当前正在交互的窗口要不要先收掉软键盘、要不要退出分屏、要不要把焦点还给锁屏。反过来锁屏侧要显示的时候也需要向窗口管理层申请我要挡住下面的应用。这两次交互都发生在很窄的时间窗口里一旦某一侧的处理耗时过长就会出现肉眼可见的异常。还有一个容易被忽略的耦合点SurfaceFlinger的合成节奏。灭屏不是瞬间切断面板供电而是先让亮度渐变到零再通知显示硬件进入低功耗状态。这个渐变过程通常有几十毫秒到几百毫秒这段时间内画面还在合成如果SystemUI此时正好在大量创建或销毁视图就会拖慢合成线程表现为灭屏动画掉帧或者黑屏之前画面卡一下。注意判断问题归属时先确认是显示侧先关还是锁屏侧先动。这两件事的先后顺序在不同版本、不同定制系统上是有差异的不能靠记忆下结论必须看日志。1.2 一次电源键按下到底有哪几条链路在同时跑把一次电源键短按拆开至少有四条链路在并行推进链路主要参与者典型耗时量级关键动作输入分发输入子系统、窗口管理策略层微秒到毫秒拦截电源键决定是否交给应用电源状态迁移PowerManagerService、显示电源控制器毫秒级更新wakefulness发起显示关断请求窗口策略通知窗口管理策略层、SystemUI委托毫秒级通知睡眠开始、通知屏幕即将关闭显示硬件关断显示电源控制器、合成器、面板驱动几十到数百毫秒亮度渐变、面板进入低功耗这四条链路里第一条是触发者第四条是终点中间两条是真正需要关注的地方。绝大多数锁屏没准备好的问题根因都在第三条链路——策略通知发出去了但SystemUI侧的处理是异步的处理完之前屏幕已经黑了而下一次亮屏时又走了一遍相反的异步流程两次异步叠加就出现了时序空洞。很多人会问为什么不能等SystemUI处理完再关屏答案在功耗上。屏幕是整机最大的耗电单元之一从用户按下电源键到面板真正断电每一毫秒都在耗电。系统设计上倾向于尽快关断而不是等待用户态把收尾工作做完。这个取舍决定了收尾工作必须做得足够快也决定了它是天然的竞态高发区。理解了这四条链路的并行关系接下来就能逐条拆开看了。顺序上我建议先看电源与显示这条主干因为它是时间的基准再回头看锁屏侧的准备因为它的所有动作都是围绕这条基准来安排节奏的。2. 从PowerManagerService到SurfaceFlinger显示关断的完整链路这条链路是整个流程的时间基准理解了它你才知道锁屏侧到底有多少时间可以挥霍。链路大致是PowerManagerService 接收睡眠请求更新内部状态算出显示电源请求交给显示电源控制器控制器决定何时、以何种方式关断最后通过合成器下发给显示硬件。中间任何一环都有可能出现延迟但延迟的形态不一样有的表现为关得晚有的表现为关得不干净。这里要强调一个概念显示关断是有档位的不是简单的开和关。常见的档位包括完全关闭、低功耗保持用于息屏显示类场景、挂起态、以及正常点亮。不同的档位对应不同的硬件行为和不同的合成器状态SystemUI在这几种档位下的准备动作也不一样。如果你只按开/关二值来理解遇到息屏显示相关的问题就会完全找不到北。2.1 wakefulness状态迁移与updatePowerStateLocked的收敛过程PowerManagerService 内部把所有影响电源决策的因素收集起来通过一个叫updatePowerStateLocked的方法统一收敛。这个方法做的事情可以概括为先算用户活动状态再算wakelock再算显示电源请求最后把结果下发。它是幂等的、可重入的一次睡眠请求可能触发多次重算因为每一步都可能反过来影响前面的输入。wakefulness的迁移路径值得单独记一下。正常的灭屏走的是AWAKE → ASLEEP但如果设备配置了息屏显示中间会插入DOZING也就是屏幕进入低功耗保持而不是断电。DREAMING则是另一条路径通常和充电时的屏保、底座模式相关。这三条路径对应SystemUI侧的三种不同处理逻辑混在一起看会非常乱排查时必须先确认走的是哪条。// 简化示意仅表达收敛思路非可直接编译代码 private void updatePowerStateLocked() { // 第一轮算用户活动与唤醒锁 updateIsPoweredLocked(mDirty); updateStayOnLocked(mDirty); // 第二轮算wakefulness决定是睡还是醒 updateWakefulnessLocked(mDirty); // 第三轮根据wakefulness和显示请求算最终显示状态 updateDisplayPowerStateLocked(mDirty); // 第四轮下发给显示电源控制器 if (mDisplayReady) { // 请求下发注意这里是异步的 mDisplayManagerInternal.requestPowerState(displayId, mDisplayPowerRequest, false); } }这段结构里最关键的是异步两个字。requestPowerState返回不代表屏幕已经关了它只是把请求放进控制器的队列。控制器可能还需要做亮度渐变、等待前一帧合成完成、等待硬件确认。所以你在日志里看到请求关屏的时间戳和屏幕已关的时间戳中间可能差上百毫秒。另一个容易踩的点是mDisplayReady这个标志。它表示显示子系统已经准备好接受新请求如果上一轮的请求还没完成新的请求会被排队甚至被合并。这就是为什么连续快速按电源键时日志里的状态迁移看起来会跳步——不是丢了状态而是中间态被合并掉了。2.2 DisplayPowerController的请求仲裁与状态机显示电源控制器是一个典型的请求仲裁者。它接收来自上层的电源请求同时还要考虑接近传感器、环境光传感器、亮度策略、超时策略等一堆输入最后给出一个确定的屏幕状态。它的内部状态机有明确的迁移规则不是任意两个状态之间都能直接跳。举个常见的例子从点亮直接切到完全关闭和从点亮先切到低功耗再切到关闭走的是不同分支。前者会执行一次完整的亮度渐暗动画后者可能在低功耗状态下就已经把亮度降到零切到关闭时只需要断电。如果你在做灭屏动画的体验优化这两种路径需要分别处理。控制器还负责一个很实际的功能屏幕关闭前的渐暗。这个渐暗不是为了好看而是为了给用户一个视觉上的过渡避免画面瞬间消失带来的突兀感。渐暗的时长和曲线是可配置的不同定制系统差异很大。有些系统为了省电把渐暗时间压得很短结果就是用户感觉啪一下就黑了体验不好但功耗友好有些系统做得比较柔和但会拉长整个关屏链路。提示调试灭屏时序时如果发现从按键到黑屏的时间异常长先确认是不是渐暗动画被拉长了而不是去怀疑锁屏侧的问题。这两个现象在日志上表现完全不同但用户描述起来都是灭屏慢。控制器的状态变更最终会通过回调通知上层这个回调是SystemUI和窗口管理层感知屏幕真的关了的主要途径。回调里会带上当前状态和一系列标志位标志位里有些信息在排查问题时非常有用比如是否因为接近传感器而关闭、是否处于低功耗保持态等。2.3 SurfaceFlinger收到setPowerMode之后做了什么合成器这一层相对薄但它的行为直接决定画面什么时候真正消失。收到电源模式变更后它会更新对应显示设备的状态暂停该显示上的合成工作并通知显示硬件驱动。注意这里的关键词是暂停合成不是销毁图层。所有图层都还在只是不再参与合成这样下次点亮时可以快速恢复不需要重建整套图层树。这个设计对锁屏加载流程有直接影响因为图层不销毁锁屏窗口的内容即使在你看来没显示它其实一直存在于合成器的图层列表里。这意味着锁屏视图的状态是持续的灭屏不会让它回到初始状态。很多开发者以为灭屏会重置锁屏实际上重置是SystemUI自己主动做的不是合成器或窗口管理器的行为。合成器还有一个和排查密切相关的行为它会记录每一帧的合成耗时和图层数量。当你怀疑灭屏时是不是有大量视图在重建导致卡顿看合成器的帧记录比看应用层日志更直接。图层数量在灭屏前后如果有剧烈波动基本可以确认是视图层面发生了大规模变更。# 查看显示与电源相关信息 adb shell dumpsys display adb shell dumpsys power | grep -i -E wakefulness|Display Power|mScreenOn adb shell dumpsys SurfaceFlinger --dispsync上面第三条命令在不同版本上输出差异较大实际使用时可以先dumpsys SurfaceFlinger看整体结构再决定过滤什么。命令的具体输出格式会随版本变化但显示状态和图层列表这两块信息一直都在。到这里时间基准就清楚了从按键到面板断电中间有请求收敛、状态仲裁、渐暗动画、硬件确认四道关口。锁屏侧的所有准备工作都必须塞进这个窗口里完成否则就会出现下一节要讲的问题。3. SystemUI侧锁屏视图的装配与预热现在把视角切到SystemUI。这一侧的核心问题不是怎么画锁屏而是什么时候准备好。锁屏视图的装配分为两个层次一次性的构建创建视图树、绑定监听器、初始化各个子控制器和每次灭屏亮屏时的状态重置收起安全输入区、清除临时状态、同步用户信息。前者在进程生命周期里只发生少数几次后者每次灭屏都可能发生。我见过的最常见的误解是把锁屏加载等同于每次灭屏都重新inflate一遍布局。实际上如果真这么做性能会非常难看因为锁屏布局包含的层级相当深涉及通知、媒体控制、快捷入口、安全输入区等多个子系统inflate一次的代价不小。真实的做法是视图常驻、状态重置、按需显示。3.1 KeyguardViewMediator锁屏的总调度台锁屏侧的总调度角色是KeyguardViewMediator它接收来自各个方向的事件——电源状态变化、用户切换、SIM卡状态变化、生物识别结果、窗口管理层的遮挡通知——然后决定锁屏当前应该处于什么状态并把决定下发给真正干活的视图管理层。它维护的状态里最需要关注的是三个是否正在显示、是否被遮挡、是否正在退出。这三个状态组合起来能表达出锁屏在后面但被应用盖住了锁屏正在滑出锁屏完全隐藏等多种情形。排查问题时第一件事就是把这几个状态打出来看它们和你预期的是否一致。// 状态输出示意实际字段名随版本略有差异 // adb shell dumpsys activity service com.android.systemui/.keyguard.KeyguardService // 关注输出中的 // showingtrue/false // occludedtrue/false // showingLockscreentrue/false // screenState...showingtrue而occludedtrue是一个特别值得注意的组合它表示锁屏认为自己应该显示但被上层窗口挡住了。这种状态如果长期不消除用户就会遇到锁屏逻辑上在但看到的是应用的情况。造成它的原因通常是有应用申请了覆盖锁屏的窗口标志而灭屏亮屏的过程中这个标志没有被正确清理。调度台还负责一件容易被忽略的事在屏幕即将关闭时通知视图层收尾。收尾包括收起已经展开的通知面板、退出安全输入区、隐藏生物识别提示、把滚动位置复位。这些动作必须在屏幕完全黑掉之前完成否则下一次亮屏时用户会看到一个半开的锁屏视觉上非常突兀。3.2 锁屏视图的inflate时机与View层级归属锁屏视图挂在哪里这个问题的答案直接决定了它的生命周期。在主流实现里锁屏并不是一个独立的窗口而是状态栏窗口的一部分和通知面板共享同一个窗口容器。这么设计的原因很实际锁屏和通知面板在视觉上需要无缝衔接下拉通知时锁屏要跟着动分成两个窗口会产生同步问题。因为是同一个窗口所以窗口创建时锁屏视图就一起被inflate了。窗口创建的时间点通常在SystemUI进程启动后不久也就是开机阶段。这就解释了为什么第一次亮屏时锁屏出现得很快——它在开机时就已经建好了。同时也解释了另一个现象如果SystemUI进程被杀重启那么重启后的第一次灭屏亮屏锁屏的出现会明显慢一些因为视图树需要重新构建。视图层级大致是这样的结构窗口根容器锁屏容器包含壁纸层、遮罩层、内容层安全输入区容器按需展开通知与媒体区域快捷入口与状态信息通知面板容器状态栏容器内容层里的各个子区域大多是懒加载的比如安全输入区只有在需要解锁时才会真正构建内部视图。这种懒加载设计让首次构建的成本降下来了但把成本转移到了第一次解锁的时刻所以你会看到某些设备上第一次输密码时界面有轻微延迟。提示如果你在优化亮屏到锁屏可交互的时间别只盯着inflate安全输入区的懒加载往往是大头。可以提前触发一次构建来预热代价是常驻内存略微增加。3.3 安全区、Bouncer与多用户切换时的视图复用安全输入区业内常叫Bouncer是锁屏里最复杂的部分它要处理密码、PIN、图案、生物识别等多种解锁方式还要处理错误重试、锁定倒计时、多用户切换等状态。它的显示与隐藏不是简单的可见性切换而是有一套完整的状态机。灭屏时如果Bouncer正处于展开状态通常会做两件事一是记录下来当前是哪种解锁方式二是把它收起来。收起而不是销毁是为了下次亮屏时能快速恢复。但如果用户在灭屏前输错了几次密码锁定倒计时还在走这个倒计时状态必须保留不能因为灭屏就清零——这是安全要求不是体验取舍。多用户场景会放大复杂度。切换用户时锁屏需要重新绑定到新用户的配置、重新加载该用户的通知、重新判断该用户是否设置了锁屏。这个过程和灭屏流程可能同时发生产生叠加。我遇到过的一个典型案例是切用户的同时按电源键灭屏再亮屏时锁屏显示的是上一个用户的信息。根因就是两个流程的异步回调交错后到的回调覆盖了先到的状态。// 伪代码灭屏时的收尾逻辑要点 void onScreenTurningOff() { // 1. 收起通知面板避免下次亮屏呈现半开状态 collapseNotificationPanel(); // 2. 收起安全输入区但保留解锁方式与错误计数 if (isBouncerShowing()) { rememberSecurityMode(); hideBouncer(/* destroy */ false); } // 3. 停止生物识别监听释放传感器 stopBiometricListening(); // 4. 复位滚动位置与临时UI状态 resetTransientState(); }这四步的顺序是有讲究的。先收面板再收Bouncer是因为面板展开时Bouncer可能被压在下面顺序反了会出现动画冲突。停止生物识别要在收Bouncer之前还是之后不同实现选择不同取决于传感器释放的时机要求。4. 灭屏到亮屏之间锁屏必须准备好的时间窗口前面两条链路讲完现在可以把它们对起来看时间了。灭屏时系统给锁屏侧的窗口很窄亮屏时给锁屏侧的窗口同样窄而且这两次窗口的性质不一样灭屏窗口是收尾型做不完最多是下次亮屏状态不干净亮屏窗口是呈现型做不完用户立刻能看到问题。这个不对称性决定了两侧的优化重点不同。灭屏侧要保证收尾动作快且不漏亮屏侧要保证呈现路径短且可控。很多团队把精力全放在亮屏侧结果就是各种状态残留问题反复出现。4.1 onScreenTurningOff与onStartedGoingToSleep的调用次序这两个回调的语义经常被混淆。前者表示屏幕即将关闭是一个预警此时画面还在还有时间做视觉上的收尾后者表示设备开始进入睡眠语义更宽可能发生在屏幕关闭之前也可能与之并行。在典型实现里顺序是先收到睡眠开始的通知再收到屏幕即将关闭的通知最后收到屏幕已关闭的确认。为什么要有即将关闭这个预警而不是直接给已关闭因为有些收尾动作必须在画面还在的时候做比如把展开的面板收起来如果等到屏幕已关再做用户下次亮屏就会看到一个正在收起的动画很怪。这个预警窗口的长度取决于渐暗动画的时长通常是百毫秒量级。排查时要特别留意这两个回调的间隔。如果间隔异常短说明渐暗被压缩了收尾动作可能来不及如果间隔异常长说明某个环节卡住了需要看是谁在阻塞主线程。4.2 快速亮屏场景下的锁屏就绪判断快速亮屏是指灭屏后很短时间内再次亮屏典型场景是用户误按电源键后立刻再按一次。这个场景是竞态的重灾区因为灭屏的收尾还没做完亮屏的呈现就开始了。系统对此通常有一个就绪判断亮屏时先检查锁屏是否处于可呈现状态如果不是要么等待要么走降级路径。降级路径的表现形式就是先显示一个空白或壁纸锁屏内容稍后补上。用户感知到的就是闪一下。要减少这种闪思路有两个方向一是缩短收尾耗时让锁屏更快回到可呈现状态二是在亮屏前预判提前把状态准备好。前者是常规优化后者需要在灭屏流程里做提前量代价是可能做了无用功。场景灭屏收尾是否完成亮屏表现建议处理方向正常间隔亮屏已完成锁屏立即出现无需处理极短间隔亮屏未完成短暂空白后补内容缩短收尾或增加就绪等待收尾被阻塞卡在中途锁屏半开或状态错乱定位阻塞点检查主线程进程重启后首次视图未构建明显延迟提前预热视图树这张表可以直接拿去对照现场现象。第三行是最麻烦的因为现象看起来像锁屏坏了实际是主线程被别的东西占住了。4.3 Doze与息屏显示场景下的额外状态配了息屏显示的设备灭屏流程会多出一个中间态。屏幕不是直接断电而是进入低功耗保持此时面板仍在刷新但刷新率极低、内容极简。这个状态下锁屏的处境很微妙它既不能完全显示会耗电且不符合息屏显示的视觉规范也不能完全隐藏用户抬手时需要快速过渡到完整锁屏。典型做法是在进入低功耗态时把锁屏内容层整体淡出只保留息屏显示自己的极简内容抬手或收到通知时再快速把锁屏内容层淡入。这个淡入淡出和电源状态机是联动的如果联动关系没处理好就会出现抬手后锁屏和息屏内容重叠或者息屏内容不消失的问题。注意息屏显示相关的时序问题和普通灭屏问题表象相似但根因完全不同。排查前先确认设备是否开启了息屏显示否则会在错误的方向上浪费大量时间。5. 拿到现场用dumpsys、trace和日志把流程钉死原理讲完接下来是实操。这类问题的特点是看一眼日志就能定位一半但前提是你知道看哪几个字段。我一般按三步走先看状态快照确认当前处于哪个状态再抓一次完整过程看状态是怎么迁移的最后看关键路径的耗时找出是谁慢。顺序不能反。上来就抓trace数据量大且没有参照很容易迷失。先有状态快照作为锚点后面的数据才有解读的基准。5.1 三条命令看清当前状态第一条看电源侧第二条看窗口侧第三条看锁屏侧。三条命令的输出对照着看能立刻发现状态不一致的地方。# 电源与显示状态 adb shell dumpsys power # 窗口与锁屏控制状态 adb shell dumpsys window | grep -i -E keyguard|mAwake|mScreenOn|Display # 锁屏服务详细状态 adb shell dumpsys activity service com.android.systemui/.keyguard.KeyguardService第一条里重点看mWakefulness和显示电源请求的字段确认设备当前是醒着还是睡着。第二条里重点看窗口管理层记录的锁屏显示标志这个标志和锁屏服务自己认为的状态如果不一致基本就是同步出了问题。第三条里看前面提到的 showing/occluded 组合。我习惯把三条命令的输出拼在一起存成一个文件命名带上时间戳这样对比前后两次的状态非常方便。遇到偶现问题时让测试同学在出问题的瞬间执行这套命令比事后复现高效得多。5.2 用perfetto抓一次灭屏-亮屏全过程抓trace的目的是看时序不是看调用栈。所以配置时要打开的关键数据源是电源状态、显示状态、窗口状态、主线程的调度情况。合成的帧数据也要用来判断是不是合成拖慢了。# 抓取一个覆盖灭屏亮屏的trace adb shell perfetto -o /data/misc/perfetto-traces/kgoff.perfetto-trace -t 10s \ sched freq idle am wm power pm ss # 拉回本地 adb pull /data/misc/perfetto-traces/kgoff.perfetto-trace .配置里的几个数据源缩写含义大致是调度、频率、空闲状态、活动管理、窗口管理、电源管理、系统状态。抓的时候让测试同学在第2秒灭屏、第5秒亮屏这样整个过程都落在窗口内。分析时按这个顺序看先看电源状态那条泳道找到睡眠开始和唤醒的两个时间点再看窗口管理泳道看锁屏相关事件落在这两个点的哪个位置最后看主线程看灭屏和亮屏附近有没有长任务。这个顺序能保证你始终在有时间基准的前提下看数据。常见的发现有三类一是锁屏事件明显晚于电源事件说明收尾被拖后了二是主线程在灭屏附近有一个长任务说明有别的东西在抢时间三是合成泳道在亮屏后有明显空洞说明画面供应跟不上。三类发现对应三种不同的优化方向。5.3 日志过滤的关键字与误判日志过滤的关键字建议按模块分组不要用一个宽泛的词去捞。宽泛的词会捞出大量无关内容反而掩盖关键信息。关注点建议关键字说明电源状态迁移wakefulness, goToSleep, wakeUp看状态变化的时间点显示关断DisplayPower, setPowerMode, screenState看请求与确认的间隔锁屏调度KeyguardViewMediator, onScreenTurningOff看收尾动作是否执行窗口遮挡Keyguard, occluded, going away看遮挡状态是否正确清理视图构建inflate, KeyguardSecurityContainer看是否有意外的重建误判最常发生在inflate这个关键字上。日志里出现inflate不代表视图真的重建了有些实现会在调试模式下打印加载路径但走缓存分支。判断是否真的重建要看构造相关的日志或者看trace里有没有对应的方法执行光看关键字会得出错误结论。另一个常见误判是把亮度变化当成屏幕状态变化。有些日志会把每一次亮度调整都打出来灭屏时的渐暗会刷出好几条看起来像是状态反复切换实际只是一次关断过程中的亮度步进。6. 几个高频翻车点与原理解释理论铺垫够了这一节讲几个我在实际项目里反复遇到的问题。每一个都会给出表象、根因和排查路径你可以直接拿去对照自己手上的现象单。6.1 灭屏瞬间的桌面残影表象是按电源键灭屏黑屏前的一瞬间能看到桌面图标或应用界面闪现。用户描述通常是灭屏时闪一下。根因通常不是锁屏没准备好而是关断顺序错了。正确顺序是先让锁屏盖上来或者保持盖着再执行亮度渐暗。如果先渐暗再显示锁屏那么在渐暗的最后一刻上层覆盖窗口的状态可能还没生效合成出来的最后一帧就是桌面。排查路径抓trace看锁屏显示事件和亮度渐暗开始事件的相对位置。如果渐暗在前基本可以确认。修复方向是调整调用顺序或者让锁屏容器在灭屏开始时就提前置为可见透明度从0开始渐入也可以。这个问题的迷惑性在于它和锁屏加载慢表象相似但根因完全不同。区分方法很简单看残影内容是桌面还是空白。残影是桌面说明是顺序问题残影是空白说明是锁屏内容没赶上。6.2 解锁后再次灭屏锁屏被跳过表象是解锁进入桌面后灭屏再亮屏锁屏没有出现直接就是桌面。有时候过几秒锁屏才姗姗来迟。根因是遮挡状态没有正确清理。解锁时光滑地退出锁屏系统会给锁屏打上被遮挡的标记表示有应用盖在上面。正常情况下这个标记会在应用退到后台时清掉。如果灭屏发生在标记清理之前那么再次亮屏时锁屏认为自己应该显示但被遮挡于是选择不显示。排查路径用前面的三条命令在问题发生时看 showing 和 occluded 的组合。如果 showing 为真而 occluded 也为真就坐实了。再看是谁设置了遮挡标记通常在窗口管理层的日志里能看到设置者的包名和窗口标志。提示这类问题上先怀疑有应用申请了覆盖锁屏的窗口标志而不是先怀疑锁屏本身。实际案例里第三方应用的悬浮窗、画中画、无障碍服务覆盖层都是常见元凶。6.3 自定义锁屏替换系统锁屏时的生命周期错位有些项目需要做深度定制的锁屏界面通常会选择在锁屏容器里替换内容层或者干脆用一个独立窗口实现。两种做法各有各的坑。替换内容层的做法坑在于生命周期绑得太紧。系统锁屏的收尾逻辑收面板、停生物识别、复位状态是围绕它自己的内容层写的你替换了内容层但没接管这些收尾动作就会出现自定义锁屏还开着系统已经在收尾的错位。表现为自定义界面的动画被打断或者下次亮屏时自定义界面处于半完成状态。独立窗口的做法坑在于遮挡状态和窗口层级的协调。锁屏窗口的层级是有明确定义的你的独立窗口如果层级不对要么盖不住状态栏要么把系统锁屏也盖住了导致双重锁屏。我的建议是尽量走内容层替换并且把收尾动作显式接过来在屏幕即将关闭的回调里主动做完自己的收尾再交还给系统。这样时序上是串行的不会错位。// 思路示意接管收尾 public void onScreenTurningOff() { // 先做自己的收尾 if (customLockView ! null) { customLockView.prepareForScreenOff(); // 自定义层的复位 } // 再走系统原有逻辑 super.onScreenTurningOff(); }顺序不能反。先交还给系统系统的收尾动作可能会把容器状态改掉你的自定义层再操作就会读到不一致的状态。最后分享一个我在实际调试里总结的小习惯每次遇到这类问题先不动代码花五分钟把灭屏前状态、灭屏中动作、亮屏后状态这三段分别用命令和日志记录下来写成三行对照。很多时候写完这三行根因就自己浮出来了。真正难的不是修而是搞清楚到底哪一段和哪一段对不上。