UsageStatsService之坑:一個XML解析異常導致的開機動畫死循環
聲明
鄭重聲明:博文爲原創內容,可以轉載或引用,但必須在明顯位置標明原文作者和出處,未經同意不得擅自修改本文內容!
博客地址:http://blog.csdn.net/luzhenrong45
問題說明
最近客戶返修了一個盒子(Android4.2系統),問題現象表現爲盒子開機後,系統的開機動畫不斷循環播放,一直沒進Launcher,而且是必現問題,每次開機都是如此。
同樣的ROM,同樣硬件類型的盒子,其他盒子都複製不出來,因此剛開始懷疑是OTA升級異常導致ROM系統文件丟失,進而導致開機啓動異常。
後來確認過,客戶升級使用的OTA包是沒有問題的,從升級日誌上來看也是OK的。爲了排除ROM文件導致問題的可能性,我還一一將boot 、kernel、system、recovery各個分區使用分區刷機工具重新刷機了,問題依舊…
日誌排查
幸運的是,盒子接上內部串口線,還能進入命令行模式,各種系統命令如ps, lsof, top,logcat 都能正常使用,這樣情況還不算最糟。(之前排查開機黑屏問題,什麼工具都用不了,各種想哭…)
趁現場環境還在,於是果斷先把logcat日誌復現了一份。
首先,我們知道,按照Android正常的開機啓動流程,system_server進程起來之後 ,會把一些系統重要的服務逐個拉起來,比如:PowerManagerService、ActivityManagerService、DisplayManagerService、PackageManagerService,這個從系統的logcat日誌也可以看出來,大致如下所示:
11-07 08:39:17.790 I/SystemServer( 666): Entered the Android system server!
11-07 08:39:17.800 I/installd( 103): new connection
11-07 08:39:17.800 I/SystemServer( 666): Enabled StrictMode logging for UI Looper
11-07 08:39:17.800 I/SystemServer( 666): Waiting for installd to be ready.
11-07 08:39:17.800 I/SystemServer( 666): Enabled StrictMode logging for WM Looper
11-07 08:39:17.800 I/Installer( 666): connecting...
11-07 08:39:17.800 I/SystemServer( 666): Entropy Mixer
11-07 08:39:17.830 E/RKPowerHAL( 666): Error opening /sys/devices/system/cpu/cpufreq/interactive/boostpulse: No such file or directory
11-07 08:39:17.830 I/SystemServer( 666): Power Manager
11-07 08:39:17.830 I/SystemServer( 666): Activity Manager
11-07 08:39:17.840 I/ActivityManager( 666): Memory class: 128
11-07 08:39:17.890 D/dalvikvm( 666): GC_CONCURRENT freed 246K, 7% free 4291K/4576K, paused 3ms+1ms, total 15ms
11-07 08:39:17.900 I/SystemServer( 666): Display Manager
11-07 08:39:17.900 I/SystemServer( 666): Telephony Registry
11-07 08:39:17.900 I/ActivityManager( 666): Enabled StrictMode logging for AThread's Looper
11-07 08:39:17.900 I/SystemServer( 666): Scheduling Policy
11-07 08:39:17.910 I/DisplayManagerService( 666): Display device added: DisplayDeviceInfo{"內置屏幕": 1920 x 1080, 60.000004 fps, density 160, 159.89508 x 160.42105 dpi, touch INTERNAL, FLAG_DEFAULT_DISPLAY, FLAG_ROTATES_WITH_CONTENT, FLAG_SECURE, FLAG_SUPPORTS_PROTECTED_BUFFERS, rotation 0, type BUILT_IN, address null}
11-07 08:39:17.920 I/SystemServer( 666): Package Manager
11-07 08:39:18.580 D/dalvikvm( 666): GC_EXPLICIT freed 332K, 8% free 4862K/5252K, paused 2ms+2ms, total 25ms
11-07 08:39:18.590 I/SystemServer( 666): User Service
11-07 08:39:18.590 I/SystemServer( 666): Account Manager
11-07 08:39:18.590 I/SystemServer( 666): Content Manager
11-07 08:39:18.590 I/SystemServer( 666): System Content Providers
11-07 08:39:18.690 I/libsuspend( 666): Selected early suspend
11-07 08:39:18.690 I/libsuspend( 666): Starting early suspend unblocker thread
11-07 08:39:18.690 I/SystemServer( 666): Lights Service
11-07 08:39:18.690 I/SystemServer( 666): Battery Service
11-07 08:39:18.690 I/SystemServer( 666): Vibrator Service
11-07 08:39:18.700 D/PermissionCache( 99): checking android.permission.ACCESS_SURFACE_FLINGER for uid=1000 => granted (316 us)
11-07 08:39:18.700 D/SurfaceFlinger( 99): Screen acquired, type=0 flinger=0x416f7318
11-07 08:39:18.700 D/SurfaceFlinger( 99): screen was previously acquired
11-07 08:39:18.700 D/AlarmManagerService( 666): Kernel timezone updated to -480 minutes west of GMT
11-07 08:39:18.700 I/SystemServer( 666): Alarm Manager
11-07 08:39:18.700 I/SystemServer( 666): Init Watchdog
11-07 08:39:18.700 I/SystemServer( 666): Input Manager
11-07 08:39:18.700 I/InputManager( 666): Initializing input manager, mUseDevInputEventForAudioJack=false
11-07 08:39:18.700 I/SystemServer( 666): Window Manager
而出問題的盒子,logcat日誌而不盡相同,通過對比正常日誌,發現在system_server啓動階段,日誌稍微不同:
11-07 14:13:23.780 I/SystemServer( 669): Entered the Android system server!
11-07 14:13:23.780 I/SystemServer( 669): Enabled StrictMode logging for UI Looper
11-07 14:13:23.780 I/SystemServer( 669): Waiting for installd to be ready.
11-07 14:13:23.780 I/SystemServer( 669): Enabled StrictMode logging for WM Looper
11-07 14:13:23.780 I/Installer( 669): connecting...
11-07 14:13:23.780 I/SystemServer( 669): Entropy Mixer
11-07 14:13:23.810 E/RKPowerHAL( 669): Error opening /sys/devices/system/cpu/cpufreq/interactive/boostpulse: No such file or directory
11-07 14:13:23.810 I/SystemServer( 669): Power Manager
11-07 14:13:23.810 I/SystemServer( 669): Activity Manager
11-07 14:13:23.820 I/ActivityManager( 669): Memory class: 128
11-07 14:13:23.850 W/UsageStats( 669): Usage stats version changed; dropping
**=========打了一行正常版本沒有的可疑日誌==========**
11-07 14:13:23.950 D/dalvikvm( 669): GC_CONCURRENT freed 283K, 8% free 4211K/4536K, paused 2ms+1ms, total 13ms
11-07 14:13:24.120 D/dalvikvm( 669): GC_CONCURRENT freed 402K, 10% free 4210K/4652K, paused 2ms+2ms, total 11ms
11-07 14:13:24.280 D/dalvikvm( 669): GC_CONCURRENT freed 396K, 10% free 4210K/4652K, paused 2ms+1ms, total 11ms
11-07 14:13:24.440 D/dalvikvm( 669): GC_CONCURRENT freed 403K, 10% free 4210K/4652K, paused 2ms+2ms, total 14ms
11-07 14:13:24.620 D/dalvikvm( 669): GC_CONCURRENT freed 399K, 10% free 4210K/4652K, paused 2ms+2ms, total 12ms
11-07 14:13:25.620 D/dalvikvm( 669): GC_CONCURRENT freed 400K, 10% free 4210K/4652K, paused 2ms+2ms, total 13ms
另外,使用 top 命令查看CPU佔用率情況,發現system_server進程 cpu佔用率高達 25% ,足足佔了一個核的資源!
重啓幾次,發現日誌大致相同。因此基本可以確定,問題出在 system_server 的初始化階段。更具體地講,從日誌上看,system_server在啓動 ActivityManagerService 服務之後,便”戛然而止“了,接下來的系統服務都沒有再接着拉起來,而是不斷打印一堆GC垃圾回收集日誌。
但是,如果細心查看日誌,會發現,出問題的盒子在不斷打印GC日誌之前,靜悄悄地打印了一條不爲人所知的日誌:
11-07 14:13:23.850 W/UsageStats( 669): Usage stats version changed; dropping
正常的啓動過程並不會打印這條日誌,而且在這條日誌之後,就開始無窮無盡的打印dalviak GC垃圾收集回收日誌,讓我不得不懷疑這個地方。
那麼,“UsageStatsService” 又是什麼鬼?
通過查詢資料,大致知道 UsageStatsService 是Android一個私有service,主要作用是收集用戶使用每一個APP的頻率、使用時常等,用於統計應用程序的使用情況。
該service 是在ActivityManagerService服務中啓動的:
這也剛好驗證了我的猜想,問題出在ActivityManagerService服務的啓動階段。既然如此,那就回到UsageStatService 當中來,從以下這句異常日誌入手:
11-07 14:13:23.850 W/UsageStats( 669): Usage stats version changed; dropping
搜索了一下4.2的系統源碼,發現該句日誌是在UsageStatsService.java 文件中一個叫“ readStatsFLOCK ” 的函數調用中打印的:
關於UsageStatsService的工作原理這裏不打算深入介紹,我們只需要知道UsageStatsService會在/data/system/usagestats/目錄下保存一些記錄app使用頻率和時長的數據文件。類似如下:
ls /data/system/usagestats/
usage-20161102
usage-20161103
usage-20161108
usage-history.xml
其中:
- /data/system/usagestats/usage-日期” 文件記錄的當天的使用記錄的數據
- /data/system/usagestats/usage-history.xml文件中讀取每個APP中每個Activity最後啓動的時間
UsageStatsService服務啓動的時候,會去讀取並解析這些文件。一般情況下,UsageStatsService取到的ver與 VERSION(常量1007),通過增加log,發現這裏取到的ver居然爲0,於是檢查了一下/data/system/usagestats/下面的文件,發現居然都是空文件零字節的!
猜想和驗證
猜想:會不會是 system_server初始化過程中,UsageStatService解析這些零字節文件時發生了文件操作異常?
驗證:使用 lsof 命令查看出問題時data分區所有被打開的文件,發現其中就有usage-history.xml !並且操作該文件的進程恰恰就是system_server!(此刻表示千軍萬馬從心裏奔騰而過)
root@android:/data/system/usagestats # lsof | grep data
system_se 1283 system 54 ??? ??? ??? ??? /data/system/usagestats/usage-history.xml
問題定位
到這裏基本可以確定是system_server 啓動過程中在讀寫 usage-history.xml 文件時出了問題。但是如果是簡單的文件操作錯誤,最多是報個crash,理論上CPU佔用率不應該那麼高。除非是文件操作過程中發生死鎖或者死循環之類的致命錯誤。
查了代碼,UsageStatsService服務會在多個地方操作到usage-history.xml 文件,單純從代碼上看,並不能快速確定出總理 的地方。
或許可以看看 system_server的調用堆棧,興許可以給我們留下一些線索…
查看進程堆棧,一種簡單而實用的方法就是使用Android自帶的DDMS工具。打開DDMS, 選中需要查看的進程號,這裏選擇的自然是 system_server (進程號爲1283),UsageStatsService是由ActivityManager啓動的,因此先選中ActivityManager,接下來就查看相應的調用堆棧了。
從函數中調用堆棧信息中,可以看到UsageStatsService相關的堆棧停在了readHistoryStatsFLOCK(),代碼370行的地方。
這個時候確實需要查看一下源碼了:
readHistoryStatsFLOCK(AtomicFile file)函數主要負責解析我們前面說到的usage-history.xml文件。
插曲 – XML的解析
在JAVA的世界裏,解析xml文件有多種方式,不同的解析方式有着各自的優缺點和適用環境。在Android中常見的XML解析器分別爲SAX解析器、DOM解析器以及PULL解析器。
這裏就不展開具體介紹了,具體可以參考本人以前寫的博文:
看代碼,知道上面使用的是PULL的方式來解析xml文件的,PULL技術是基於事件類型的解析,基本的事件類型有 5 個:
- START_DOCUMENT (常量0,標記文檔的開始)
- START_TAG (常量2,標記一個標籤的開始)
- TEXT (常量4,標記一個標籤的內容)
- END_TAG (常量3,標記一個標籤的結束)
- END_DOCUMENT (常量1,標記文檔的結束)
使用 parser.next() 可以進入下一個元素並觸發相應事件。現在,再來看看剛纔那段代碼:
while (eventType != XmlPullParser.START_TAG) {
eventType = parser.next();
}
從代碼上分析,這裏用了一個while循環,想要實現的功能是,一旦識別到XML的標籤事件(START_TAG),便退出等待循環,開始usage-history.xml標籤內容的解析工作。
但是,此處該碼有一個致命的問題:如果待解析的XML文件中到達文件結尾時,還解析不到START_TAG事件,便會陷入死循環當中!
而一旦陷在while死循環裏面,system_server便無法往下繼續啓動
其他系統服務,因而不會等到系統ready後通知系統進入桌面。於是便出現系統開機後一直停留在開機動畫播放階段,不斷循環的現象!
而前面我們說到,該設備usage-history.xml是個零字節空文件,因此問題便出在這裏了!
問題修復
知道原因,那麼,便知道怎麼修改了,一旦解析到達xml文件末尾,還解析不到
START_TAG 事件,就應該主動退出while循環。
while (eventType != XmlPullParser.START_TAG) {
+ //PN: fix empty xml file cause death cycle
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ Slog.w(TAG, "get END_DOCUMENT tag of xml file, maybe the file is empty, stop parsing");
+ break;
+ }
eventType = parser.next();
}
查看了各個歷史版本,Android4.4以更低的版本皆存在這樣的隱患問題,而Android5.0之後,由於UsageStatsService機制改變,因此已經修復該問題。
usage-history.xml一般情況下都會有內容,因此,觸發到死循環的概率很低。但有些情況也可能導致 usage-history.xml爲零字節:
- 比如用戶強行斷電情況下便可能導致IO讀寫無法正常結束,引發零字節問題。
- 比如 flash 硬件質量問題
- 比如Android Ext4文件系統的延遲分配(delalloc)功能,該功能網上反饋有有比較大的問題,可能造成數據丟失、0長度等問題,
PS: 解決此類問題的方案就是在分區掛載的時候,禁用延遲分配功能,也就是開啓 nodelalloc 選項。
舉一反三
另外,搜索了整個Android4.2的系統源碼,發現還有幾處代碼也是採用類似的寫法,這些地方也是埋了坑的,哪天不小心就會陷入這些死循環,
導致另一個開機白屏問題。因此,也是需要把這些隱患一塊修復掉!
參考資料
《Android UsageStatsService:要點解析》
《Why-delayed-allocation-is-bad》
《How to Solve Zero Length File Problem in Linux’s Ext4 File System》
修改說明
作者 | 版本 | 修改時間 | 修改說明 |
---|---|---|---|
WalkAloner | V1.0 | 2016/11/08 | 第一版 |
WalkAloner | V1.1 | 2019/07/26 | 圖牀搬遷,換markdown格式 |