流轉(zhuǎn)全流程)
在安卓RAndroid 11上啃SystemUI源碼有一個繞不開的狀態(tài)機Keyguard的occluded被遮擋變化流程。平時大家關(guān)注比較多的是Keyguard怎么顯示、怎么dismiss很少有人專門把occluded這條鏈路單獨拉出來講。但這個狀態(tài)恰恰是很多疑難雜癥的源頭比如來電全屏界面蓋住鎖屏、某個應(yīng)用希望隱藏鎖屏但又不希望解鎖、以及鎖屏截圖黑屏這些奇奇怪怪的問題追根溯源都指向occluded的變化邏輯。我花了不少時間把這套流程從API入口到SystemUI內(nèi)部通知、再到WindowManager側(cè)的旗標變更完整捋了一遍發(fā)現(xiàn)它比想象中更有意思也更容易踩坑。這里把整個流轉(zhuǎn)過程整理出來配合一些實測經(jīng)驗和排查思路給同樣在Android R上做系統(tǒng)定制或SystemUI二次開發(fā)的朋友做個參考。1. occluded到底是什么意思1.1 先從使用場景理解這個概念occluded在英文里的意思是“被遮擋的”在鎖屏的場景下很好理解當某個全屏界面最常見的例子是來電界面覆蓋在鎖屏之上時當前鎖屏雖然還在底層但實際上用戶已經(jīng)看不到它了這時候Keyguard就處于occluded狀態(tài)。有一點必須強調(diào)occluded不等于dismissed。dismiss意味著鎖屏被解除用戶進入桌面而occluded只是被別的窗口蓋住了鎖屏本身還“活著”只是看不見。這個區(qū)別在后文分析狀態(tài)回切時會反復(fù)出現(xiàn)。實際開發(fā)中occluded最常見的入口有兩個第三方應(yīng)用申請隱藏鎖屏比如視頻播放器在全屏播放時申請隱藏鎖屏讓用戶能直接操作返回鍵而非先解鎖。Telephony層來電全屏界面通話過程中不需要用戶先解鎖來電界面本身就是全屏且優(yōu)先級更高此時鎖屏要進入occluded狀態(tài)。1.2 為什么需要專門維護一個occluded狀態(tài)鎖屏不僅僅是畫了個界面它背后牽扯到安全策略、指紋/人臉解鎖流程、通知欄展開策略、window flag輸出給WindowManager的聯(lián)動邏輯等。如果只是簡單地把鎖屏View蓋住而不通知其他模塊狀態(tài)變化就會出現(xiàn)各種沖突鎖屏還認為自己在前臺去搶焦點或攔截觸摸事件通知欄邏輯不知道鎖屏已被遮擋下拉時出現(xiàn)重疊WindowManager側(cè)不知道該不該給窗口加鎖屏相關(guān)的flag。所以occluded需要作為Keyguard狀態(tài)機的一部分被系統(tǒng)性地管理。簡單說這個狀態(tài)承載的是“鎖屏還在但被更高層級的界面完全蓋住”的核心語義。2. SystemUI側(cè)的核心鏈路拆解2.1 誰是這套流程的主心骨在Android R里Keyguard的狀態(tài)管理主要圍繞幾個核心類轉(zhuǎn)KeyguardViewController負責(zé)KeyguardView的創(chuàng)建、顯示、與WindowManager的綁定KeyguardViewMediator全局狀態(tài)流轉(zhuǎn)的總控制任何狀態(tài)變化都會經(jīng)過它做仲裁KeyguardOcclusionController專門維護occluded狀態(tài)的控制器監(jiān)聽來源變化并分發(fā)結(jié)果NotificationShadeWindowController/StatusBarWindowController負責(zé)和WindowManager層溝通窗口狀態(tài)、flag的寫入方。occluded變化流程大體可以描述為外部觸發(fā)者比如Telephony或者第三方應(yīng)用發(fā)起請求 → 通知到KeyguardViewMediator → KeyguardViewController和KeyguardOcclusionController聯(lián)動更新 → 通過NotificationShadeWindowController把最終狀態(tài)同步到WindowManager。2.2 從API入口看一次調(diào)用最終去了哪里在Android R源碼中Keyguard控制的主要對外接口是StatusBarManagerService或PhoneWindowManager間接調(diào)用進去的。以setOccluded為例最終會走到KeyguardViewMediator.setOccluded(boolean isOccluded, boolean animate)。這里要注意setOccluded有兩個重載版本區(qū)別在于是否帶動畫。系統(tǒng)內(nèi)的來電界面通常希望鎖屏消失或出現(xiàn)時帶動畫鋪滿而純狀態(tài)切換時則不帶動畫。這個動畫開關(guān)會影響到后續(xù)NotificationShadeWindowController內(nèi)部對StatusBarState和窗口轉(zhuǎn)場動畫的處理。流程內(nèi)部會調(diào)用// KeyguardViewMediator內(nèi)部 mKeyguardViewController.setOccluded(occluded, animate);KeyguardViewController攜帶occluded信息更新KeyguardOcclusionController里的狀態(tài)值同時通知NotificationShadeWindowController做同步例如setKeyguardOccluded的調(diào)用。2.3 StateController在其中的角色Android R的SystemUI里有一個統(tǒng)一的狀態(tài)機叫StatusBarStateController部分版本是CentralSurfaces負責(zé)它維護了KEYGUARD、SHADE、SHADE_LOCKED幾個狀態(tài)。occluded變化會引發(fā)狀態(tài)機的切換舉個常見的組合當前狀態(tài)是KEYGUARD收到occludedtrue時狀態(tài)機暫時保持KEYGUARD但所有與用戶交互相關(guān)的策略進入“掛起”狀態(tài)如果occluded變化的同時發(fā)生了解鎖KEYGUARD會切到SHADE并立即dismiss鎖屏occluded隨之被清理為false。代碼層面occluded對狀態(tài)機的影響往往體現(xiàn)在mStatusBarStateController.setKeyguardOccluded(occluded);這一步會回調(diào)給所有注冊了StateListener的組件包括通知面板、快捷開關(guān)面板、導(dǎo)航欄。這些組件收到回調(diào)后自行調(diào)整自己的可交互狀態(tài)。比如通知面板在鎖屏遮擋時不應(yīng)該響應(yīng)用戶下拉手勢此時它收到回調(diào)后會把NotificationsPanelView的觸摸響應(yīng)給裁剪掉。2.4 一次典型的變化時序以第三方應(yīng)用申請occlude為例完整時序可以歸納如下應(yīng)用通過StatusBarManager.disable或WindowManager.LayoutParams的FLAG_SHOW_WHEN_LOCKED請求全屏顯示PhoneWindowManager根據(jù)窗口屬性判斷需要通知SystemUI調(diào)用KeyguardViewMediator.setOccludedKeyguardViewController更新鎖屏View的可見性策略同時調(diào)用KeyguardOcclusionController保存最新的occluded值StatusBarStateController廣播狀態(tài)到通知欄、快捷設(shè)置等組件NotificationShadeWindowController將occluded語義翻譯成WindowManager能識別的窗口flag通過mWindowManager.updateViewLayout()提交。最終在WMS層面鎖屏窗口會帶上類似SYSTEM_FLAG_ALWAYS_ON_TOP之類的屬性但真正讓它被蓋住的是上層全屏窗口的存在occluded只是系統(tǒng)性確認這個事實。3. 那些容易忽略的內(nèi)部細節(jié)3.1 KeyguardOcclusionController的存續(xù)期小陷阱排查問題期間發(fā)現(xiàn)一個比較邪門的地方KeyguardOcclusionController的實例在很多定制ROM里會被重復(fù)創(chuàng)建。一旦重復(fù)創(chuàng)建注冊的listener可能出現(xiàn)錯亂導(dǎo)致occluded回調(diào)被重復(fù)派發(fā)甚至出現(xiàn)“明明已經(jīng)取消遮擋鎖屏還處于隱藏態(tài)”的詭異現(xiàn)象。原因是Android R在KeyguardViewController初始化和配置變更比如屏幕旋轉(zhuǎn)時都會觸發(fā)現(xiàn)有實例的銷毀重建。如果依賴注入關(guān)系沒有梳理干凈listener沒有在onDestroy時及時解綁后續(xù)就會連環(huán)踩雷。實測建議在定制代碼里新增對occluded狀態(tài)監(jiān)聽的模塊時務(wù)必在onStart里注冊、onStop里反注冊并且使用LifecycleBoundObserver或等效機制避免重復(fù)注冊回調(diào)。3.2 WindowManager側(cè)的表現(xiàn)方式occluded狀態(tài)從SystemUI傳遞到WindowManager之后WindowManager并不會直接感知“鎖屏被遮擋”這個概念而是通過窗口順序和FLAG_SHOW_WHEN_LOCKED等機制配合實現(xiàn)。在R上鎖屏窗口本身是通過StatusBarWindowView承載的它對應(yīng)一個ViewRootImplWMS側(cè)的類型通常是TYPE_STATUS_BAR或TYPE_NAVIGATION_BAR這些系統(tǒng)級窗口。來電這類全屏窗口類型級別更高從而在層級上天然壓住鎖屏。occluded在這里的意義更多是決策依據(jù)WindowManager知道“既然鎖屏被全屏窗口遮擋了那么系統(tǒng)返回鍵和某些焦點事件可以直接穿過”避免不必要的觸摸重定向。3.3 occluded與OffScreen的相互作用在Android 11里繞不開的一個話題是offScreen與occluded的區(qū)別。簡單說offScreen表示鎖屏窗口被移到了屏幕外比如用戶解鎖后鎖屏從當前顯示移除occluded表示鎖屏窗口仍然在屏幕上但被全屏窗口擋住。這兩個狀態(tài)在某些情況下會互相轉(zhuǎn)化。典型場景用戶正在通話中鎖屏被來電界面occlude通話掛斷后全屏界面退出occluded返回false但此時鎖屏不一定重新可見——如果在此之前用戶已經(jīng)完成了生物識別解鎖系統(tǒng)會直接走onKeyguardDismissed路徑把鎖屏徹底關(guān)閉。這個轉(zhuǎn)化過程中最常踩的坑是只監(jiān)聽setOccluded(false)就想當然認為鎖屏?xí)匦嘛@示結(jié)果發(fā)現(xiàn)KeyguardView還在鎖屏卻黑屏了。正確的做法是根據(jù)KeyguardViewMediator里當前mShowing、mHiding和用戶解鎖狀態(tài)綜合判斷不要只盯著occluded一個變量。4. 實戰(zhàn)中的特定場景解析4.1 場景一來電界面遮擋鎖屏這是最經(jīng)典的occluded場景也是最容易復(fù)現(xiàn)問題的測試路徑。正常流程是系統(tǒng)收到來電 → TelephonyRegistry通知SystemUI → 系統(tǒng)UI通過PhoneWindowManager內(nèi)部邏輯讓來電界面顯示在最高層級 → 觸發(fā)setOccluded(true)。在Android R上來電界面走的是CallNotification配合SystemUI內(nèi)部的StatusBar策略。這里有個細節(jié)值得說來電界面并不是直接調(diào)setOccluded的而是通過啟動一個全屏的Activity如InCallActivity這個Activity的window在PhoneWindowManager里被判為canShowWhenLocked屬性從而回調(diào)給Keyguard狀態(tài)機。我的實測經(jīng)驗是這個場景下最容易出現(xiàn)的問題是動畫沖突。如果鎖屏正在做指紋解鎖動畫同時來電界面請求occluded兩個動畫會搶資源出現(xiàn)解鎖動畫卡在半路、來電界面白屏幾秒的情況。排查方向是檢查WindowManager側(cè)的轉(zhuǎn)場動畫是否被設(shè)置為TRANSIT_OLD_WALLPAPER這種高開銷動畫。4.2 場景二視頻應(yīng)用申請沉浸式隱藏鎖屏很多視頻App在播放時會設(shè)置FLAG_SHOW_WHEN_LOCKED來避免鎖屏打斷觀影體驗。開發(fā)者申請這個flag時鎖屏?xí)遫ccluded流程。這個場景里真正的風(fēng)險在“取消遮擋”的一刻。視頻App退出全屏或播放結(jié)束時如果應(yīng)用直接清了flag但系統(tǒng)UI還沒來得及把鎖屏恢復(fù)顯示用戶會看到一個短暫的黑屏或桌面閃爍。Android R的優(yōu)化是讓KeyguardViewMediator在setOccluded(false)時判斷當前是否處于屏保狀態(tài)如果是則延遲恢復(fù)鎖屏避免閃爍。開發(fā)者在方案設(shè)計時建議預(yù)留至少300毫秒的重疊時間讓鎖屏View的alpha先恢復(fù)再讓全屏窗口退場能有效減少視覺突兀感。4.3 場景三鎖屏截圖黑屏問題很多人遇到過在occluded狀態(tài)下按電源鍵截圖截出來是黑屏。原因是鎖屏層被遮擋后截屏的buffer抓的是最上層全屏窗口內(nèi)容但這個過程有時會出現(xiàn)窗口同步問題導(dǎo)致抓到的是被遮擋中的鎖屏窗口而鎖屏窗口為了省電可能已經(jīng)停止渲染。Android R引入了一個補丁邏輯在occluded true時鎖屏View會跳過surface更新直接復(fù)用之前的buffer。這本來是為了省電但如果上層窗口也是全黑或者還沒準備就緒就會截到黑圖。排查方法很直接抓取dumpsys window看鎖屏窗口的mHasSurface標志。如果為false基本就是渲染被掛起導(dǎo)致。復(fù)現(xiàn)時可用命令縮短周期adb shell dumpsys window | grep -E mHasSurface|occluded|StatusBar4.4 場景四多用戶切換時的狀態(tài)殘留Android R的多用戶切換會觸發(fā)SystemUI重建而系統(tǒng)在切換完成前會做一輪鎖屏狀態(tài)重置。如果切換期間occluded狀態(tài)沒有被正確還原新用戶進入桌面時會遇到“鎖屏消失了但狀態(tài)欄還顯示鎖屏圖標”的詭異情況。根因通常是UserSwitch通知與setOccluded(false)時序交錯occluded的清理回調(diào)落在用戶切換廣播之前導(dǎo)致KeyguardOcclusionController以為還在遮擋狀態(tài)新用戶自然看不到鎖屏。解決方案是在BootReceiver或UserController的onUserSwitchComplete回調(diào)里主動調(diào)用一次setOccluded(false)強制復(fù)位。5. 常見問題速查與調(diào)試技巧5.1 常見問題對照表現(xiàn)象可能原因排查命令/思考方向來電全屏界面卡白屏occlusion和dismiss動畫沖突檢查WindowManager轉(zhuǎn)場動畫類型occludedfalse但鎖屏不恢復(fù)用戶已解鎖或dismiss流程已走完看mShowing、mHiding和生物識別狀態(tài)鎖屏截圖黑屏鎖屏surface渲染掛起dumpsys window查mHasSurface通知欄在鎖屏遮擋時還能下拉occluded回調(diào)沒有傳到狀態(tài)機檢查listener是否重復(fù)注冊/漏注冊多用戶切換后鎖屏狀態(tài)錯亂occluded清理時序早于用戶切換在onUserSwitchComplete里強制復(fù)位取消遮擋時屏幕閃爍全屏窗口退場和鎖屏恢復(fù)不同步考慮alpha漸變疊加或延遲恢復(fù)5.2 高效調(diào)試指令調(diào)試這套流程我最常用的命令是# 查看鎖屏狀態(tài)和遮擋狀態(tài) adb shell dumpsys statusbar | grep -E Keyguard|occluded # 查看WindowManager側(cè)的窗口信息 adb shell dumpsys window windows | grep -E Window|mCurrentFocus|mObscured # 查看SystemUI狀態(tài) adb shell dumpsys activity service com.android.systemui/.SystemUIService | grep -i keyguard如果懷疑是動畫問題可以臨時關(guān)閉動畫來快速驗證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 一個值得養(yǎng)成的習(xí)慣強烈建議在KeyguardViewMediator的setOccluded方法入口處加自定義日志把調(diào)用來源、當前mState、mShowing、mHiding、occluded變化值全部打出來。這個入口幾乎是所有鎖屏遮擋問題的交匯點日志一開很多問題直接水落石出。我自己實際用下來只要在這里加一行Log.d(TAG, setOccluded: occluded occluded animate animate state mState showing mShowing hiding mHiding);配合系統(tǒng)的dumpsys statusbar大部分occluded相關(guān)的問題都能在十分鐘內(nèi)定位到具體模塊。6. 從Android R延伸到更高版本的差異速覽Android R之后的S、T、U系列版本occluded的狀態(tài)管理有了不少變化這里羅列幾個影響較大的點供參考。6.1 從StatusBarStateController到CentralSurfaces在Android 12之后SystemUI做了大規(guī)模重構(gòu)原先的StatusBarStateController逐步被CentralSurfaces取代occluded狀態(tài)的存儲和回調(diào)也一并遷移。所以如果你的代碼是基于R寫的升級到更高版本時第一件事就是核對API類的包路徑。6.2 引入OccludingLayer和自定義布局Android 12還引入了OccludingLayer的概念讓鎖屏支持被部分遮擋比如只有底部導(dǎo)航區(qū)域被某個全屏應(yīng)用覆蓋。這類變更讓occluded不再是一個單純的布爾值問題而是一個幾何區(qū)域計算問題調(diào)試維度完全不同。6.3 安全策略增強更高版本在設(shè)備鎖屏策略上對FLAG_SHOW_WHEN_LOCKED的限制更嚴格比如要求應(yīng)用需要具備特定權(quán)限或用戶明確授權(quán)后才能隱藏鎖屏。開發(fā)者在定制ROM時要特別留意系統(tǒng)包對permission的校驗時序。不過基礎(chǔ)流程的原理并沒有變外部來源觸發(fā) → KeyguardViewMediator仲裁 → KeyguardViewController刷新狀態(tài) → NotificationShadeWindowController同步給WindowManager。理解了R上的這條鏈路再往上走會輕松很多。7. 結(jié)束語與個人實操心得最后分享幾個我在實際調(diào)試occluded流程時攢下的經(jīng)驗。第一動代碼之前先把dumpsys statusbar的輸出研究透。這個命令會給出當前keyguard的核心狀態(tài)快照包括occluded是否生效、前后臺狀態(tài)、window state等能省下大量盲調(diào)時間。第二測試時一定要覆蓋“快速連續(xù)變化”的場景。比如通話掛斷又立刻來電、指紋解鎖進行中來電這種并發(fā)場景很多時序bug都是在這種極限切換下爆出來的。自己寫自動化測試腳本時優(yōu)先模擬這些操作。第三多留意SystemUI日志里關(guān)于KeyguardOcclusionController和KeyguardViewController的ANR和warning。它們通常意味著Listener回調(diào)阻塞了主線程尤其是自定義ROM加了商用模塊之后這個問題會被放大。對我來說Android R這套occluded流程最吸引人的地方在于它不是一個孤立的開關(guān)而是一個橫跨WindowManager、SystemUI狀態(tài)機、鎖屏控制器、窗口轉(zhuǎn)場動畫的綜合性狀態(tài)協(xié)調(diào)機制。把這條鏈路吃透對整個Android系統(tǒng)UI的運作方式都會有更深的理解。