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