來慶波, 陳 博, 茆 蕾, 汪福翔, 司俊超
(中國科學(xué)技術(shù)大學(xué) 軟件學(xué)院(蘇州市中國科學(xué)技術(shù)大學(xué)蘇州研究院), 蘇州 215123)
社交網(wǎng)絡(luò)的快速發(fā)展, 使得智能手機(jī)已深度融入生活. 另一方面, 頻繁的交互操作使得手機(jī)功耗問題凸顯. 為延長待機(jī)時間, 許多智能手機(jī)系統(tǒng)使用睡眠策略來節(jié)省電量, 但有些應(yīng)用需要手機(jī)在某些關(guān)鍵計算時保持運(yùn)行狀態(tài). 例如銀行應(yīng)用程序, 當(dāng)用戶在線轉(zhuǎn)賬時,交易可能需要一段時間才能完成. 若手機(jī)在等待服務(wù)器消息時被置于休眠, 造成沒能及時響應(yīng), 則轉(zhuǎn)賬將會失敗. 為解決此問題, Android系統(tǒng)設(shè)計了喚醒鎖, 使某些硬件在計算時保持運(yùn)行狀態(tài) .
然而, 在現(xiàn)實中, 很多開發(fā)者存在濫用喚醒鎖的問題, 在不必要的時候仍占用喚醒鎖, 這加重電能消耗,嚴(yán)重影響用戶體驗.
為解決喚醒鎖誤用帶來的電耗增加問題, 本論文研究了常見的喚醒鎖誤用類型, 在此基礎(chǔ)上, 總結(jié)出了兩種判定誤用的策略: 第一種是根據(jù)CPU占用率模式,第二種是根據(jù)源碼分析建立黑名單. 并利用PowerManager提供的服務(wù)接口來檢測與釋放喚醒鎖.
Linux存在兩種電源管理方案: 高級電源管理(APM)和高級配置電源界面(ACPI). 缺省情況下運(yùn)行ACPI. ACPI在節(jié)電方面有很多機(jī)制, 可以讓你把機(jī)器處于Suspend(懸掛)或Standby(備用)狀態(tài). 還可以讓你把外設(shè)(如: 顯示器、顯卡、PCI總線)單獨(dú)斷電.
Linux的休眠機(jī)制可概括為以下三個步驟:
(1)凍結(jié)用戶態(tài)進(jìn)程和內(nèi)核態(tài)任務(wù);
(2)調(diào)用注冊的設(shè)備的Suspend的回調(diào)函數(shù), 其調(diào)用順序是按照驅(qū)動加載時的注冊順序;
(3)休眠核心設(shè)備和使CPU進(jìn)入休眠態(tài). 凍結(jié)進(jìn)程是內(nèi)核把進(jìn)程列表中所有的進(jìn)程的狀態(tài)都設(shè)置為停止, 并且保存下所有進(jìn)程的上下文.
標(biāo)準(zhǔn)的Linux電源管理機(jī)制是給帶有外接電源的電腦設(shè)計的, 睡眠機(jī)制有一些缺陷(如, 所有模塊必須同時睡眠或喚醒), 這會導(dǎo)致不必要的能耗. 這些機(jī)制并不適用于電池容量有限的移動平臺, 因此Android在Linux休眠機(jī)制的基礎(chǔ)上衍生出了獨(dú)特的WakeLock機(jī)制來管理和節(jié)省電源.
其基本原理如下: 當(dāng)啟動一個應(yīng)用程序的時候, 它可以申請一個wake_lock, 每當(dāng)申請成功之后都會在內(nèi)核中注冊一下(通知系統(tǒng)內(nèi)核, 現(xiàn)在已經(jīng)有鎖被申請),當(dāng)應(yīng)用程序在某種情況下釋放wake_lock的時候, 會注銷之前所申請的wake_lock. 特別要注意的是: 只要系統(tǒng)中有一個wake_lock, 系統(tǒng)此時都不能進(jìn)行睡眠.只有當(dāng)系統(tǒng)中所有的wake_lock都被釋放之后, 系統(tǒng)才會進(jìn)入真正的睡眠狀態(tài).
圖1描述了Android喚醒鎖調(diào)用的內(nèi)部設(shè)計. 內(nèi)核喚醒鎖是只能通過Linux內(nèi)核內(nèi)部獲取或釋放.Android開發(fā)人員無法直接控制內(nèi)核喚醒鎖, 但應(yīng)用程序可能會間接觸發(fā)這些喚醒鎖. 當(dāng)通過PowerManager的API創(chuàng)建和獲取喚醒鎖時, 該請求將通過綁定IPC機(jī)制傳遞到名為PowerManagerService的系統(tǒng)服務(wù). 如果該請求是PARTIAL_WAKE_LOCK實例, Power-ManagerService將調(diào)用Android_os_power.cpp的方法,通過JNI讀取/寫入Linux內(nèi)核系統(tǒng)處理請求. 否則,PowerManagerService本身將以不同于部分喚醒鎖定的方式處理請求. PowerManagerService記錄PARTIAL_WAKE_LOCK的數(shù)目. 當(dāng)應(yīng)用程序獲取PARTIAL_WAKE_LOC實例時, PowerManagerService會將計數(shù)增加1, 并在釋放喚醒鎖時減少1. 最后, 如果PARTIAL_WAKE_LOCK的計數(shù)為零, PowerManagerService將通知Linux電源管理系統(tǒng), 設(shè)備已經(jīng)可以進(jìn)入休眠模式.
圖1 Android喚醒鎖調(diào)用的內(nèi)部設(shè)計圖
近年來很多人開展了有關(guān)喚醒鎖的研究. 對于誤用喚醒鎖檢測方面, Pathak等人進(jìn)行了第一次的相關(guān)研究, 并采用數(shù)據(jù)流分析技術(shù)檢測喚醒鎖泄漏[1]. 后來相繼出現(xiàn)了其他靜態(tài)和動態(tài)分析技術(shù), 為喚醒鎖誤用的檢測提供了眾多方法. 例如: Vekris等人提出了一種驗證Android應(yīng)用程序中是否存在喚醒鎖泄漏的靜態(tài)分析技術(shù)[2]; 以及在一篇介紹 WLCleaner的論文中, 設(shè)計了一種動態(tài)分析技術(shù), 檢測由應(yīng)用程序引起的喚醒鎖誤用, 并采取措施糾正這些誤用[3]. 這些研究集中于研究喚醒鎖引起的能耗問題. 在資源管理和泄漏檢測方面, Jindal等人通過研究識別了Android設(shè)備驅(qū)動程序中的四種睡眠沖突類型, 并提出了一種避免這種沖突的運(yùn)行時調(diào)試系統(tǒng)[4]. Relda采用資源安全策略檢查的思想來檢測Android應(yīng)用程序中的資源泄漏, 包括喚醒鎖資源的泄漏[5].
據(jù)Android官方稱Android 8.0 (API26)為了改善電池壽命, 引入了一種新的機(jī)制[6]. 當(dāng)某個應(yīng)用程序進(jìn)入緩存狀態(tài)、沒有活動的組件時, 系統(tǒng)會自動釋放此應(yīng)用程序持有的所有喚醒鎖. 但是根據(jù)Google發(fā)布的數(shù)據(jù), Android 8.0目前的市場占有率僅有5%左右; 且目前很多在用的手機(jī), 制造商不會提供Android 8.0的系統(tǒng). 所以研究Android 8.0之前版本的功耗優(yōu)化目前仍具有現(xiàn)實意義.
喚醒鎖誤用是指程序本來應(yīng)該釋放喚醒鎖而因為各種原因未能正確釋放喚醒鎖. 導(dǎo)致這一問題的原因非常復(fù)雜, 下面分析幾種常見的情景.
由于Android程序?qū)儆谑录?qū)動型的, 不同的用戶交互行為或不同的硬件環(huán)境會導(dǎo)致不同的運(yùn)行路徑,在代碼動態(tài)運(yùn)行過程中, 會出現(xiàn)開發(fā)者未能預(yù)料到的運(yùn)行路徑. 即使經(jīng)驗豐富的開發(fā)者也難以避免這一情況. 原因在于: 觸發(fā)這一情況的條件比較苛刻. 只有在特定的使用場景下才會發(fā)生. 如, 特定的用戶交互行為或特定的使用環(huán)境(如GPS信號弱), 而這些情況是很少出現(xiàn)的.
如下面的代碼段:
上面的示例代碼是一段典型的可能存在喚醒鎖泄漏的情況, 關(guān)鍵任務(wù)run_cal()被喚醒鎖保護(hù), 以保證CPU在執(zhí)行此任務(wù)時不會進(jìn)入休眠狀態(tài). 按照開發(fā)者的設(shè)想, 執(zhí)行完run_cal()任務(wù)后, 喚醒鎖就被會被釋放掉. 但是如果在run_cal()執(zhí)行過程中, 發(fā)生了異常錯誤, 比如數(shù)學(xué)運(yùn)算時產(chǎn)生了除零錯誤、傳送文件時網(wǎng)絡(luò)斷開、打開的文件不存在、GPS信號弱等, 這時拋出的異常被catch語句塊捕獲并產(chǎn)生相應(yīng)的處理. 導(dǎo)致wkl.release語句不能被執(zhí)行, 從而使CPU不能進(jìn)入休眠狀態(tài). 一個解決方法是: 在finally語句塊中加入wkl.release()語句. 這樣, 即使run_cal()任務(wù)在執(zhí)行過程中發(fā)生了異常, 喚醒鎖也會被正確釋放.
在 youku 3.0[7]中, 開發(fā)者僅在 DownloadListenerImpl類的onFinished()回調(diào)函數(shù)中釋放了喚醒鎖,這樣當(dāng)在下載過程中出錯或取消下載時, 就會發(fā)生喚醒鎖泄漏. 正確的做法是在onCancel()和onException()兩個回調(diào)函數(shù)中增加釋放喚醒鎖的語句.
在mytracks[8]中, 程序在后臺中執(zhí)行記錄蹤跡任務(wù)時, 需要獲取喚醒鎖. 當(dāng)任務(wù)完成正常返回時, 調(diào)用onPostExecute()函數(shù)釋放喚醒鎖. 當(dāng)如果在任務(wù)執(zhí)行時, 發(fā)生了用戶取消任務(wù)或程序異常, 就會發(fā)生喚醒鎖泄漏. 正確的做法是在onCance()回調(diào)函數(shù)和finally語句塊中增加釋放喚醒鎖的語句.
Android程序的入口不是單一的main函數(shù), 而是若干個回調(diào)函數(shù). 這些回調(diào)函數(shù)可分為系統(tǒng)回調(diào)函數(shù)和用戶回調(diào)函數(shù). 系統(tǒng)回調(diào)函數(shù)包含組件的生命周期函數(shù) (如: onCreate(), onStart(), onResume(), onPause(),onStop(), onDestroy())和一些安卓框架函數(shù)(如Thread類里的run()). 用戶回調(diào)函數(shù)通常是UI事件觸發(fā)的, 如鼠標(biāo)點(diǎn)擊事件onClick()、觸摸事件onTouch()、鍵盤事件onKey()、狀態(tài)變換事件onFocusChange()等.
如果開發(fā)者僅僅在用戶回調(diào)函數(shù)中設(shè)置了釋放喚醒鎖的語句, 而未在系統(tǒng)回調(diào)函數(shù)中設(shè)置釋放語句. 釋放喚醒鎖的語句很可能因為用戶沒有執(zhí)行預(yù)設(shè)動作而未能觸發(fā). 這一情況在經(jīng)驗不足的開發(fā)者中經(jīng)常出現(xiàn).如, 在BaiduMap 5.0中開發(fā)者僅在用戶回調(diào)函數(shù)中設(shè)置了釋放LocationManager和AudioManager的語句.
在相應(yīng)的系統(tǒng)回調(diào)函數(shù)(如onPause(), onStop())中增加釋放喚醒鎖的語句, 可有效解決此類問題.
安卓的四大組件都有相應(yīng)的一組函數(shù)來處理生命周期事件. 圖2描述了Activity組件的生命周期[9].
圖2 Activity生命周期示意圖
Activity生命周期有三個嵌套循環(huán), 分別對應(yīng)完整生命期、可見生命期、前臺生命期.
生命周期函數(shù)都有固定的調(diào)用順序. 如在Activity組件被創(chuàng)建時, onCreate()、onStart()、onResume()會相繼被調(diào)用. 如果開發(fā)者對上述生命周期的調(diào)用順序理解不足, 可能會造成以下誤用: 喚醒鎖的釋放早于喚醒鎖的獲取, 這將會釋放不存在的喚醒鎖, 導(dǎo)致系統(tǒng)崩潰. 如ConnetBot[10]就存在此類誤用.
另外, 安卓系統(tǒng)在前臺退出一個進(jìn)程時, 為方便下次啟動更快速, Android后臺并沒有完全退出, 在內(nèi)存足夠的情況下系統(tǒng)還會把它留在內(nèi)存里. 只有當(dāng)手機(jī)內(nèi)存不足以啟動一個新進(jìn)程時, Android才會把不用的進(jìn)程徹底停掉. 也就是說, 在前臺退出進(jìn)程時onDestroy()并沒有被調(diào)用. 如果開發(fā)者對上述知識理解不足, 可能會僅在onDestroy()中釋放喚醒鎖而沒有在onPasuse()中釋放喚醒鎖. 這會導(dǎo)致系統(tǒng)長時間不能進(jìn)入休眠狀態(tài), 從而引起電耗增加.
Android系統(tǒng)提供了四種喚醒鎖. PARTIAL_WAKE_LOCK: 保持CPU運(yùn)轉(zhuǎn), 屏幕和鍵盤燈有可能是關(guān)閉的. SCREEN_DIM_WAKE_LOCK: 保持CPU運(yùn)轉(zhuǎn), 允許保持屏幕低亮度顯示, 允許關(guān)閉鍵盤燈. SCREEN_BRIGHT_WAKE_LOCK: 保持CPU運(yùn)轉(zhuǎn), 允許保持屏幕高亮顯示, 允許關(guān)閉鍵盤燈. FULL_WAKE_LOCK: 保持 CPU 運(yùn)轉(zhuǎn), 保持屏幕高亮顯示, 鍵盤燈也保持常亮. 使用PARTIAL_WAKE_LOCK鎖,無論屏幕的狀態(tài)是什么, 或者用戶按了電源按鈕, CPU都會繼續(xù)工作. 如果是其它的喚醒鎖, 設(shè)備會在用戶按下電源鈕后停止工作進(jìn)入休眠狀態(tài).
表1 Android喚醒鎖類型
如果開發(fā)者對以上所述的喚醒鎖類型理解不到位,使用了不合適的喚醒鎖類型, 就會造成誤用. 例如在記步軟件中, 進(jìn)程在后臺獲取位置信息的時候并不需要屏幕保持常亮, 使用PARTIAL_WAKE_LOCK保持CPU運(yùn)轉(zhuǎn)就可以了. 如果開發(fā)者使用了FULL_WAKE_LOCK類型的鎖讓屏幕保持常亮, 就會造成不必要的電量消耗.
本文研究了喚醒鎖檢測及電耗優(yōu)化機(jī)制, 并開發(fā)一款A(yù)ndroid應(yīng)用程序“Wlresolver”, 來實現(xiàn)這些機(jī)制.
在Android系統(tǒng)中可以通過PowerManager提供的服務(wù)接口來獲取與釋放WakeLock, 然而用戶無法直接訪問WakeLock的相關(guān)數(shù)據(jù). PowerManagerService位于應(yīng)用框架層, 提供與電源管理相關(guān)的一系列接口,是整個系統(tǒng)的電源管理核心, 在應(yīng)用框架層之下的硬件抽象層有一個power.c文件, 通過上層傳遞的參數(shù),向/sys/power/wake_lock或者/sys/power/wake_unlock文件寫入數(shù)據(jù)來與內(nèi)核進(jìn)行通信, Wlresolver在Android系統(tǒng)層次結(jié)構(gòu)中的位置如圖3.
圖3 Wlresolver在Android系統(tǒng)層次結(jié)構(gòu)圖中的位置圖
Wlresolver主要由三個部分組成: WakeLock檢測模塊、WakeLock處理模塊、用戶交互界面模塊.
當(dāng)手機(jī)屏幕關(guān)閉后延時觸發(fā)處理模塊從檢測模塊中獲取數(shù)據(jù)并處理其中誤用的WakeLock. 當(dāng)屏幕關(guān)閉動作產(chǎn)生時, Android系統(tǒng)會向所有應(yīng)用發(fā)出廣播, 在軟件中添加廣播接收器接收系統(tǒng)廣播, 當(dāng)收到系統(tǒng)發(fā)出的屏幕關(guān)閉廣播時觸發(fā)啟動后臺服務(wù)的代碼, 此時處理模塊開始運(yùn)行, 當(dāng)屏幕點(diǎn)亮?xí)r, 另一個廣播接收器接收到廣播, 關(guān)閉處理模塊.
WakeLock檢測模塊通過adb shell命令“dumpsys power”獲取 WakeLock 相關(guān)的信息, 如圖 5, 并且將得到的原始文本數(shù)據(jù)轉(zhuǎn)換所成需要的數(shù)據(jù), 轉(zhuǎn)換出的數(shù)據(jù)包含三個部分的信息: WakeLock對應(yīng)的軟件名稱、WakeLock類型、uid、pid.
Wlresolver的工作流程圖如圖6所示.
圖4 Wlresolver的主要模塊示意圖
圖5 Wlresolver獲取數(shù)據(jù)
圖6 Wlresolver工作流程圖
當(dāng)程序被觸發(fā)時, 首先導(dǎo)出系統(tǒng)中的WakeLock數(shù)據(jù), 并將原始數(shù)據(jù)解析為我們所需要的數(shù)據(jù). 接著過濾掉 uid>10000 的數(shù)據(jù), 因為 uid>10000 的是系統(tǒng)程序, 系統(tǒng)程序在屏幕關(guān)閉時可能需要一直運(yùn)行, 強(qiáng)行關(guān)閉系統(tǒng)程序可能導(dǎo)致系統(tǒng)崩潰.
為了判斷應(yīng)用程序是否有誤用, 第一個策略是: 檢測持有喚醒鎖的進(jìn)程的CPU使用率. 如果某進(jìn)程一直持有喚醒鎖, 但是在某段時間內(nèi)(如50 s)未使用CPU,可以判定為該進(jìn)程申請的喚醒鎖為非必要的持有, 可以將其釋放. 因為根據(jù)統(tǒng)計數(shù)據(jù)[11], 有70%的持有喚醒鎖的進(jìn)程會一直使用CPU, 其它的持有喚醒鎖的進(jìn)程也會在5 s以內(nèi)使用CPU(由于程序運(yùn)行中可能會因為一些操作中斷例如I/O或者用戶交互, 在這種情況下它將在短時間內(nèi)恢復(fù)運(yùn)行).
另一個釋放喚醒鎖的策略是: 先利用反向工程取得應(yīng)用程序的源碼, 然后利用elite進(jìn)行靜態(tài)分析[12], 預(yù)先識別出存在誤用的應(yīng)用程序. 進(jìn)而在Wlresolver中建立一個黑名單列表. 黑名單內(nèi)的進(jìn)程如果持續(xù)持有喚醒鎖超過某個時間就釋放該進(jìn)程的喚醒鎖.
為評估Wlresolver的使用效果和本文提出的優(yōu)化策略的有效性, 我們開展了相關(guān)實驗.
本文使用HUAWEI GRA-TL00型號手機(jī)作為測試平臺, Android版本為5.0.1. 為了保證整個測試過程的一致性, 手機(jī)關(guān)閉了wifi信號、蜂窩信號等使用頻次不確定的服務(wù).
本文使用Google開發(fā)的battery-historian來檢測電量消耗. Android為了方便開發(fā)人員分析整個系統(tǒng)平臺和某個進(jìn)程在運(yùn)行時間內(nèi)的所有信息, 專門開發(fā)了bugreport工具. 在終端執(zhí)行: adb bugreport >bugreport.txt, 即可生成 bugreport文件. Google 針對Android 5.0(api 21)以上的系統(tǒng)開發(fā)了一個叫做battery historian的分析工具, 用來解析bugreport.txt文本文件,并用Web圖形的形式展現(xiàn)出來, 從而獲得詳細(xì)的電池耗電情況[13].
本文從華為應(yīng)用商店下載了70個安卓應(yīng)用程序,并且從Google Code、Github、SourceForge等開源倉庫下載了40個開源軟件, 進(jìn)行測試.
在手機(jī)中運(yùn)行測試軟件, Wlresolver檢測到了6個進(jìn)程(pedometer、悅動圈、網(wǎng)易云音樂等)在運(yùn)行時持有喚醒鎖. Wlresolver開啟服務(wù)后, 有一個進(jìn)程(悅動圈)被殺掉. 說明根據(jù)Wlresolver的策略判定悅動圈持有的喚醒鎖為誤用. 我們對悅動圈的使用情況進(jìn)行了分析, 發(fā)現(xiàn)此進(jìn)程在用戶不記步的情況下仍然持有喚醒鎖, 消耗大量電能, 確實存在誤用的情況.
此后, 我們進(jìn)行了電量消耗速率進(jìn)行了多組測試.選取了四組實驗數(shù)據(jù)繪圖, 四組實驗在Wlresolver未開啟服務(wù)的情況下分別運(yùn)行1 h/5 h/12 h/24 h, 接著在Wlresolver開啟服務(wù)的情況下分別運(yùn)行1 h/5 h/12 h/24 h. 使用battery-historian獲得電量消耗數(shù)據(jù), 進(jìn)行電量消耗對比. 從圖9可以看出開啟服務(wù)后電量消耗速率均有所下降, 平均下降了1.85%. 下降幅度取決于存在喚醒鎖誤用的進(jìn)程數(shù)量. 如果手機(jī)運(yùn)行時存在喚醒鎖誤用的進(jìn)程越多, 電耗下降效果就會越明顯.
圖7 Wlsolver運(yùn)行時截圖
圖8 利用battery-historian顯示電池消耗數(shù)據(jù)
圖9 根據(jù)battery-historian數(shù)據(jù)繪制的電耗速率對比圖
本文首先介紹了Linux和Android的休眠機(jī)制. 然后分析了Android開發(fā)者在實際編碼過程中可能存在的喚醒鎖誤用類型及原因. 最后研究了喚醒鎖檢測及電耗優(yōu)化機(jī)制并通過Wlresolver實現(xiàn)了上述機(jī)制, 開啟服務(wù)后, 軟件會自動按照既定策略運(yùn)行, 不需要用戶手動干預(yù); 經(jīng)過實驗驗證, Wlresolver啟動服務(wù)后成功的清除了誤用的喚醒鎖, 系統(tǒng)電量消耗有所下降.