Android 後臺耗電分析及最佳化

在路上發表於2020-12-11

原文見: 在路上的 blog

主要參考資料:

  • Android 程式效能最佳化之耗電最佳化
  • Android vitals 管理中心文件

開源後臺耗電分析工具: battery_alalyze

一、什麼是耗電最佳化?

在實踐中,如果我們的應用需要播放影片、獲取 GPS 資訊、需要拍照,這些耗電看起來是無法避免的。
如果發現某個應用沒怎麼使用(前臺時間很少),但是耗電卻非常多。這種情況會跟使用者的預期差別很大,這種情況就需要最佳化。

二、耗電最佳化第一個方向:最佳化後臺耗電

根據 Android Vitals 定義,影響後臺耗電的動作如下:

  • 喚醒鎖定操作卡住
  • 喚醒鎖定操作卡住(後臺)
  • 喚醒次數過多
  • WLAN 掃描次數過多(後臺)
  • 網路使用量過高(後臺)

1、喚醒鎖定操作卡住(前臺&後臺)

應用會透過呼叫帶有 PARTIAL_WAKE_LOCK 標記的 acquire() 來獲取部分喚醒鎖定。當您的應用在後臺執行時,如果部分喚醒鎖定保持了較長時間,則會變為卡住狀態(使用者看不到應用的任何部分)。 它會阻止裝置進入低功耗狀態。部分喚醒鎖定僅應在必要時使用,並且在不再需要時立即釋放。

Android Vitals 報告部分喚醒鎖定卡住的條件是在以下任一時段內至少發生了一次時長達 1 小時的部分喚醒鎖定:
(1)所有情況下至少 0.70% 的電池工作時段

(2)僅在後臺執行時至少 0.10% 的電池工作時段

喚醒鎖定操作卡住的問題發現和修復建議

2、喚醒次數過多

喚醒是 AlarmManagerAPI 中的一種機制,可讓開發者設定鬧鐘以在指定時間喚醒裝置。為設定喚醒鬧鐘,您的應用會呼叫 AlarmManager 中某個帶有 RTC_WAKEUPELAPSED_REALTIME_WAKEUP 標記的 set() 方法。當喚醒鬧鐘觸發時,裝置會在執行鬧鐘的 onReceive()onAlarm() 方法期間退出低功耗模式並保持部分喚醒鎖定。如果喚醒鬧鐘觸發次數過多,則可能會耗盡裝置的電池電量。

喚醒次數過多標準:使用者遇到每小時 10 次以上喚醒的電池工作時段數百分比。

  • Vital 詳細資訊:
    • 受影響的工作時段數:使用者遇到每小時 10 次以上喚醒的電池工作時段數百分比。電池會話是指裝置在兩次充滿電之間的間隔時間。Google 僅會在裝置未充電時收集這項資料。
    • 會話數:系統已記錄的會話的大概數量。
    • 第 90/99 個百分位:10%/1% 的每日工作時段中使用者每小時遇到喚醒次數高於顯示的值。 最低 25%:如果您的應用發生問題的工作時段比例等於或高於顯示的閾值,則系統會將此應用歸在這項指標的最低 25% 區間(依據為 Google Play 上前 1000 個熱門應用,按安裝量統計)。

喚醒過多修復及建議

3、WLAN 掃描次數過多(後臺)

當應用在後臺執行 WLAN 掃描時,它會喚醒 CPU,從而加快耗電速度。掃描次數過多時,裝置的電池續航時間可能會明顯縮短。如果某個應用處於 PROCESS_STATE_BACKGROUND 或 PROCESS_STATE_CACHED 狀態,則會被視為在後臺執行。

WLAN 掃描次數過多的標準:在後臺執行時,應用在 0.10% 的電池工作時段內每小時執行的掃描超過 4 次。

建議:如果可能,您的應用執行 WLAN 掃描時應該是在前臺執行。前臺服務會自動顯示通知;在前臺執行 WLAN 掃描,從而讓使用者知道裝置上發生 WLAN 掃描的原因和時間。

掃描次數過多最佳化:如果您的應用無法避免在後臺執行期間執行 WLAN 掃描,則可能適合採用偷懶至上策略。“偷懶至上” 包含三種可用於消減 WLAN 掃描次數的方法:“減少”、“推遲” 和 “合併”。如需瞭解這些方法,請參閱針對電池續航時間進行最佳化

4、後臺行動網路使用量過高

當應用在後臺連線行動網路時,應用會喚醒 CPU 並開啟無線裝置。如果反覆執行此操作,可能會耗盡裝置的電池電量。如果某個應用處於 PROCESS_STATE_BACKGROUND 或 PROCESS_STATE_CACHED 狀態,則會被視為在後臺執行。

後臺網路使用量過高的標準:在後臺執行時,應用在 0.10% 的電池工作時段內每小時傳送和接收的資料合計達 50 MB。

建議:可以將應用的行動網路使用量移至前臺,提醒使用者目前正在進行下載,併為他們提供暫停或停止下載的控制元件。為此,請呼叫 DownloadManager 並根據情況設定 setNotificationVisibility(int)

三、耗電最佳化第二個方向:讓系統認為是正常耗電

如何讓系統認為是正常耗電呢?當耗電指標低於規則時,系統也就認為是正常耗電了。

(1)海外應用

海外應用主要參考 Google Vitals 的規則。
對於 Google Vitals 的後臺耗電過多統計規則中的電池工作時段百分比,對於質量評估來看,較難把握。所以主要關注規則的具體指標,即相對更嚴格的質量要求:

(2)國內應用之華為後臺資源紅線標準

(3)經驗性總結規則

對於國內應用來說,目前還沒有非常通用且權威的後臺耗電規則,根據經驗,我們將監控的內容抽象成規則。
當然不同應用監控的事項或者引數都不太一樣。由於每個應用的具體情況都不太一樣。
下面是一些可以用來參考的簡單規則。

四、耗電監控

那我們的耗電監控系統應該監控哪些內容,怎麼樣才能比 Android Vitals 做得更好呢?

  • 監控資訊:簡單來說系統關心什麼,我們就監控什麼,而且應該以後臺耗電監控為主。類似 Alarm wakeup、WakeLock、WiFi scans、Network 都是必須的,其他的可以根據應用的實際情況。如果是地圖應用,後臺獲取 GPS 是被允許的;如果是計步器應用,後臺獲取 Sensor 也沒有太大問題。
  • 現場資訊:監控系統希望可以獲得完整的堆疊資訊,比如哪一行程式碼發起了 WiFi scans、哪一行程式碼申請了 WakeLock 等。還有當時手機是否在充電、手機的電量水平、應用前臺和後臺時間、CPU 狀態等一些資訊也可以幫助我們排查某些問題。

1、google vitals 不適合

缺點:

  • 耗電規則無法修改
  • 無法拿到堆疊和其他電池資訊
  • 國內應用無法使用

2、合適的耗電監控方式

(1)解析 bugreport

通常大家可能會使用 Battery Historian 來分析後臺耗電,但是不夠靈活。比如需要人工檢視各資源使用情況及是否達標。所以用 python 實現了一個簡單的分析 bugreport 檔案的小工具;
核心程式碼是剛做測開半年左右寫的,比較亂且水平有限,大家輕拍,也歡迎大家參與最佳化。

  • 實現邏輯:
    • 重置電池統計資訊和歷史記錄 (dumpsys batterystats --reset)
    • 開啟詳細的 wakelock 資料開關,日誌量較大,一般可正常儲存 3 個小時以內。
      • dumpsys batterystats --enable full-wake-history --啟用
      • dumpsys batterystats --disable full-wake-history --關閉
    • 匯出 bugreport 檔案
      • Android 7.0 and higher: adb bugreport > bugreport.zip
      • Android 6.0 and lower: adb bugreport > bugreport.txt
    • 利用battery_analyze生成後臺耗電報告

(2)Java Hook

Hook 方案的好處在於使用者接入非常簡單,不需要去修改自己的程式碼。下面我以幾個比較常用的規則為例,看看如果使用 Java Hook 達到監控的目的。

  • WakeLock:WakeLock 用來阻止 CPU、螢幕甚至是鍵盤的休眠。類似 Alarm、JobService 也會申請 WakeLock 來完成後臺 CPU 操作。WakeLock 的核心控制程式碼都在 PowerManagerService 中,實現的方法非常簡單。

// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this)


@Override
public void beforeInvoke(Method method, Object[] args) {
    // 申請 Wakelock
    if (method.getName().equals("acquireWakeLock")) {
        if (isAppBackground()) {
            // 應用後臺邏輯,獲取應用堆疊等等     
         } else {
            // 應用前臺邏輯,獲取應用堆疊等等
         }
    // 釋放 Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {
       // 釋放的邏輯    
    }
}
  • Alarm:Alarm 用來做一些定時的重複任務,它一共有四個型別,其中ELAPSED_REALTIME_WAKEUPRTC_WAKEUP型別都會喚醒裝置。同樣,Alarm 的核心控制邏輯都在AlarmManagerService中,實現如下:
// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this)


public void beforeInvoke(Method method, Object[] args) {
    // 設定 Alarm
    if (method.getName().equals("set")) {
        // 不同版本引數型別的適配,獲取應用堆疊等等
    // 清除 Alarm
    } else if (method.getName().equals("remove")) {
        // 清除的邏輯
    }
}
  • 其他:對於後臺 CPU,我們可以使用卡頓監控相關的方法。對於後臺網路,同樣我們可以透過網路監控相關的方法。對於 GPS 監控,我們可以透過 Hook 代理 LOCATION_SERVICE。對於 Sensor,我們透過 Hook SENSOR_SERVICE 中的 “mSensorListeners”,可以拿到部分資訊。

透過 Hook,我們可以在申請資源的時候將堆疊資訊儲存起來。當我們觸發某個規則上報問題的時候,可以將收集到的堆疊資訊、電池是否充電、CPU 資訊、應用前後臺時間等輔助資訊也一起帶上。

(3)插樁

雖然使用 Hook 非常簡單,但是某些規則可能不太容易找到合適的 Hook 點。而且在 Android P 之後,很多的 Hook 點都不支援了。
出於相容性考慮,我首先想到的是寫一個基礎類,然後在統一的呼叫介面中增加監控邏輯。以 WakeLock 為例:

public class WakelockMetrics {
    // Wakelock 申請
    public void acquire(PowerManager.WakeLock wakelock) {
        wakeLock.acquire();
        // 在這裡增加 Wakelock 申請監控邏輯
    }
    // Wakelock 釋放
    public void release(PowerManager.WakeLock wakelock, int flags) {
        wakelock.release();
        // 在這裡增加 Wakelock 釋放監控邏輯
    }
}

Facebook 也有一個耗電監控的開源庫 Battery-Metrics,它監控的資料非常全,包括 Alarm、WakeLock、Camera、CPU、Network 等,而且也有收集電量充電狀態、電量水平等資訊。
Battery-Metrics 只是提供了一系列的基礎類,在實際使用中,接入者可能需要修改大量的原始碼。但對於一些第三方 SDK 或者後續增加的程式碼,我們可能就不太能保證可以監控到了。這些場景也就無法監控了,所以 Facebook 內部是使用插樁來動態替換。
遺憾的是,Facebook 並沒有開源它們內部的插樁具體實現方案。大家可以自行搜尋不同插樁方案的實現。
插樁方案使用起來相容性非常好,並且使用者也沒有太大的接入成本。但是它並不是完美無缺的,對於系統的程式碼插樁方案是無法替換的,例如 JobService 申請 PARTIAL_WAKE_LOCK 的場景。

名次解釋

  • 電池工作時段:是指兩次電池充滿電的時間間隔。

相關文章