UsageStatsService之坑:一個XML解析異常導致的開機動畫死循環

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_cpu

重啓幾次,發現日誌大致相同。因此基本可以確定,問題出在 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服務中啓動的:
ams
這也剛好驗證了我的猜想,問題出在ActivityManagerService服務的啓動階段。既然如此,那就回到UsageStatService 當中來,從以下這句異常日誌入手:

11-07 14:13:23.850 W/UsageStats(  669): Usage stats version changed; dropping

搜索了一下4.2的系統源碼,發現該句日誌是在UsageStatsService.java 文件中一個叫“ readStatsFLOCK ” 的函數調用中打印的:
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,接下來就查看相應的調用堆棧了。

ddms

從函數中調用堆棧信息中,可以看到UsageStatsService相關的堆棧停在了readHistoryStatsFLOCK(),代碼370行的地方。

這個時候確實需要查看一下源碼了:

readHistoryStatsFLOCK

readHistoryStatsFLOCK(AtomicFile file)函數主要負責解析我們前面說到的usage-history.xml文件。

插曲 – XML的解析

在JAVA的世界裏,解析xml文件有多種方式,不同的解析方式有着各自的優缺點和適用環境。在Android中常見的XML解析器分別爲SAX解析器、DOM解析器以及PULL解析器。
這裏就不展開具體介紹了,具體可以參考本人以前寫的博文:

《Android XML文檔解析(一)——SAX解析》

《Android XML文檔解析(二)——DOM解析》

《Android XML文檔解析(三)——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格式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章