長按白屏:SystemUI到Settings跳轉(zhuǎn)鏈路排查與修復(fù))
先說結(jié)論這種白屏問題十有八九不是SystemUI自己崩了而是長按之后要跳轉(zhuǎn)的那個設(shè)置頁Activity/Fragment在定制ROM里被改壞或者被裁掉了SystemUI這邊又沒做異常兜底于是直接啟動了一個空殼頁面。做系統(tǒng)定制的兄弟應(yīng)該都懂下拉狀態(tài)欄里的快捷開關(guān)AOSP里叫QSTile單擊負(fù)責(zé)開關(guān)功能長按負(fù)責(zé)跳轉(zhuǎn)到對應(yīng)的詳細(xì)設(shè)置頁。點(diǎn)按沒問題長按就白屏十有八九問題出在跳轉(zhuǎn)鏈路而不是開關(guān)本身。這個現(xiàn)象還特別容易出現(xiàn)在Android 10.0的定制ROM上尤其是從原生AOSP代碼改過來的項(xiàng)目。因?yàn)锳ndroid 10正好把Wi-Fi和移動數(shù)據(jù)整合成了一個新的“互聯(lián)網(wǎng)”Internet入口長按Wi-Fi圖標(biāo)不再像以前那樣直接跳進(jìn)WifiSettingsActivity而是會拉起一個網(wǎng)絡(luò)設(shè)置面板。如果這個面板在你的ROM里沒有正確注冊或者對應(yīng)的Fragment被裁剪了那白屏幾乎是必然的。藍(lán)牙那邊也是類似道理長按藍(lán)牙圖標(biāo)跳的是藍(lán)牙設(shè)置頁一旦Settings模塊出問題同樣白屏。這篇文章我會把整個排查和修復(fù)流程完整捋一遍包括怎么抓日志定位是SystemUI的鍋還是Settings的鍋、怎么用adb命令單獨(dú)拉起頁面來驗(yàn)證、代碼層面怎么改才穩(wěn)妥以及一些我在實(shí)際操作中踩過的坑。適合正在做Android 10.0系統(tǒng)定制、SystemUI二次開發(fā)或者維護(hù)第三方ROM的工程師參考。原生的代碼邏輯我也會點(diǎn)到但重點(diǎn)還是講清楚排查思路畢竟每家的改動不一樣思路通了比抄代碼管用得多。1. 先說現(xiàn)象快捷開關(guān)長按白屏到底是個什么鬼1.1 白屏現(xiàn)象和影響范圍先描述一下這個bug的典型表現(xiàn)。系統(tǒng)正常開機(jī)下拉狀態(tài)欄快捷開關(guān)區(qū)域能看到一排圖標(biāo)Wi-Fi、藍(lán)牙、移動數(shù)據(jù)、飛行模式這些。單擊Wi-Fi或者藍(lán)牙圖標(biāo)開關(guān)功能正常圖標(biāo)顏色會變Wi-Fi也能正常連上路由器藍(lán)牙也能正常配對。但是長按Wi-Fi圖標(biāo)或者長按藍(lán)牙圖標(biāo)本來應(yīng)該跳轉(zhuǎn)到對應(yīng)設(shè)置頁的結(jié)果屏幕突然白屏像是一個空白的Activity被拉起來了既沒有標(biāo)題欄也沒有內(nèi)容。屏幕響應(yīng)倒是沒死因?yàn)榘碒ome鍵還能退出但再點(diǎn)進(jìn)去也是同樣的白屏。這個問題的影響范圍看起來不大但實(shí)際很煩。用戶可能不常用長按功能可一旦用就會覺得系統(tǒng)“壞了”尤其是普通用戶不會區(qū)分“設(shè)置進(jìn)不去”和“系統(tǒng)崩潰”反饋到售后那邊的描述基本就是“手機(jī)出問題了”。測過幾個定制項(xiàng)目后發(fā)現(xiàn)這個問題通常不是偶發(fā)性的而是100%復(fù)現(xiàn)所以只要復(fù)現(xiàn)一次定位起來其實(shí)并不難。1.2 長按和單擊走的不是同一條代碼路徑很多不熟悉SystemUI內(nèi)部機(jī)制的開發(fā)可能第一反應(yīng)是在SystemUI的開關(guān)View里找問題覺得是長按事件沒處理好。這里先糾正一個方向在AOSP里快捷開關(guān)的單擊和長按本來就是兩條完全獨(dú)立的代碼路徑。單擊走的是handleClick邏輯很直接就是切換狀態(tài)、調(diào)用對應(yīng)服務(wù)。長按走的是handleSecondaryClick次級點(diǎn)擊的語義就是“我要進(jìn)入更多的設(shè)置”所以里面幾乎不會去干開關(guān)狀態(tài)的事而是直接構(gòu)造一個Intent跳轉(zhuǎn)Settings里的某個Activity或者Fragment。這條路徑上任何一個環(huán)節(jié)出了問題比如Intent構(gòu)造錯了、目標(biāo)Activity不存在、Settings端加載頁面時(shí)異常表現(xiàn)就是白屏。這一點(diǎn)理解之后排查范圍就能迅速縮小要么是SystemUI這邊跳轉(zhuǎn)的Intent有問題要么是Settings那邊被跳轉(zhuǎn)的頁面有問題。別再傻乎乎去查藍(lán)牙開關(guān)的底層邏輯了方向錯了只會浪費(fèi)時(shí)間。2. 白屏根因分析到底是SystemUI的鍋還是Settings的鍋2.1 Android 10.0里長按Wi-Fi和藍(lán)牙到底會跳到哪里AOSP原生代碼里Android 10.0的SystemUI對Wi-Fi圖標(biāo)的長按邏輯已經(jīng)不是直接跳到WifiSettingsActivity了。因?yàn)锳ndroid 10把Wi-Fi和移動數(shù)據(jù)整合到一個叫“互聯(lián)網(wǎng)”Internet的大設(shè)置入口里長按Wi-Fi圖標(biāo)會嘗試?yán)鹨粋€網(wǎng)絡(luò)相關(guān)的設(shè)置面板這個面板可能是Settings.Panel.ACTION_INTERNET也可能是直接跳轉(zhuǎn)到“網(wǎng)絡(luò)與互聯(lián)網(wǎng)”這個設(shè)置頁面。具體要看InternetTile這個類的實(shí)現(xiàn)它在源碼里的路徑大概是frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java關(guān)鍵代碼大致長這樣Override public void handleSecondaryClick(Nullable View view) { Intent intent new Intent(Settings.Panel.ACTION_INTERNET); mHost.startActivityDismissingKeyguard(intent); }藍(lán)牙的長按邏輯在BluetoothTile或者類似的Tile實(shí)現(xiàn)里一般會跳轉(zhuǎn)到Settings.ACTION_BLUETOOTH_SETTINGS。AOSP源碼路徑大概是frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java原生代碼里用的是startActivityDismissingKeyguard這個方法會先解鎖屏幕再啟動Activity。如果目標(biāo)Activity在系統(tǒng)里找不到理論上應(yīng)該拋ActivityNotFoundException但很多定制ROM在改代碼的時(shí)候給這層跳轉(zhuǎn)加了try-catch或者干脆把它包在了一個異步任務(wù)里異常吞掉之后Activity沒起來白屏就出現(xiàn)了。2.2 Settings端的白屏才是最常見的大頭從我實(shí)際處理過的幾個項(xiàng)目來看SystemUI端的Intent問題反而好修真正讓人頭疼的是Settings端。長按之后確實(shí)拉起了一個Activity但那個Activity里加載的Fragment在啟動過程中拋了異常于是呈現(xiàn)給用戶的就是一個白屏。具體原因可能有很多常見的有這么幾類第一定制ROM把Settings里的某個Fragment裁剪了但Activity還在。比如有些廠商精簡系統(tǒng)時(shí)把藍(lán)牙設(shè)置頁或者網(wǎng)絡(luò)設(shè)置頁相關(guān)的Fragment代碼刪掉了或者是通過混淆配置把這個類給干掉了Activity啟動后嘗試加載一個不存在的Fragment結(jié)果就是onCreate都走完了但頁面內(nèi)容區(qū)域完全是空的。第二Fragment依賴了某個系統(tǒng)服務(wù)或者Controller但那個服務(wù)在當(dāng)前版本的ROM里被改沒了或者返回了null。比如藍(lán)牙設(shè)置Fragment通常會依賴BluetoothAdapter如果藍(lán)牙服務(wù)起不來或者適配器沒初始化好Fragment就會在創(chuàng)建列表的時(shí)候拋NPE白屏可以說是必然的。第三主題資源問題。白屏并不全是崩潰也有可能是Activity起來了但布局加載時(shí)用到的自定義主題沒有正確適配導(dǎo)致背景是白色的、內(nèi)容也是白色的看起來像白屏實(shí)際只是看不見內(nèi)容。這種問題在替換了第三方主題或者改了overlay資源之后比較容易出現(xiàn)。2.3 還在白屏先看是“純白”還是“帶崩潰”排查的時(shí)候我習(xí)慣先把白屏分成兩種一種是純粹的白色背景好像是空Activity另一種是啟動時(shí)閃了一下然后白屏之后可能還會彈一個“設(shè)置屢次停止運(yùn)行”的對話框。如果是后者那基本就是Settings端Fragment或Activity拋異常了直接去抓崩潰日志最有效。如果一直是白色、沒有任何崩潰提示那更像是在啟動階段就被攔住了或者Activity的contentView根本沒加載出來。這兩種情況的側(cè)重點(diǎn)不同前者要去解崩潰后者要去查Activity啟動流程和布局加載。區(qū)分方法很簡單白屏出現(xiàn)之后不要急著按Home直接連adb抓logcat日志搜索關(guān)鍵字AndroidRuntime或者FATAL EXCEPTION有崩潰就是Settings端的問題什么都沒有再搜ActivityTaskManager看有沒有START相關(guān)的日志沒有的話就說明Activity壓根沒啟動起來。3. 修復(fù)實(shí)操從抓日志到改代碼的完整流程3.1 第一步先復(fù)現(xiàn)并抓全日志看清報(bào)錯再動手修這類白屏問題我從來不會一上來就翻代碼而是先把日志抓全。步驟很簡單手機(jī)連上adb在終端里執(zhí)行adb logcat -c清掉舊的日志然后讓測試機(jī)復(fù)現(xiàn)一次白屏最快的方法是下拉狀態(tài)欄長按Wi-Fi圖標(biāo)等白屏出現(xiàn)。再切回終端執(zhí)行adb logcat -d -b crash這條命令會打印出崩潰緩沖區(qū)的日志。如果白屏是由Settings崩潰導(dǎo)致的在這里基本就能看到關(guān)鍵信息。比如我之前遇到過一個案例日志里明確寫著FATAL EXCEPTION: main Process: com.android.settings, PID: 3127 java.lang.NullPointerException: Attempt to invoke virtual method boolean android.bluetooth.BluetoothAdapter.isEnabled() on a null object reference這就是典型的藍(lán)牙Adapter為null導(dǎo)致的白屏。原因也簡單定制ROM在啟動階段延遲初始化了藍(lán)牙服務(wù)Settings的Fragment加載的時(shí)候BluetoothAdapter還沒準(zhǔn)備好。如果crash緩沖區(qū)里沒有東西再用常規(guī)日志過濾關(guān)鍵進(jìn)程adb logcat -d | grep -E ActivityTaskManager|SystemUI|Settings重點(diǎn)看有沒有和START、Displayed相關(guān)的記錄確認(rèn)Activity到底啟動了沒有。這一步做完問題方向基本就能定了。3.2 第二步用adb直接拉起目標(biāo)Activity隔離問題邊界抓完日志后我習(xí)慣再用adb單獨(dú)啟動一次目標(biāo)Activity來進(jìn)一步確認(rèn)是SystemUI跳轉(zhuǎn)的問題還是Settings本身就起不來。以藍(lán)牙設(shè)置頁為例執(zhí)行adb shell am start -a android.settings.BLUETOOTH_SETTINGS直接拉起系統(tǒng)的藍(lán)牙設(shè)置頁。如果這樣拉起后頁面能正常顯示那就說明Settings本身沒毛病問題在SystemUI那邊的Intent構(gòu)造或者跳轉(zhuǎn)方式上。如果這樣拉起后依然是白屏那基本可以確定就是Settings端的問題了可以放心把重點(diǎn)放在Settings代碼上。對于Wi-Fi/互聯(lián)網(wǎng)入口對應(yīng)的Action有很多種可以先試試adb shell am start -a android.settings.WIFI_SETTINGS或者adb shell am start -a android.settings.INTERNET_SETTINGS拉到哪個能用就用哪個。這一步能非常快地把問題邊界畫出來避免在SystemUI代碼里瞎翻。我見過有同行花了一整天才查明白結(jié)果adb一拉發(fā)現(xiàn)Settings自己也起不來問題本來就出在Settings端。3.3 第三步代碼級修復(fù)三種情況的針對性處理確認(rèn)了問題邊界之后再動手改代碼心里就有底了。這里分三種情況說每種我都給出比較穩(wěn)妥的改法。情況一Settings端的Fragment或者Service依賴崩潰比如前面提到的BluetoothAdapter為null這種情況。穩(wěn)妥的做法是在Fragment里做空判斷至少保證頁面主體能渲染出來不讓整個Activity變成白屏。常見的處理是在onViewCreated或者列表Adapter初始化之前加一道保護(hù)BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); if (adapter null) { // 頁面主體改為顯示藍(lán)牙不可用之類的占位布局 showBluetoothUnavailablePlaceholder(); return; }當(dāng)然這是應(yīng)急修復(fù)長期來講還是得把藍(lán)牙服務(wù)的初始化時(shí)序問題解決了但至少用戶不會看到白屏。占位頁面可以用一個簡單的LinearLayout加上提示文字代碼量不大能顯著提升用戶體驗(yàn)。情況二SystemUI跳轉(zhuǎn)的Intent目標(biāo)不存在或者被裁剪這種情況在定制ROM里頻繁出現(xiàn)因?yàn)榫喸O(shè)置項(xiàng)的時(shí)候很容易把目標(biāo)Activity的注冊拿掉但SystemUI里沒人同步改。最穩(wěn)妥的改法是在SystemUI的Tile里加異常兜底。以InternetTile為例try { Intent intent new Intent(Settings.Panel.ACTION_INTERNET); mHost.startActivityDismissingKeyguard(intent); } catch (ActivityNotFoundException e) { Log.w(TAG, Internet settings not found, fallback to WIFI_SETTINGS, e); try { Intent fallbackIntent new Intent(Settings.ACTION_WIFI_SETTINGS); mHost.startActivityDismissingKeyguard(fallbackIntent); } catch (ActivityNotFoundException ex) { // 到這里說明連Wi-Fi設(shè)置都沒了那就只能提示一下了 } }這樣做的好處是即使主入口被裁了也能退而求其次跳到另一個相關(guān)設(shè)置頁而不是留一個白屏給用戶。情況三目標(biāo)Activity的exported屬性或權(quán)限問題有次遇到一個很有意思的問題SettingsActivity明明存在但SystemUI長按跳不過去原因是定制ROM給Settings的某個Activity加了android:exportedfalse導(dǎo)致跨進(jìn)程調(diào)用被拒絕。SystemUI是獨(dú)立進(jìn)程它要啟動Settings的Activity對方必須是exported的否則會拋SecurityException或者直接失敗。如果確認(rèn)是這個原因在Settings的AndroidManifest.xml里給對應(yīng)Activity補(bǔ)上android:exportedtrue同時(shí)檢查有沒有權(quán)限限制如果有permission要求要么補(bǔ)上權(quán)限聲明要么確認(rèn)調(diào)用方已經(jīng)有權(quán)限。這里要強(qiáng)調(diào)一下不是所有Activity都適合設(shè)成exported設(shè)置頁里的Activity一般問題不大但涉及支付、賬號等敏感頁面的就不要亂動。3.4 第四步編譯驗(yàn)證別只測一次就算完改完代碼當(dāng)然要編譯。如果只改了SystemUI在AOSP根目錄執(zhí)行make SystemUI -j16如果改了Settings執(zhí)行make Settings模塊編譯完替換進(jìn)系統(tǒng)鏡像重新燒錄或者push進(jìn)去。SystemUI通??梢赃@樣快速替換adb root adb remount adb push out/target/product/xxx/system/system/systemui/SystemUI.apk /system/system/systemui/SystemUI.apk adb rebootSettings也一樣替換對應(yīng)APK然后重點(diǎn)回歸這幾個場景一是長按Wi-Fi圖標(biāo)確認(rèn)能進(jìn)入設(shè)置頁而不是白屏二是長按藍(lán)牙圖標(biāo)確認(rèn)能進(jìn)入藍(lán)牙設(shè)置頁三是單擊Wi-Fi和藍(lán)牙圖標(biāo)確認(rèn)開關(guān)功能沒受影響四是飛行模式下長按這兩個圖標(biāo)確認(rèn)不會因?yàn)榫W(wǎng)絡(luò)狀態(tài)變化引出新的異常。這幾輪回歸沒問題修復(fù)才算真正落地。4. 常見問題與排查技巧實(shí)錄4.1 快速定位“白屏”是SystemUI的問題還是Settings的問題這里整理一個速查思路遇到白屏問題可以按這個順序排查現(xiàn)象優(yōu)先懷疑方向排查手段白屏且時(shí)有崩潰彈窗Settings端Fragment崩潰adb logcat -d -b crash白屏但無崩潰日志Activity未啟動或布局空洞adb logcat -d | grep ActivityTaskManager單擊正常長按白屏SystemUI跳轉(zhuǎn)邏輯或目標(biāo)Activity注冊adb shell am start單獨(dú)拉起目標(biāo)頁長按后快速閃一下再白屏Resource加載異常或主題問題檢查overlay資源和主題繼承關(guān)系只在特定定制版本出現(xiàn)裁剪導(dǎo)致的依賴缺失adb shell pm list packages對比目標(biāo)項(xiàng)這個表格基本能覆蓋90%的白屏場景。核心原則就是先定位邊界再動手改。4.2 長按熱點(diǎn)、NFC等開關(guān)白屏的通用處理思路Wi-Fi和藍(lán)牙只是最常見的兩個長按其他快捷開關(guān)也可能遇到類似白屏問題比如熱點(diǎn)、NFC、無線充電等。處理思路完全一樣不用為每個開關(guān)單獨(dú)撓頭。先把開關(guān)對應(yīng)的Tile類找到路徑一般都在frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/下面文件名和開關(guān)名基本對應(yīng)。然后看它的handleSecondaryClick方法確認(rèn)它長按之后跳的是什么Intent再去Settings里確認(rèn)對應(yīng)的Activity有沒有注冊、能不能正常啟動。如果不想每個Tile都去改一遍兜底邏輯也可以考慮在QSTileHost這一層做統(tǒng)一的Intent啟動包裝在startActivityDismissingKeyguard里統(tǒng)一加try-catch日志也統(tǒng)一打點(diǎn)后續(xù)排查起來能省不少事。4.3 邏輯對但Selector沒生效優(yōu)先檢查這幾個地方另外有個很容誤判的場景代碼邏輯完全對Settings也能正常起但長按跳轉(zhuǎn)過去之后頁面糊了看起來像白屏。這種情況優(yōu)先檢查AndroidManifest里Activity的啟動模式以及SystemUI傳過去的Intent有沒有帶特殊flag。比如有次遇到的問題是SystemUI啟動Wi-Fi設(shè)置時(shí)帶了FLAG_ACTIVITY_CLEAR_TASK直接把之前的設(shè)置頁任務(wù)棧清空了導(dǎo)致頁面起來后棧內(nèi)狀態(tài)異常內(nèi)容顯示不出來。去掉多余flag之后問題就消失了。還有一次是Intent里帶了一個不存在的extraSettings端解析extra時(shí)拋了異常同樣表現(xiàn)為白屏。這類問題沒有統(tǒng)一的解法邏輯上就是啟動參數(shù)越簡單越好非必要不傳extra。4.4 不同Android版本之間的差異要特別注意標(biāo)題雖然寫的是Android 10.0但實(shí)際做適配的兄弟可能在Android 11、12、13上也會遇到類似問題。不同版本的SystemUI代碼結(jié)構(gòu)差異很大比如Android 12開始SystemUI的很多類都遷移到了新的包名和架構(gòu)下Tile的獲取方式也變了。我見過有人拿著Android 10的修改方案硬套Android 13結(jié)果編譯都過不了。所以這里特別提醒換了一個大版本先看對應(yīng)版本的源碼結(jié)構(gòu)再決定怎么改。不要直接拿老補(bǔ)丁硬懟。比如Android 13里InternetTile的跳轉(zhuǎn)邏輯、Settings頁面的Fragment路徑和Android 10都有不小的差異白屏的根因也可能完全不同。4.5 獨(dú)家避坑技巧改完別忘回歸開關(guān)本身最后分享一個我踩過很多次才記牢的坑修白屏?xí)r很容易只顧著長按跳轉(zhuǎn)的路徑結(jié)果一不小心把單擊開關(guān)的功能也影響了。比如在Tile類里做異常兜底時(shí)改的handler被單擊和長按共用稍微一不注意單擊也會出問題。所以每次修完白屏除了驗(yàn)證長按跳轉(zhuǎn)一定把單擊開關(guān)、開關(guān)狀態(tài)同步、不同系統(tǒng)語言下的表現(xiàn)都過一遍?;貧w時(shí)間多了十來分鐘但能避免提交一個修了A害了B的補(bǔ)丁。5. 寫在最后的實(shí)操心得這類長按白屏的問題說穿了就是典型的系統(tǒng)定制殘留問題。原生AOSP在出廠狀態(tài)下通常不會有這種毛病一旦出現(xiàn)在定制ROM里八成是因?yàn)槟硞€模塊被改過、被精簡過或者被替換過導(dǎo)致原本能正常走的調(diào)用鏈斷了。SystemUI只是個受害者真的元兇往往在Settings那邊被裁掉了一個Fragment、一個Activity或者一個不起眼的服務(wù)依賴。我個人的習(xí)慣是遇到這類問題不要急著看代碼先花十分鐘把日志和Activity啟動情況摸清楚。用adb單獨(dú)拉起目標(biāo)Activity這一步真的特別好用能一瞬間把問題邊界畫清楚至少幫我省掉過一整天的無用功。修復(fù)的時(shí)候SystemUI側(cè)的異常兜底是必備品不管問題源頭在哪兒先保證長按不白屏、能回退到一個可用的頁面這是底線。Settings側(cè)的崩潰得治本空指針判斷和占位頁面只能算應(yīng)急真正讓依賴服務(wù)穩(wěn)定起來才是長久之計(jì)。如果大家在自己項(xiàng)目里還碰到過其他詭異的白屏場景不管是哪個快捷開關(guān)、哪個Android版本歡迎在評論區(qū)一起交流踩坑經(jīng)驗(yàn)共享出來后面的人就能少走點(diǎn)彎路。