Android 滑动解锁概率性黑屏优化 —— SystemUI + Launcher 实践
适用场景:Android 12+ / AOSP SystemUI Keyguard 解锁动画 / Launcher3
关键词:滑动解锁、黑屏、KeyguardViewMediator、RemoteAnimation、Launcher、SurfaceFlinger
难度:中级
文章目录
- Android 滑动解锁概率性黑屏优化 —— SystemUI + Launcher 实践
- 一、需求背景
- 二、整体方案
- 2.1 模块职责
- 2.2 问题根因(日志对比)
- 2.3 方案思路
- 三、SystemUI 解锁动画闸门实现
- 3.1 文件与改动点
- 3.2 关键代码:defer 过早 exit
- 3.3 关键代码:修复 Launcher 等待
- 四、Launcher 首帧就绪标志实现
- 4.1 文件与改动点
- 4.2 关键代码
- 4.3 模块衔接
- 五、关键改动文件清单
- 六、验证建议
- 七、总结
- 八、参考:端到端时序
一、需求背景
从锁屏界面滑动解锁进入桌面时,概率性闪黑一下:锁屏层已经消失,但桌面/壁纸尚未完成首帧绘制,合成器中间出现“无内容可画”的空窗。
现象特点:
- 快滑、短滑时更容易复现;
- 同一轮操作可能第一次黑、第二次正常,具备明显竞态特征;
- 黑屏持续时间通常在几十到一两百毫秒量级。
| 项 | 说明 |
|---|---|
| 入口 | 锁屏界面上滑解锁到桌面 |
| 期望行为 | 锁屏 → 桌面过渡连续,无黑帧 |
| 适用范围 | 无密码滑动解锁;重点覆盖解锁到 Launcher 场景 |
二、整体方案
2.1 模块职责
| 模块 | 职责 |
|---|---|
SystemUIKeyguardUnlockAnimationController | 根据滑动 dismiss 进度决定何时显示背后 Surface、何时真正结束 Keyguard |
SystemUIKeyguardViewMediator | 发起keyguardGoingAway,在 RemoteAnimation 结束后 hide Keyguard |
| WindowManager / Shell | 创建 TO_BACK Transition,回调handleStartKeyguardExitAnimation |
| Launcher3 | 从STOPPED恢复并完成首帧绘制;通过 Settings 标记“桌面已就绪” |
2.2 问题根因(日志对比)
异常解锁(出现黑屏)典型时序:
keyguardGoingAway ↓(约 30ms) exitKeyguard(此时 RemoteAnimation 尚未 start) ↓ hide Keyguard + Wallpaper Surface hide ↓ Launcher onStart / Start draw(仍未就绪) ↓ 中间约 90~170ms:锁屏没了、壁纸没了、Launcher 未画完 → 闪黑 ↓ handleStartKeyguardExitAnimation(已晚)正常解锁时序:
keyguardGoingAway ↓ Launcher HAS_DRAWN ↓ handleStartKeyguardExitAnimation / fadeInSurfaceBehind ↓ 再 exitKeyguard / hide Keyguard关键结论:
- 滑动 dismiss 量很快超过
DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD(0.3)时,会在surface-behind RemoteAnimation 尚未 running时就调用exitKeyguard; - 既有的
launcher_oncreated等待逻辑存在缺陷:Settings 只读一次且不重读,Launcher 又只在onCreate置位,灭屏onStop后标志仍为 1,等待形同虚设; - Keyguard 提前隐藏后,Wallpaper 也会短暂
Surface hide,进一步放大黑帧。
2.3 方案思路
采用“主路径闸门 + 就绪等待”双保险:
滑动 dismiss ≥ 0.3 ↓ 【闸门】surface-behind RemoteAnimation 是否已 running? ├─ 否 → 暂不 exit,等 notifyStartSurfaceBehindRemoteAnimation 再判定 └─ 是 → 进入 exitKeyguard ↓ 【等待】解锁到 Launcher 且 launcher_oncreated==0? ├─ 是 → 轮询(最多 800ms)直到首帧置 1 └─ 否 → 立即 hide Keyguard设计取舍:
- 优先保证无黑帧,允许 Keyguard 多停留几十毫秒;
- 解锁到后台 App 时,不因 Launcher 标志为 0 而额外等待;
- 不修改 WM 合成策略,只在 SystemUI / Launcher 侧对齐时序。
三、SystemUI 解锁动画闸门实现
3.1 文件与改动点
| 文件 | 改动类型 | 关键点 | 作用 |
|---|---|---|---|
KeyguardUnlockAnimationController.kt | 修改 | finishKeyguardExitRemoteAnimationIfReachThreshold() | dismiss 过阈值但动画未 running 时 defer exit |
KeyguardViewMediator.java | 修改 | exitKeyguardAndFinishSurfaceBehindRemoteAnimation() | 修复并收紧 Launcher 就绪等待 |
上下游关系:
- 滑动过程中
onKeyguardDismissAmountChanged()会反复调用阈值判断; showSurfaceBehindKeyguard()后,WM 回调notifyStartSurfaceBehindRemoteAnimation();- 该方法末尾在非 canned 动画路径会再次调用阈值判断,从而接住此前被 defer 的 exit。
3.2 关键代码:defer 过早 exit
路径:frameworks/base/packages/SystemUI/.../KeyguardUnlockAnimationController.kt
valdismissAmount=keyguardStateController.dismissAmountif(dismissAmount>=1f||(keyguardStateController.isDismissingFromSwipe&&!keyguardStateController.isFlingingToDismissKeyguardDuringSwipeGesture&&dismissAmount>=DISMISS_AMOUNT_EXIT_KEYGUARD_THRESHOLD)){// Swipe can cross EXIT threshold before WM starts the surface-behind remote// animation. Hiding keyguard then leaves a gap until Launcher/wallpaper draws.// Defer exit; notifyStartSurfaceBehindRemoteAnimation will call this again.if(!keyguardViewMediator.get().isAnimatingBetweenKeyguardAndSurfaceBehind){Log.d(TAG,"finishKeyguardExitRemoteAnimationIfReachThreshold: "+"surface behind animation not running yet, defer exit",)return}setSurfaceBehindAppearAmount(1f)dismissAmountThresholdsReached=truekeyguardViewMediator.get().exitKeyguardAndFinishSurfaceBehindRemoteAnimation(false/* cancelled */)}逻辑说明:
- 触发:滑动 dismiss 达到退出阈值(默认 0.3)或完全 dismiss;
- 新增判断:仅当
isAnimatingBetweenKeyguardAndSurfaceBehind == true(即handleStartKeyguardExitAnimation已把mSurfaceBehindRemoteAnimationRunning置 true)才真正 exit; - 为何不置
dismissAmountThresholdsReached:defer 时保持 false,以便动画开始后或取消滑动时仍可继续走正常分支; - 衔接:
notifyStartSurfaceBehindRemoteAnimation()结束时会再次调用本方法,形成闭环。
3.3 关键代码:修复 Launcher 等待
路径:frameworks/base/packages/SystemUI/.../KeyguardViewMediator.java
原实现问题:
launcher_oncreated在 Runnable 外只读一次,轮询时值不会更新;- 只要初始值不是 0(常见是 1),会立刻 hide Keyguard。
优化后:
finallongstartTime=System.currentTimeMillis();finalRunnablecheckLauncherCodeRunnable=newRunnable(){@Overridepublicvoidrun(){// Re-read each pollintlauncherFinishKeycode=Settings.Global.getInt(mContext.getContentResolver(),LAUNCHER_ONCREATED,-1);Log.d(TAG,"launcher_finsih_keycode:"+launcherFinishKeycode);// Only wait when unlocking to Launcher and it has not drawn yet (0).booleanneedWaitLauncher=launcherFinishKeycode==0&&mKeyguardUnlockAnimationControllerLazy.get().isSupportedLauncherUnderneath();if(needWaitLauncher&&System.currentTimeMillis()-startTime<800){mHandler.postDelayed(this,50);}else{onKeyguardExitFinished("exitKeyguardAndFinishSurfaceBehindRemoteAnimation");if(mKeyguardStateController.isDismissingFromSwipe()||wasShowing){mKeyguardUnlockAnimationControllerLazy.get().hideKeyguardViewAfterRemoteAnimation();}finishSurfaceBehindRemoteAnimation(showKeyguard);mUpdateMonitor.dispatchKeyguardDismissAnimationFinished();}}};mHandler.post(checkLauncherCodeRunnable);逻辑说明:
- 每次轮询重读
Settings.Global,避免捕获到过期值; - 仅解锁到 Launcher且标志为 0 才等待,避免解锁回 App 被拖慢;
- 超时仍为800ms,Launcher 异常时不会永久卡在锁屏;
- 轮询间隔改为50ms,首帧就绪后更快放行。
四、Launcher 首帧就绪标志实现
4.1 文件与改动点
| 文件 | 改动类型 | 关键点 | 作用 |
|---|---|---|---|
Launcher.java | 修改 | onStop/onStart/onCreate/onDestroy | 用“可见首帧”表达桌面就绪 |
Launcher.java | 新增 | markLauncherDrawnReadyAfterNextDraw() | 下一帧onDraw后写 Settings=1 |
launcher_oncreated语义调整为:
| 值 | 含义 |
|---|---|
0 | 未就绪(onCreate开始 /onStop) |
1 | 已产生可见帧(首帧onDraw) |
-2 | ActivityonDestroy |
4.2 关键代码
路径:packages/apps/Launcher3/.../Launcher.java
生命周期侧:
@OverrideprotectedvoidonStop(){super.onStop();// Clear ready flag so unlock waits for the next visible frame after restart.Settings.Global.putInt(getContentResolver(),LAUNCHER_ONCREATED,0);// ...}@OverrideprotectedvoidonStart(){// ...super.onStart();markLauncherDrawnReadyAfterNextDraw();// ...}onCreate末尾不再直接写1,改为同样走首帧回调:
markLauncherDrawnReadyAfterNextDraw();首帧置位实现(精简):
privatevoidmarkLauncherDrawnReadyAfterNextDraw(){finalViewroot=getRootView();if(root==null){return;}clearLauncherReadyDrawListener();mLauncherReadyDrawListener=newOnDrawListener(){@OverridepublicvoidonDraw(){Settings.Global.putInt(getContentResolver(),LAUNCHER_ONCREATED,1);finalOnDrawListenerlistener=this;root.post(()->{try{ViewTreeObserverobserver=root.getViewTreeObserver();if(observer.isAlive()){observer.removeOnDrawListener(listener);}}catch(IllegalStateExceptione){// ViewTreeObserver may already be dead during teardown.}if(mLauncherReadyDrawListener==listener){mLauncherReadyDrawListener=null;}});}};ViewTreeObserverobserver=root.getViewTreeObserver();if(observer.isAlive()){observer.addOnDrawListener(mLauncherReadyDrawListener);}else{mLauncherReadyDrawListener=null;Settings.Global.putInt(getContentResolver(),LAUNCHER_ONCREATED,1);}}逻辑说明:
- 灭屏进入
onStop后标志清零,下次解锁必须等新的可见帧; onStart/onCreate注册一次性OnDrawListener,首帧后写 1 并移除监听;onDestroy清理监听,避免泄漏;- 与 SystemUI 等待逻辑通过
Settings.Global("launcher_oncreated")衔接。
4.3 模块衔接
Launcher.onStop → Settings=0 ↓ 亮屏滑动解锁 SystemUI showSurfaceBehindKeyguard / keyguardGoingAway ↓ WM Transition ready,RemoteAnimation running ↓ SystemUI 允许 exitKeyguard ↓ 若 Settings==0 且底下是 Launcher → 轮询等待 ↓ Launcher.onStart → 首帧 onDraw → Settings=1 ↓ SystemUI hide Keyguard(此时桌面已有内容)五、关键改动文件清单
| 层级 | 文件 | 改动类型 | 作用 |
|---|---|---|---|
| SystemUI | .../keyguard/KeyguardUnlockAnimationController.kt | 修改 | 动画未 running 时 defer exit |
| SystemUI | .../keyguard/KeyguardViewMediator.java | 修改 | 重读 Settings + 仅解锁到桌面时等待 |
| Launcher3 | .../launcher3/Launcher.java | 修改 | onStop 清零、首帧置位就绪标志 |
可选说明:
| 曾考虑的改动 | 最终未采用原因 |
|---|---|
| 直接关闭/拉长 unlock 动画 duration | 影响全局解锁手感,且不能解决“锁屏先藏”的根因 |
无条件等待launcher_oncreated==0 | 会拖慢解锁回 App 的路径 |
六、验证建议
- 快滑解锁到桌面:灭屏 → 亮屏 → 快速上滑,反复 20+ 次,预期无黑闪;日志可看到
surface behind animation not running yet, defer exit,以及launcher_finsih_keycode从 0 变为 1。 - 慢滑 / 正常滑:过渡自然,无额外卡顿感。
- 滑一半取消:上滑到半屏松手回弹,应回到锁屏,不卡死。
- 解锁回后台 App:打开 App → 灭屏 → 解锁,应无明显额外延迟。
- 指纹/人脸解锁到桌面:冒烟通过即可(本方案主要作用于滑动阈值路径)。
七、总结
本次优化的核心是:不要在 surface-behind RemoteAnimation 真正开始前隐藏 Keyguard,并用“Launcher 可见首帧”作为第二道就绪信号。通过 SystemUI 闸门 + Launcher Settings 标志联动,消除滑动解锁到桌面时的概率性黑帧。
八、参考:端到端时序
用户上滑 ↓ dismiss ≥ 0.15 → showSurfaceBehindKeyguard → keyguardGoingAway ↓ dismiss ≥ 0.3 ├─ animation 未 running → defer exit(关键) └─ animation 已 running → exitKeyguard ↓ 等待 Launcher 首帧(仅解锁到桌面) ↓ hide Keyguard / wallpaper 过渡 ↓ 桌面稳定显示如有问题欢迎留言交流。