1. 程式人生 > >喚醒鎖: 檢測 Android* 應用中的 No-Sleep(無法進入睡眠)問題

喚醒鎖: 檢測 Android* 應用中的 No-Sleep(無法進入睡眠)問題

如果 Android* 應用使用喚醒鎖不當,將會顯著增加電池耗電量。 在本文中,我們將介紹一些提示和技巧,幫助您瞭解如何確認與誤用喚醒鎖有關的 No Sleep 漏洞。

1. 介紹

限制電池耗電量對智慧手機非常有必要。 為了獲得最大的自主性,Android 的作業系統設計可在檢測到系統上無使用者活動時進入睡眠模式。 一些應用需要裝置保持開啟狀態 — 即使長時間無使用者操作。 比如,看視訊、聽音樂、使用 GPS 以及玩遊戲。 Android 可為作業系統或應用提供了這樣的機制,以確保裝置保持喚醒狀態。 該機制稱為喚醒鎖。 如欲瞭解其他資訊,請閱讀 Christopher Bird 的文章: “

適用於 Android 的喚醒鎖”。

這種機制的出現讓管理元件活動的責任落到應用開發人員的身上。 如果使用錯誤,應用可能會大量消耗電池電量 — 即使應用並未在前臺執行。

2. 喚醒鎖

2.1.喚醒鎖簡介

喚醒鎖是一種控制主機裝置電源狀態的軟體機制。 作業系統可匯出明確的電源管理控制代碼和 API,以指定某個元件何時需要保持開啟或喚醒狀態,直至其從任務中被明確釋放。

喚醒鎖機制可在兩個層面上實施: 使用者和核心。 下圖展示了 Android 喚醒鎖實施的內部設計。 使用者喚醒鎖可被高層面的作業系統服務或應用採用,並通過電源管理服務提供。 它支援應用控制裝置的電源狀態。 核心喚醒鎖由作業系統核心或驅動程式控制。 使用者喚醒鎖被對映至核心喚醒鎖。 任何活動的核心層面喚醒鎖都可阻止系統在 ACPI S3 狀態掛起(在 RAM 掛起)— 它是移動裝置最節能的狀態。

2.2. Android 使用者喚醒鎖

Android 架構通過 PowerManager 匯出喚醒鎖機制。喚醒鎖可劃分為並識別四種使用者喚醒鎖:

請注意,自 API 等級 17 開始,FULL_WAKE_LOCK 將被棄用。 應用應使用 FLAG_KEEP_SCREEN_ON。

可以使用喚醒鎖強迫一些元件(CPU、螢幕和鍵盤)保持喚醒狀態。

請了解有關 PARTIAL_WAKE_LOCK 的特別提醒:無論任何顯示器超時或螢幕處於任何狀態,CPU 都將繼續執行 — 即使使用者按下電源按鈕。 這可能會導致出現靜默耗電,即手機看上去處於待機模式(螢幕關閉),但是實際上處於完全喚醒狀態。

在其他喚醒鎖中,使用者仍可使用電源按鈕讓裝置進入睡眠狀態。 按下電源按鈕後,除區域性喚醒鎖外所有喚醒鎖均將完全釋放。

上述即為應用控制喚醒鎖的方法。 基本而言,它是一個獲取/釋放機制。 當應用需要讓一些元件保持開啟狀態時,它便會獲取喚醒鎖。 當不再需要這些元件處於開啟狀態時,則需要將喚醒鎖釋放。

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
wl.acquire();
..screen will stay on during this section..
wl.release();

2.3. Android 核心喚醒鎖

核心喚醒鎖是由核心控制的低階喚醒鎖。 它們可從核心內部獲取/釋放。 就此而言,應用開發人員對它們的直接控制更少,但是應用的執行狀況可以間接觸發這些喚醒鎖並在無意中增加電池的耗電量。

下面是核心喚醒鎖的示例。

Wlan_rx: 當通過 Wi-Fi* 傳送或接收資料時由核心控制。

PowerManagerService: 是適用於所有區域性喚醒鎖的容器。

Sync: 在同步流程執行時啟用。

Alarm_rtc: 控制告警(當應用或流程執行定期檢查時使用)。

Main: 保持核心處於喚醒狀態。 系統進入掛起模式時,這是最後一個被釋放的喚醒鎖。

2.4. No-Sleep 漏洞

應用必須在某一時刻釋放它需要的每個喚醒鎖,以便允許系統返回深睡眠模式。 如果該喚醒鎖一直未被釋放,則該獲取/釋放機制可能會導致出現漏洞。 即使啟用了喚醒鎖的應用停止在前臺執行,喚醒鎖仍在使用中。 當使用釋放呼叫明確釋放喚醒鎖或應用被終止(強制關閉)時,喚醒鎖才可被釋放。 讓一些喚醒鎖處於啟用狀態將阻止系統進入深睡眠模式 — 即使沒有活動大量增加耗電量和靜默減少電池的自主性。 這就是 No-Sleep 漏洞。 由於 Android 的事件驅動特性,開發人員可能無法想到其應用獲取並需要關閉的喚醒鎖的所有程式碼路徑。 這種漏洞型別稱為 No-Sleep 漏洞程式碼路徑。

發生這種型別的漏洞的另一種情況是,喚醒鎖未有效獲取而被釋放。 在不同的執行緒中獲取和釋放喚醒鎖的多執行緒程式碼中可能會出現這種情況。 這就是 No-Sleep 漏洞競態條件。

最後一個問題是 No-sleep 擴張,其中獲取喚醒鎖的時間比實際所需的時間長。

為何重點指出這些問題? 根據 P. Vekris 的研究: “328 個使用喚醒鎖的應用中,55% 的應用未遵循我們針對 no-sleep 漏洞提供的策略”[2012]。 一些主要的應用在出現 No-Sleep 漏洞時被釋放。 因此,開發人員需要意識到這一點,以便以最優的方式執行其應用。

3. 找出 No-Sleep 漏洞

您可以通過兩種方式解決 no sleep 漏洞: 靜態的程式碼路徑掃描分析和動態的執行時分析。 在本文中,我們主要介紹執行時分析。

這種方法無法保證您找到應用中所有的喚醒鎖漏洞。 但是,它可以幫助您找到在執行時期間出現的喚醒鎖問題。 如要找出喚醒鎖問題,您需要按照未釋放喚醒鎖的程式碼路徑執行。 測試一個應用是否解決了 no sleep 漏洞包括在應用的不同位置操作應用,嘗試以不同的方式從應用中退出,以確認喚醒鎖是否仍然存在。

在某些情況下,有必要阻止系統進入深睡眠狀態 — 即使應用停止在前臺執行。 應用可能需要在後臺執行一項任務。 此時即為這種情況,例如,如果應用需要長時間下載: 視訊或遊戲資料集。 在進行下載時,使用者可能讓應用在後臺執行,但是在下載完成前,手機應保持喚醒狀態。 在這種情況下,在下載完成前,應用應一直啟用喚醒鎖。 當然,您需要確保在某個點釋放喚醒鎖。 例如,當手機在下載期間斷開網路,如果無法再執行操作,手機則無需保持喚醒狀態。

總之,找出 No Sleep 漏洞與所在的環境關係密切。 沒有預定義的標準可讓您輕鬆找出該問題。 只能依靠常識找到上述的漏洞。

3.1. 使用 adb

shell 命令是檢視喚醒鎖最簡單的工具。

如要完整了解核心喚醒鎖,請輸入:

adb shell cat /proc/wakelocks

name count expire_count wake_count active_since total_time
"PowerManagerService" 1502 0 0 0 337817677431
"main" 15 0 0 0 984265842688
"alarm" 1512 0 792 0 217778251643
"radio-interface" 16 0 0 0 16676538930
"alarm_rtc" 804 4 0 0 1204136324759
"gps-lock" 1 0 0 0 10753659786
name sleep_time max_time last_change
"PowerManagerService" 95729409122 140921663667 9723417252748
"main" 0 212424732355 9498127170228
"alarm" 217617362047 357976941 9723371461242
"radio-interface" 0 1659328496 9486387144974
"alarm_rtc" 1200253446201 66082936501 9483176054624
"gps-lock" 0 10753659786 37632803440

對於使用核心 3.4 或更高版本的映像,請使用“adb shell cat /sys/kernel/debug/wakeup_sources”。 雖然該格式可提供全部資訊,但是不太適合使用者使用。 我們在下面介紹的工具更方便。

使用“adb shell dumpsys power”可以輕鬆檢視特定的應用。 以下是該命令典型的輸出方式。 您可以看到,在命令釋出後,使用者喚醒鎖將以紅色呈現。 該命令可以檢視系統中呈現的使用者喚醒鎖。

Power Manager State:
mIsPowered=true mPowerState=3 mScreenOffTime=1618305 ms
mPartialCount=3
mWakeLockState=SCREEN_ON_BIT
mUserState=SCREEN_BRIGHT_BIT SCREEN_ON_BIT
mPowerState=SCREEN_BRIGHT_BIT SCREEN_ON_BIT
mLocks.gather=SCREEN_ON_BIT
mNextTimeout=2382037 now=2378097 3s from now
mDimScreen=true mStayOnConditions=3 mPreparingForScreenOn=false mSkippedScreenOn=false
mScreenOffReason=0 mUserState=3
mBroadcastQueue={-1,-1,-1}
mBroadcastWhy={0,0,0}
mPokey=0 mPokeAwakeonSet=false
mKeyboardVisible=false mUserActivityAllowed=true
mKeylightDelay=6000 mDimDelay=587000 mScreenOffDelay=7000
mPreventScreenOn=false mScreenBrightnessOverride=-1 mButtonBrightnessOverride=-1
mScreenOffTimeoutSetting=600000 mMaximumScreenOffTimeout=2147483647
mLastScreenOnTime=27380
mBroadcastWakeLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mStayOnWhilePluggedInScreenDimLock=UnsynchronizedWakeLock(mFlags=0x6 mCount=0 mHeld=true)
mStayOnWhilePluggedInPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=true)
mPreventScreenOnPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mProximityPartialLock=UnsynchronizedWakeLock(mFlags=0x1 mCount=0 mHeld=false)
mProximityWakeLockCount=0
mProximitySensorEnabled=false
mProximitySensorActive=false
mProximityPendingValue=-1
mLastProximityEventTime=0
mLightSensorEnabled=true mLightSensorAdjustSetting=0.0
mLightSensorValue=11.0 mLightSensorPendingValue=10.0
mHighestLightSensorValue=47 mWaitingForFirstLightSensor=false
mLightSensorPendingDecrease=false mLightSensorPendingIncrease=false
mLightSensorScreenBrightness=42 mLightSensorButtonBrightness=0 mLightSensorKeyboardBrightness=0
mUseSoftwareAutoBrightness=true
mAutoBrightessEnabled=true
creenBrightnessAnimator:
animating: start:42, end:42, duration:480, current:42
startSensorValue:47 endSensorValue:11
startTimeMillis:2361638 now:2378092
currentMask:SCREEN_BRIGHT_BIT
mLocks.size=4:
SCREEN_DIM_WAKE_LOCK          'StayOnWhilePluggedIn_Screen_Dim' activated (minState=1, uid=1000, pid=388)
PARTIAL_WAKE_LOCK             'StayOnWhilePluggedIn_Partial' activated (minState=0, uid=1000, pid=388)
PARTIAL_WAKE_LOCK             'HDA_PARTIAL_WAKE_LOCK' activated (minState=0, uid=10046, pid=4690)
PARTIAL_WAKE_LOCK             'AudioOut_2' activated (minState=0, uid=1013, pid=157)
mPokeLocks.size=0:

如要確認應用進入後臺後是否還有喚醒鎖啟用,您可以按照下列流程操作:

1. 將裝置連線至 USB。
2. 啟用應用並在該應用上操作。
3. 按電源按鈕進入睡眠模式或以某種方式退出應用。
4. 等待 20 秒鐘。
5. 以命令列的方式輸入下列指令:
> adb shell dumpsys power
6. 檢視是否有 PARTIAL_WAKE_LOCKs 與此相同,例如:
PARTIAL_WAKE_LOCK       ‘AudioOut_2’ activated(minState=0, uid=1013, pid=157)
7.每隔 15 秒鐘重複一次第 5 步,共重複 3 至 5 次。 如果結果相同,可能出現了問題。

3.2. 使用 BetterBatteryStats 應用

首先,通過選擇 “Other”條目,您可以檢視深睡眠和喚醒模式與總時間。 理想狀態下,大多數情況,如果手機未使用則應處於深睡眠狀態。

此外,您還可對比喚醒時間與螢幕開啟時間,以瞭解何時為實際活動狀態。 正常情況下,螢幕開啟時間和喚醒時間應處於關閉狀態。

您可以檢視各時間的電池充電評估和喚醒、螢幕開啟和 Wi-Fi* 狀態。

然後,您可以檢視核心喚醒鎖。 您可以檢視每種核心喚醒鎖花費的時間及數量。 時間長或數量多可能代表出現了問題。 在該報告中,您無法找出是哪一應用或流程導致熱點出現,但是可以發現特定應用觸發的執行狀況。

在核心喚醒鎖報告中,“PowerManagerService”可彙總使用者喚醒鎖中花費的時間。 如果該命令列顯示了熱點,您可以通過檢視區域性喚醒鎖報告找出。

大多數情況下,區域性喚醒鎖可指出控制它的應用。 找到導致問題出現的元凶會有很大的幫助。 但是,有時,一些活動可能通過其他應用啟動。 例如,遊戲可能會通過分配給 Android 媒體庫的 AudioOut 聲道播放聲音。 如果未正確編碼,您可能會認為未關閉聲道是由於遊戲出現問題, 而不會認為是 Android Gallery 出現問題。

AlarmManager 可能會提示告警導致喚醒出現,或某個應用做了大量的告警修改。 您可能希望檢視“告警”部分,但是隻有在擁有根許可權的映像上才能進行檢視。

此外,網路接入也僅支援有根許可權的映像。 如果您察覺到網路上出現較高的流量可能會有幫助。 multipdp/svnet-dormancy 核心喚醒鎖可能指示您還有一些較高的網路使用率。

4. 測試案例

讓我們看一個使用遊戲的真實案例。 啟動遊戲,玩 5 分鐘左右,然後以非常規的方式從遊戲中退出。 在我們的案例中,我們通過按主頁鍵強制退出。 音樂停止,主頁介面出現。 使用者看來,一切都正常。 停止活動幾分鐘後,螢幕正常變黑。 讓手機保持該狀態約半個小時的時間,然後使用 BestBatteryStats 來檢查它。 在“Other”介面上,您可以看到,雖然螢幕未開啟,但是手機仍然處於喚醒狀態。 此外,您還可以看到電池耗電率為 8.5%/小時。 因此在這種狀態下,充滿電的手機持續使用時間也不會超過 12 個小時。

現在,我們來看一下核心喚醒鎖。 我們可以看到,雖然沒有活動,但是兩個核心喚醒鎖仍然保持喚醒狀態。 其中一個是 PowerManagerService,這意味著有使用者區域性喚醒鎖開啟;另一個是 AudioOutLock 等待鎖。 我們來看一下區域性喚醒鎖介面。

在區域性喚醒鎖介面上,我們可以看到媒體庫應用中的音訊通道仍然開啟。 這很奇怪,因為使用者未明確使用媒體庫應用。 實際上,遊戲啟動了媒體庫,以便播放遊戲的音樂。 開發人員忘記了在主頁按鈕中斷應用時關閉音訊通道。 開發人員應將這種情況考慮在內,並相應地對應用進行修改。

5. 結論

喚醒鎖是非常有用且強大的工具。 但是如果使用不當,它們可能會對裝置的電池壽命產生非常不好的影響,從而極大地影響使用者體驗。 對於開發人員來講,應確保在 QA 過程中其程式碼未導致任何 No Sleep 漏洞出現。 他們應考慮使用可用的工具對實際使用中其應用對電池的影響進行分析,並儘量降低其對使用者裝置的影響。

6. 參考文獻