Android耗電原理及飛書耗電治理

語言: CN / TW / HK

這篇文章中簡紹Android的耗電原理,以及飛書的耗電治理規劃。

Android耗電統計原理

我們先了解一下Android系統是如何進行耗電的統計的,最精確的方式當然是使用電流儀來進行統計,但是正常狀態下手機硬件不支持,所以系統統計耗電時,使用的基本是模塊功率✖️模塊耗時這個公式來進行的,但不同的模塊還是會有一些差別。這種統計方式沒法做到非常的精確,但是也基本能反應出各應用電量的消耗大小。

模塊功率

我們先來看看模塊功率,每個模塊的耗電功率都是不一樣的,以計算方式來分,又分為下面三類

  1. 第一類是Camera、FlashLight、MediaPlayer等一般傳感器或設備的模塊的模塊。其工作功率基本和額定功率保持一致,所以模塊電量的計算只需要統計模塊的使用時長再乘以額定功率即可。
  1. 第二類是Wifi、Mobile、BlueTooth這類數據模塊。其工作功率可以分為不同的檔位,比如,當手機的 Wifi 信號比較弱的時候,Wifi 模塊就必須工作在比較高的功率檔位以維持數據鏈路,所以這類模塊的電量計算有點類似於我們日常的電費計算,需要 “階梯計費”。
  1. 第三類是屏幕,CPU模塊。CPU 模塊除了每一個CPU Core 需要像數據模塊那樣階梯計算電量之外,CPU 的每一個集羣(Cluster,一般一個集羣包含一個或多個規格相同的 Core)也有額外的耗電,此外整個 CPU 處理器芯片也有功耗。簡單計算的話,CPU 電量 = SUM (各核心功耗) + 各集羣(Cluster)功耗 + 芯片功耗 。屏幕模塊的電量計算就更麻煩了,很難把屏幕功耗合理地分配給各個 App, 因此 Android 系統只是簡單地計算 App 屏幕鎖(WakeLock)的持有時長,按固定係數增加 App CPU 的統計時長,粗略地把屏幕功耗算進 CPU 裏面。

每個模塊的功耗大小位於framework的power_profile.xml文件中,由廠商自己提供,裏面規定了每個模塊的功耗,下面是一台一加9的測試機的power_profile文件

通過apktook反解出來的power_profile如下

文件中每個模塊的對應説明,可以在谷歌提供的文檔中看到詳細的説明。

http://source.android.com/devices/tech/power/values

模塊耗時

瞭解了模塊的功率,我們再來看看模塊耗時,耗電模塊在工作或者狀態變更時,都會通知batterystats這個service,而BatteryStatsService會調用BatteryStats對象進行耗時的統計,BatteryStats的構造函數中會初始化各個模塊的Timer,用來進行耗時的統計,並將統計的數據存儲在batterystats.bin文件中

BatteryStatsImpl.java

我們來詳細看看下面幾個模塊的是如何進統計的,

  • wifi模塊

``` public void noteWifiOnLocked() { if (!mWifiOn) { final long elapsedRealtime = mClocks.elapsedRealtime(); final long uptime = mClocks.uptimeMillis(); mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_ON_FLAG; addHistoryRecordLocked(elapsedRealtime, uptime); mWifiOn = true; mWifiOnTimer.startRunningLocked(elapsedRealtime); scheduleSyncExternalStatsLocked("wifi-off", ExternalStatsSync.UPDATE_WIFI); } }

public void noteWifiOffLocked() {
    final long elapsedRealtime = mClocks.elapsedRealtime();
    final long uptime = mClocks.uptimeMillis();
    if (mWifiOn) {
        mHistoryCur.states2 &= ~HistoryItem.STATE2_WIFI_ON_FLAG;
        addHistoryRecordLocked(elapsedRealtime, uptime);
        mWifiOn = false;
        mWifiOnTimer.stopRunningLocked(elapsedRealtime);
        scheduleSyncExternalStatsLocked("wifi-on", ExternalStatsSync.UPDATE_WIFI);
    }
}

```

  • Audio模塊

``` public void noteAudioOnLocked(int uid) { uid = mapUid(uid); final long elapsedRealtime = mClocks.elapsedRealtime(); final long uptime = mClocks.uptimeMillis(); if (mAudioOnNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_AUDIO_ON_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Audio on to: " + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); mAudioOnTimer.startRunningLocked(elapsedRealtime); } mAudioOnNesting++; getUidStatsLocked(uid).noteAudioTurnedOnLocked(elapsedRealtime); }

public void noteAudioOffLocked(int uid) {
    if (mAudioOnNesting == 0) {
        return;
    }
    uid = mapUid(uid);
    final long elapsedRealtime = mClocks.elapsedRealtime();
    final long uptime = mClocks.uptimeMillis();
    if (--mAudioOnNesting == 0) {
        mHistoryCur.states &= ~HistoryItem.STATE_AUDIO_ON_FLAG;
        if (DEBUG_HISTORY) Slog.v(TAG, "Audio off to: "
                + Integer.toHexString(mHistoryCur.states));
        addHistoryRecordLocked(elapsedRealtime, uptime);
        mAudioOnTimer.stopRunningLocked(elapsedRealtime);
    }
    getUidStatsLocked(uid).noteAudioTurnedOffLocked(elapsedRealtime);
}

```

  • Activity狀態改變

``` public void noteActivityResumedLocked(int uid) { uid = mapUid(uid); getUidStatsLocked(uid).noteActivityResumedLocked(mClocks.elapsedRealtime()); }

public void noteActivityPausedLocked(int uid) { uid = mapUid(uid); getUidStatsLocked(uid).noteActivityPausedLocked(mClocks.elapsedRealtime()); }

public static class Uid extends BatteryStats.Uid {

@Override
public void noteActivityPausedLocked(long elapsedRealtimeMs) {
    if (mForegroundActivityTimer != null) {
        mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs);
    }
}

@Override
public void noteActivityPausedLocked(long elapsedRealtimeMs) {
    if (mForegroundActivityTimer != null) {
        mForegroundActivityTimer.stopRunningLocked(elapsedRealtimeMs);
    }
}

} ```

通過上面三個例子可以看到,BatteryStats在統計模塊耗時,主要通過Timer來進行時長的統計,如WifiOnTimer、AudioOnTimer、ForegroundActivityTimer,並且根據是否有UID來決定是否要統計到UID對應的數據中,系統在統計應用的耗電時,就是根據UID下各個模塊的統計數據,來進行應用的耗電計算的。

耗電計算

當我們知道了每個模塊的耗時,每個模塊的功耗,那麼就能計算各個模塊的耗電量了,耗電量的計算在BatteryStatsHelper這個類中,下面詳細看一下Setting中,應用耗電詳情這個功能統計耗電的實現,Setting中的耗電統計這個應用主要是調用了BatteryStatsHelper中的refreshStats() 函數

refreshStats主要兩個方法是processappUsage計算應用的耗電,記憶processMiscUsage計算雜項耗電,如WIFI,通話等等

  • 計算app的電量

這裏以CameraPowerCalculator這個簡單的模塊看看它是如何統計電量的

CameraPowerCalculator.java

可以看到,裏面只是簡單的用了totalTime * mCameraPowerOnAvg,mCameraPowerOnAvg則是從power_profile.xml讀取出來,其他教負責的如CPU模塊的計算,感興趣的可以自己看看,就不在這裏説了。

  • 計算misc雜項的電量

雜項電量用來統計一些沒有特定UID的耗電,如藍牙,屏幕等等,計算方式也是類似的。

Android的耗電優化策略

Doze模式

Doze模式也被稱為低電耗模式,是針對整個系統進行一個耗電優化策略,進入Doze模式後會暫停所有的Jobs,Alarm和Network活動並推遲到窗口期執行,以及其他的一些限制來節約電量。

Doze模式的進入和退出

Doze模式分為Deep Doze和Light Doze兩種模式,Doze模式是在Android6.0引入的,也就是Deep Doze模式,Light Doze是Android7.0引入的,兩者進入的條件不一樣,Deep Doze的條件會更嚴格,下面先介紹Deep Doze

Deep Doze

系統處於息屏狀態,並且30分鐘不移動的情況下,就會進入到Deep Doze模式,Deep Doze機制中有七種狀態,分別如下

//mState值,表示設備處於活動狀態 private static final int STATE_ACTIVE = 0; //mState值,表示設備處於不交互狀態,滅屏、靜止 private static final int STATE_INACTIVE = 1; //mState值,表示設備剛結束不交互狀態,等待進入IDLE狀態 private static final int STATE_IDLE_PENDING = 2; //mState值,表示設備正在感應動作 private static final int STATE_SENSING = 3; //mState值,表示設備正在定位 private static final int STATE_LOCATING = 4; //mState值,表示設備處於空閒狀態,也即Doze模式 private static final int STATE_IDLE = 5; //mState值,表示設備正處於Doze模式,緊接着退出Doze進入維護狀態 private static final int STATE_IDLE_MAINTENANCE = 6;

這七種狀態的轉換關係如下

根據上圖,他們的關係總結如下

  1. 當設備亮屏或者處於正常使用狀態時其就為ACTIVE狀態;
  1. ACTIVE狀態下不充電且滅屏設備就會切換到INACTIVE狀態;
  1. INACTIVE狀態經過30分鐘,期間檢測沒有打斷狀態的行為Doze就切換到IDLE_PENDING的狀態;
  1. 然後再經過30分鐘以及一系列的判斷,狀態切換到SENSING;
  1. 在SENSING狀態下會去檢測是否有地理位置變化,沒有的話就切到LOCATION狀態;
  1. LOCATION狀態下再經過30s的檢測時間之後就進入了Doze的核心狀態IDLE;
  1. 在IDLE模式下每隔一段時間就會進入一次IDLE_MAINTANCE,此間用來處理之前被掛起的一些任務,這個時間段為一個小時,兩個小時,四個小時,最後穩定為最長為六個小時
  1. IDLE_MAINTANCE狀態持續5分鐘之後會重新回到IDLE狀態;
  1. 在除ACTIVE以外的所有狀態中,檢測到打斷的行為如亮屏、插入充電器,位置的改變等狀態就會回到ACTIVE,重新開始下一個輪迴。

Light Doze

從上面可以看到想要進入Doze模式的條件是很苛刻,需要在手機息屏並且沒有移動的狀態下才能進入,所以Android7.0開始引入了Light Doze,處於息屏狀態,但仍處於移動狀態可進入Light Doze,LightDoze有7個狀態,分別如下:

//mLightState狀態值,表示設備處於活動狀態 private static final int LIGHT_STATE_ACTIVE = 0; //mLightState狀態值,表示設備處於不活動狀態 private static final int LIGHT_STATE_INACTIVE = 1; //mLightState狀態值,表示設備進入空閒狀態前,需要等待完成必要操作 private static final int LIGHT_STATE_PRE_IDLE = 3; //mLightState狀態值,表示設備處於空閒狀態,該狀態內將進行優化 private static final int LIGHT_STATE_IDLE = 4; //mLightState狀態值,表示設備處於空閒狀態,要進入維護狀態,先等待網絡連接 private static final int LIGHT_STATE_WAITING_FOR_NETWORK = 5; //mLightState狀態值,表示設備處於維護狀態 private static final int LIGHT_STATE_IDLE_MAINTENANCE = 6;

這個6個狀態的轉換關係如下

根據上圖,他們的轉換關係總結如下

  1. 當設備亮屏或者處於正常使用狀態時其就為ACTIVE狀態;
  1. ACTIVE狀態下不充電且滅屏設備就會切換到INACTIVE狀態;
  1. INACTIVE狀態經過3分鐘,期間檢測沒有打斷狀態的行為就切換到PRE_IDLE的狀態;
  1. PRE_IDLE狀態經過5分鐘,期間無打斷就進入到IDLE狀態
  1. 進入IDLE狀態會根據是否有網絡連接選擇進入WAITING_FOR_NETWORK還是進入MAINTENANCE窗口期,進入窗口期的時間為:5分鐘,10分鐘,最後穩定最長為15分鐘
  1. 進入WAITING_FOR_NETWORK會持續5分鐘後重新進入到IDLE狀態
  1. 進入MAINTENANCE會解除耗電策略的限制,並在1分鐘後重新進入到IDLE狀態

Doze模式的優化策略

瞭解了Doze模式的進入和退出策略,我們再來看一下在Doze模式中,會做哪些策略來優化耗電

Deep Doze

當系統處於Doze模式下,系統和白名單之外的應用將受到以下限制:

  • 無法訪問網絡
  • Wake Locks被忽略
  • AlarmManager鬧鈴會被推遲到下一個maintenance window響應

    • 使用setAndAllowWhileIdle或SetExactAndAllowWhileIdle設置鬧鈴的鬧鐘則不會受到Doze模式的影響
    • setAlarmClock設置的鬧鈴在Doze模式下仍然生效,但系統會在鬧鈴生效前退出Doze
  • 系統不執行Wi-Fi/GPS掃描;
  • 系統不允許同步適配器運行;
  • 系統不允許JobScheduler運行;

Deep Doze也提供了白名單,位於白名單中的應用可以:

  • 繼續使用網絡並保留部分wake lock
  • Job和同步仍然會被推遲
  • 常規的AlarmManager鬧鈴也不會被觸發

Light Doze

Light Doze的限制沒有Deep Doze這麼嚴格,主要有下面幾種

  • 不允許進行網絡訪問
  • 不允許同步適配器運行
  • 不允許JobScheduler運行

Deep Doze和Light Doze的總結對比如下:

Deep Doze和Light Doze都需要達到一定條件後才能進入,並且進入後會定期提供窗口期來解除限制。

他們的對比如下

| 操作 | Deep Doze | Light Doze | | ---- | ------------------------------------------------------------- | ----------------------------------- | | 觸發因素 | 屏幕關閉,無充電,靜止 | 屏幕關閉,無充電 | | 維持時間 | 不斷增長,最長為6個小時 | 不斷增長,最長為15分鐘 | | 限制 | 無法進行網絡訪問,喚醒鎖忽略,GPS/WIFI/藍牙等無法掃描,鬧鐘/SyncAdapter/JobScheduler推遲 | 無法進行網絡訪問,SyncAdapter/JobScheduler推遲 | | 退出 | 設備移動,和用户有交互,屏幕開啟 | 屏幕開啟 |

Doze模式實現原理

前面已經瞭解了Doze模式了,下面就在通過Android中的Doze機制的源碼,深入瞭解Doze的實現原理。Doze機制相關的源碼都在DeviceIdleController這個類中

DeviceIdleController.java

進入INACTIVE狀態

從ACTIVIE進入到INACTIVE的入口方法是becomeInactiveIfAppropriateLocked中,當充電狀態發生改變,屏幕息屏等條件觸發時,都會調用該方法判斷是否可進入INACTIVE狀態。

``` //deep doze進入INACTIVE後的延時時間,這裏的COMPRESS_TIME默認為false long inactiveTimeoutDefault = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L; INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_INACTIVE_TIMEOUT, !COMPRESS_TIME ? inactiveTimeoutDefault : (inactiveTimeoutDefault / 10));

LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis( KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, !COMPRESS_TIME ? 3 * 60 * 1000L : 15 * 1000L);

void becomeInactiveIfAppropriateLocked() { final boolean isScreenBlockingInactive = mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked); //判斷是否是滅屏且非充電狀態 if (!mForceIdle && (mCharging || isScreenBlockingInactive)) { return; }

if (mDeepEnabled) {
    if (mQuickDozeActivated) {
        //1. QuickDoze是Android 10新引入的低電量的情況下,快速進入Doze的機制,會縮短進入Doze的耗時
        if (mState == STATE_QUICK_DOZE_DELAY || mState == STATE_IDLE
                || mState == STATE_IDLE_MAINTENANCE) {
            return;
        }
        mState = STATE_QUICK_DOZE_DELAY;
        resetIdleManagementLocked();
        scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);
        EventLogTags.writeDeviceIdle(mState, "no activity");
    } else if (mState == STATE_ACTIVE) {
        mState = STATE_INACTIVE;
        resetIdleManagementLocked();
        long delay = mInactiveTimeout;
        if (shouldUseIdleTimeoutFactorLocked()) {
            delay = (long) (mPreIdleFactor * delay);
        }
        //2. 執行時間為mInactiveTimeout延時的任務,這裏是30分鐘
        scheduleAlarmLocked(delay, false);
        EventLogTags.writeDeviceIdle(mState, "no activity");
    }
}
if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) {
    mLightState = LIGHT_STATE_INACTIVE;
    resetLightIdleManagementLocked();
    //3. 執行時間為LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT延時的任務,這裏是3分鐘
    scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT);
    EventLogTags.writeDeviceIdleLight(mLightState, "no activity");
}

} ```

從源碼中可以看到Deep Doze,Light Doze的處理都在這裏,並且這裏還有一個Quick Doze,它是Android 10引入,能在低電量情況下快速進入Doze的機制。

我們接着看INACTIVE向下一個狀態的改變

  • Deep Doze通過scheduleAlarmLocked(delay, false)向下一個狀態轉變,在這個時間過程中,有開屏,充電等操作,都會導致狀態轉換失敗
  • Light Doze通過scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT)向下一個狀態改變,同樣在開屏和充電狀態下,都會導致進入下一個狀態失敗

從INACTIVE狀態開始,Light Doze和Deep Doze轉換的入口就不一樣了,所以下面會分開講解

Deep Doze

1. 從INACTIVE進入STATE_IDLE_PENDING

becomeInactiveIfAppropriateLocked函數中將mState設置為STATE_INACTIVE,然後調用scheduleAlarmLocked設置了一個30分鐘的定時任務,它的邏輯實現如下。

``` void scheduleAlarmLocked(long delay, boolean idleUntil) { if (mMotionSensor == null) { //如果沒有運動傳感器,則返回,因為無法判斷設備是否保持靜止 if (mMotionSensor == nullr) { return; } //設置DeepDoze的定時Alarm mNextAlarmTime = SystemClock.elapsedRealtime() + delay; if (idleUntil) { mAlarmManager.setIdleUntil(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); } else { mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNextAlarmTime, "DeviceIdleController.deep", mDeepAlarmListener, mHandler); } }

private final AlarmManager.OnAlarmListener mDeepAlarmListener = new AlarmManager.OnAlarmListener() { @Override public void onAlarm() { synchronized (DeviceIdleController.this) { ///每次Doze狀態轉換都會在該方法中進行 stepIdleStateLocked("s:alarm"); } } }; ```

Deep Doze的scheduleAlarmLocked定時任務觸發後,會回調onAlarm,執行stepIdleStateLocked函數。

void stepIdleStateLocked(String reason) { final long now = SystemClock.elapsedRealtime(); //説明1小時內有Alarm定時時間到,暫不進入IDLE狀態,30min後再進入 if ((now+mConstants.MIN_TIME_TO_ALARM) > mAlarmManager.getNextWakeFromIdleTime()) { if (mState != STATE_ACTIVE) { //將當前設備變為活動狀態,LightDoze和DeepDoze都為Active狀態 becomeActiveLocked("alarm", Process.myUid()); becomeInactiveIfAppropriateLocked(); } return; } switch (mState) { case STATE_INACTIVE: //啟動Sensor startMonitoringMotionLocked(); //設置STATE_IDLE_PENDING狀態時長的定時Alarm,30mins scheduleAlarmLocked(mConstants.IDLE_AFTER_INACTIVE_TIMEOUT, false); mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;//5mins mNextIdleDelay = mConstants.IDLE_TIMEOUT;//60mins //此時狀態變為PENDING狀態 mState = STATE_IDLE_PENDING; break; case STATE_IDLE_PENDING: //此時狀態變為SENSING狀態 mState = STATE_SENSING; //設置STATE_SENSING狀態超時時長的定時Alarm,DEBUG?1:4mins scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT); //取消通用位置更新和GPS位置更新 cancelLocatingLocked(); mNotMoving = false; mLocated = false; mLastGenericLocation = null; mLastGpsLocation = null; //開始檢測是否有移動 mAnyMotionDetector.checkForAnyMotion(); break; case STATE_SENSING: //取消用於STATE_SENSING狀態超時時長的Alarm cancelSensingTimeoutAlarmLocked(); //此時狀態變為LOCATING mState = STATE_LOCATING; //設置STATE_LOCATING狀態時長的Alarm scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false);//DEBUG?15:30 //請求通用位置 if (mLocationManager != null && mLocationManager.getProvider(LocationManager. NETWORK_PROVIDER) != null) { mLocationManager.requestLocationUpdates(mLocationRequest, mGenericLocationListener, mHandler.getLooper()); mLocating = true; } else { mHasNetworkLocation = false; } //請求GPS位置 if (mLocationManager != null && mLocationManager.getProvider(LocationManager. GPS_PROVIDER) != null) { mHasGps = true; mLocationManager.requestLocationUpdates(LocationManager. GPS_PROVIDER, 1000, 5, mGpsLocationListener, mHandler.getLooper()); mLocating = true; } else { mHasGps = false; } //如果true,則break,因為在Location的Listener中會進入下一個狀態, //否則進入下一步狀態 if (mLocating) { break; } case STATE_LOCATING: //取消DeepDoze的Alarm cancelAlarmLocked(); //取消位置更新 cancelLocatingLocked(); //Sensor停止檢測 mAnyMotionDetector.stop(); case STATE_IDLE_MAINTENANCE: //設置STATE_IDLE狀態時長的定時Alarm,到時後將退出IDLE狀態 scheduleAlarmLocked(mNextIdleDelay, true); //設置下次IDLE時間 mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR); mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT); if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) { mNextIdleDelay = mConstants.IDLE_TIMEOUT; } mState = STATE_IDLE; //進入DeepDoze的IDLE後,覆蓋LightDoze if (mLightState != LIGHT_STATE_OVERRIDE) { mLightState = LIGHT_STATE_OVERRIDE; //取消LightDoze的定時Alarm cancelLightAlarmLocked(); } //申請wakelock保持CPU喚醒 mGoingIdleWakeLock.acquire(); //handler中處理idle狀態後各個模塊的限制工作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON); break; case STATE_IDLE: mActiveIdleOpCount = 1;//表示現在有正在活動的操作 //申請wakelock鎖保持cpu喚醒 mActiveIdleWakeLock.acquire(); //設置STATE_IDLE_MAINTENANCE狀態時長的定時Alarm, //到時後將退出維護狀態 scheduleAlarmLocked(mNextIdlePendingDelay, false); mMaintenanceStartTime = SystemClock.elapsedRealtime(); mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT, (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR)); if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) { mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; } mState = STATE_IDLE_MAINTENANCE; //Handler中處理退出idle狀態進入維護狀態後取消限制的工作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); break; } }

可以看到,Deep Doze的狀態轉換都是通過scheduleAlarmLocked和stepIdleStateLocked這兩個函數進行的。 在case為STATE_INACTIVE的邏輯中,將mState設置成了STATE_IDLE_PENDING,啟動Sensor監聽,並設置了一個30分鐘的延時任務。

2. 從STATE_DLE_PENDING進入STATE_SENSING

當30分鐘無中斷,state就從PENDING進入到了SENSING狀態中。

case STATE_IDLE_PENDING: //此時狀態變為SENSING狀態 mState = STATE_SENSING; //設置STATE_SENSING狀態超時時長的定時Alarm,4分鐘 scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT); //取消通用位置更新和GPS位置更新 cancelLocatingLocked(); mNotMoving = false; mLocated = false; mLastGenericLocation = null; mLastGpsLocation = null; //開始檢測是否有運動 mAnyMotionDetector.checkForAnyMotion(); break;

在這個狀態中,會開始運動檢測,並持續4分鐘。

3. 從STATE_SENSING進入到STATE_LOCATING
4. 從STATE_LOCATING進入到STATE_IDLE
5. 從STATE_IDLE_MAINTENANCE進入到STATE_IDLE

SENSING的下一個狀態是STATE_LOCATING,STATE_LOCATING和STATE_IDLE_MAINTENANCE的下一個狀態都是STATE_IDLE,這裏一起講

case STATE_SENSING: //取消用於STATE_SENSING狀態超時時長的Alarm cancelSensingTimeoutAlarmLocked(); //此時狀態變為LOCATING mState = STATE_LOCATING; //設置STATE_LOCATING狀態時長的Alarm, scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false); //請求通用位置 if (mLocationManager != null && mLocationManager.getProvider(LocationManager. NETWORK_PROVIDER) != null) { mLocationManager.requestLocationUpdates(mLocationRequest, mGenericLocationListener, mHandler.getLooper()); mLocating = true; } else { mHasNetworkLocation = false; } //請求GPS位置 if (mLocationManager != null && mLocationManager.getProvider(LocationManager. GPS_PROVIDER) != null) { mHasGps = true; mLocationManager.requestLocationUpdates(LocationManager. GPS_PROVIDER, 1000, 5, mGpsLocationListener, mHandler.getLooper()); mLocating = true; } else { mHasGps = false; } //如果true,則break,因為在Location的Listener中會進入下一個狀態, //否則進入下一步狀態 if (mLocating) { break; } case STATE_LOCATING: //取消DeepDoze的Alarm cancelAlarmLocked(); //取消位置更新 cancelLocatingLocked(); //Sensor停止檢測 mAnyMotionDetector.stop(); case STATE_IDLE_MAINTENANCE: //設置STATE_IDLE狀態時長的定時Alarm,到時後將退出IDLE狀態 scheduleAlarmLocked(mNextIdleDelay, true); //設置下次IDLE時間 mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR); mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT); if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) { mNextIdleDelay = mConstants.IDLE_TIMEOUT; } mState = STATE_IDLE; //進入DeepDoze的IDLE後,覆蓋LightDoze if (mLightState != LIGHT_STATE_OVERRIDE) { mLightState = LIGHT_STATE_OVERRIDE; //取消LightDoze的定時Alarm cancelLightAlarmLocked(); } //申請wakelock保持CPU喚醒 mGoingIdleWakeLock.acquire(); //handler中處理idle狀態後各個模塊的限制工作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON); break;

在這個過程中檢測是否有gps以及是否有位置移動,如果有gps,則通過break跳出循環,並進行30S的位置移動檢測;沒有gps,則進入到case為STATE_IDLE_MAINTENANCE的處理中,並將state設置為STATE_IDLE。

進入到STATE_IDLE後,會申請wakelock,同時調用MSG_REPORT_IDLE_ON的handler任務來進行耗電策略的限制,這裏和light doze的idle狀態處理都是同一個入口,所以MSG_REPORT_IDLE_ON在下面light doze中在詳細將。

同時,我們可以看到,進入STATE_IDLE後,會設置一個時間為:

``` IDLE_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_TIMEOUT, !COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);

mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR); mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT); if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) { mNextIdleDelay = mConstants.IDLE_TIMEOUT; } ```

的延時任務,IDLE_FACTOR為2,mNextIdleDelay初始值為60分鐘,MAX_IDLE_TIMEOUT為6個小時,所以這個時間為1個小時,2個小時,4個小時,最後穩定為6個小時

6. 從STATE_IDLE進入到STATE_IDLE_MAINTENANCE

case STATE_IDLE: mActiveIdleOpCount = 1;//表示現在有正在活動的操作 //申請wakelock鎖保持cpu喚醒 mActiveIdleWakeLock.acquire(); //設置STATE_IDLE_MAINTENANCE狀態時長的定時Alarm, //到時後將退出維護狀態 scheduleAlarmLocked(mNextIdlePendingDelay, false); mMaintenanceStartTime = SystemClock.elapsedRealtime(); mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT, (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR)); if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) { mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; } mState = STATE_IDLE_MAINTENANCE; //Handler中處理退出idle狀態進入維護狀態後取消限制的工作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); break;

進入MAINTENANCE狀態後,會在MSG_REPORT_IDLE_OFF的handler中取消各種限制,並位置mNextIdlePendingDelay時間段

mNextIdlePendingDelay = Math.min(mConstants.MAX_IDLE_PENDING_TIMEOUT, (long)(mNextIdlePendingDelay * mConstants.IDLE_PENDING_FACTOR)); if (mNextIdlePendingDelay < mConstants.IDLE_PENDING_TIMEOUT) { mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT; }

IDLE_PENDING_TIMEOUT為5分鐘

Light Doze

1. 從INACTIVE進入LIGHT_STATE_PRE_IDLE

scheduleLightAlarmLocked到達時間後,會觸發下面的回調

``` void scheduleLightAlarmLocked(long delay) { mNextLightAlarmTime = SystemClock.elapsedRealtime() + delay; //到達時間後,回調mLightAlarmListener.onAlarm() mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, mNextLightAlarmTime, "DeviceIdleController.light", mLightAlarmListener, mHandler); }

private final AlarmManager.OnAlarmListener mLightAlarmListener = new AlarmManager.OnAlarmListener() { @Override public void onAlarm() { synchronized (DeviceIdleController.this) { //每次LightDoze的狀態改變,都會調用該方法進行處理 stepLightIdleStateLocked("s:alarm"); } } }; ```

Light Doze的狀態改變也都是在stepLightIdleStateLocked函數中處理

void stepLightIdleStateLocked(String reason) { //如果mLigthSate為LIGHT_STATE_OVERRIDE,説明DeepDoze處於Idle狀態,由 // DeepDoze將LightDoze覆蓋了,因此不需要進行LightDoze了 if (mLightState == LIGHT_STATE_OVERRIDE) { return; } switch (mLightState) { case LIGHT_STATE_INACTIVE: //當前最小預算時間 mCurIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;//1min //表示LightDoze 進入空閒(Idle)狀態的時間 mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;//5mins //LightDoze進入維護狀態(maintenance)的開始時間 mMaintenanceStartTime = 0; if (!isOpsInactiveLocked()) { //將狀態置為LIGHT_STATE_PRE_IDLE狀態 mLightState = LIGHT_STATE_PRE_IDLE; //設置一個3分鐘的定時器 scheduleLightAlarmLocked(mConstants.LIGHT_PRE_ IDLE_TIMEOUT); break; } case LIGHT_STATE_PRE_IDLE: case LIGHT_STATE_IDLE_MAINTENANCE: if (mMaintenanceStartTime != 0) { //維護狀態的時長 long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime; if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { mCurIdleBudget += (mConstants.LIGHT_IDLE_MAINTENANCE _MIN_BUDGET-duration); } else { mCurIdleBudget -= (duration-mConstants.LIGHT_IDLE_ MAINTENANCE_MIN_BUDGET); } } mMaintenanceStartTime = 0;//重置維護開始時間 //設置一個定時器,到達時間後用來處理LightDoze處於IDLE狀態的操作 scheduleLightAlarmLocked(mNextLightIdleDelay); //計算下次進入Idle狀態的 mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT, (long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)); if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) { mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; } //將LightDoze模式置為IDLE狀態,開始進行一些限制 mLightState = LIGHT_STATE_IDLE; addEvent(EVENT_LIGHT_IDLE); //申請一個wakelock鎖,保持CPU喚醒 mGoingIdleWakeLock.acquire(); //處理LightDoze進入Idle狀態後的操作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT); break; case LIGHT_STATE_IDLE: case LIGHT_STATE_WAITING_FOR_NETWORK: if (mNetworkConnected || mLightState == LIGHT_STATE_WAITING_FOR_NETWORK) { //如果網絡有鏈接或者當前LightDoze模式為等待網絡狀態,則進行維護, // 並將LightDoze模式退出IDLE狀態,進入維護狀態 mActiveIdleOpCount = 1; mActiveIdleWakeLock.acquire(); mMaintenanceStartTime = SystemClock.elapsedRealtime(); // 保證10<=mCurIdleBudget<=30mins ,mCurIdleBudget是維護狀態的時間 if (mCurIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { mCurIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET; } else if (mCurIdleBudget > mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET) { mCurIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MAX_BUDGET; } //設置一個定時器,到達時間後用來處理LightDoze處於維護狀態的操作 scheduleLightAlarmLocked(mCurIdleBudget); mLightState = LIGHT_STATE_IDLE_MAINTENANCE;//進入維護狀態 addEvent(EVENT_LIGHT_MAINTENANCE); //處理LightDoze進入Maintenance狀態後的操作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF); } else { //將LightDoze模式置為LIGHT_STATE_WAITING_FOR_NETWORK, //在進入維護狀態前需要獲取網絡 //設置一個定時器,到達時間後用來處理LightDoze處於 //WAITING_FOR_NETWORK狀態的操作 scheduleLightAlarmLocked(mNextLightIdleDelay);//600000,5mins mLightState = LIGHT_STATE_WAITING_FOR_NETWORK; EventLogTags.writeDeviceIdleLight(mLightState, reason); } break; } }

從代碼中可以看到,case為LIGHT_STATE_INACTIVE的處理邏輯中,做了這幾件事

  1. 將當前狀態設置為LIGHT_STATE_PRE_IDLE,
  1. 併發送一個3分鐘的鬧鐘,準備進入下一個狀態。

後續狀態也全部是通過scheduleLightAlarmLocked來設置定時任務,然後在stepLightIdleStateLocked函數中處理狀態的轉換和對應狀態的邏輯

2. 從LIGHT_STATE_PRE_IDLE進入LIGHT_STATE_IDLE
3. 從LIGHT_STATE_IDLE_MAINTENANCE進入LIGHT_STATE_IDLE

LIGHT_STATE_PRE_IDLE和LIGHT_STATE_IDLE_MAINTENANCE的下一個狀態都是LIGHT_STATE_IDLE,所以他們的處理也在同一個入口

``` LIGHT_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_IDLE_TIMEOUT, !COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);

LIGHT_MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_MAX_IDLE_TIMEOUT, !COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L);

void stepLightIdleStateLocked(String reason) { //如果mLigthSate為LIGHT_STATE_OVERRIDE,説明DeepDoze處於Idle狀態,由 // DeepDoze將LightDoze覆蓋了,因此不需要進行LightDoze了 if (mLightState == LIGHT_STATE_OVERRIDE) { return; } switch (mLightState) { …… case LIGHT_STATE_PRE_IDLE: case LIGHT_STATE_IDLE_MAINTENANCE: if (mMaintenanceStartTime != 0) { //維護狀態的時長 long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime; if (duration < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) { mCurIdleBudget += (mConstants.LIGHT_IDLE_MAINTENANCE MIN_BUDGET-duration); } else { mCurIdleBudget -= (duration-mConstants.LIGHT_IDLE MAINTENANCE_MIN_BUDGET); } } mMaintenanceStartTime = 0;//重置維護開始時間 //設置一個定時器,到達時間後用來處理LightDoze處於IDLE狀態的操作 scheduleLightAlarmLocked(mNextLightIdleDelay); //計算下次進入Idle狀態的 mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT, (long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)); if (mNextLightIdleDelay < mConstants.LIGHT_IDLE_TIMEOUT) { mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT; } //將LightDoze模式置為IDLE狀態,開始進行一些限制 mLightState = LIGHT_STATE_IDLE; addEvent(EVENT_LIGHT_IDLE); //申請一個wakelock鎖,保持CPU喚醒 mGoingIdleWakeLock.acquire(); //處理LightDoze進入Idle狀態後的操作 mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT); break; …… } } ```

這裏會將state設置成LIGHT_STATE_IDLE,並設置一個mNextLightIdleDelay的計時任務,以便進入下一個狀態,mNextLightIdleDelay的初始值是5分鐘。

這裏我們可以看到LIGHT_STATE_PRE_IDLE和LIGHT_STATE_IDLE_MAINTENANCE是同一個case處理邏輯,這兩個狀態的下一個狀態都是LIGHT_STATE_IDLE。

如果上一個狀態是LIGHT_STATE_IDLE_MAINTENANCE,則

mNextLightIdleDelay = Math.min(mConstants.LIGHT_MAX_IDLE_TIMEOUT,(long)(mNextLightIdleDelay * mConstants.LIGHT_IDLE_FACTOR)),LIGHT_MAX_IDLE_TIMEOUT為15分鐘,LIGHT_IDLE_FACTOR為2

所以light doze的IDLE時間為5分鐘,10分鐘,最後穩定為15分鐘。

當state的狀態轉換成IDLE後,這裏會申請wakelock鎖,讓cpu喚醒,然後通過MSG_REPORT_IDLE_ON_LIGHT的Handler任務進行邏輯處理,然後再釋放wakelock鎖,讓cpu休眠。

剩下的幾種狀態函數轉換都在上面的函數中有註釋,就不詳細講解了。

Doze限制邏輯

我們接着看MSG_REPORT_IDLE_ON_LIGHT中做了哪些事情

case MSG_REPORT_IDLE_ON: case MSG_REPORT_IDLE_ON_LIGHT:: { final boolean deepChanged; final boolean lightChanged; if (msg.what == MSG_REPORT_IDLE_ON) { //通知PMS設置Deep Doze模式處於IDLE狀態 deepChanged = mLocalPowerManager.setDeviceIdleMode(true); //通知PMS為Light Doze模式不處於IDLE狀態 lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); } else { //通知PMS設置Deep Doze模式不處於IDLE狀態 deepChanged = mLocalPowerManager.setDeviceIdleMode(false); //通知PMS為Light Doze模式處於IDLE狀態 lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true); } try { //通知NetworkPolicyManager進入IDLE狀態,進行網絡訪問的限制 mNetworkPolicyManager.setDeviceIdleMode(true); //通知BatteryStatsService統計Light Doze或者Deep Doze進入IDLE狀態 mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON ? BatteryStats.DEVICE_IDLE_MODE_DEEP : BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid()); } catch (RemoteException e) { } //發送DeepDoze模式改變的廣播 if (deepChanged) { getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL); } //發送Light模式改變的廣播 if (lightChanged) { getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL); } //釋放wakelock mGoingIdleWakeLock.release(); } break;

可以看到,Deep Doze和Light Doze在進入IDLE狀態後的邏輯處理在同一個地方。這裏根據模式的不同,通知PowerServiceManager,NetworkPolicyManager,BatteryStats等進行不同的優化策略。這裏主要做的事情有這幾件

  1. 調用mLocalPowerManager.setDeviceIdleMode設置是否是Deep Doze的Idle狀態,如果為Idle,這一步會將應用設置成忽略WakeLock的狀態
  1. 調用mLocalPowerManager.setLightDeviceIdleMode設置是否是Light Doze的Idle狀態
  1. 調用mNetworkPolicyManager.setDeviceIdleMode(true),通過添加防火牆規則,來進行網絡訪問限制
  1. 調用BatteryStats.noteDeviceIdleMode進行狀態變更及耗時統計
  1. 調用sendBroadcastAsUser發送廣播,進入Deep Doze或者Light Doze的Idle狀態
  1. 釋放WakeLock

Doze限制邏輯取消

Light Doze和Deep Doze進入MAINTENCANCE後都會取消各種限制,取消的邏輯在MSG_REPORT_IDLE_OFF的handler任務中處理。

case MSG_REPORT_IDLE_OFF: { // mActiveIdleWakeLock is held at this point EventLogTags.writeDeviceIdleOffStart("unknown"); final boolean deepChanged = mLocalPowerManager.setDeviceIdleMode(false); final boolean lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false); try { mNetworkPolicyManager.setDeviceIdleMode(false); mBatteryStats.noteDeviceIdleMode(BatteryStats.DEVICE_IDLE_MODE_OFF, null, Process.myUid()); } catch (RemoteException e) { } if (deepChanged) { incActiveIdleOps(); getContext().sendOrderedBroadcastAsUser(mIdleIntent, UserHandle.ALL, null, mIdleStartedDoneReceiver, null, 0, null, null); } if (lightChanged) { incActiveIdleOps(); getContext().sendOrderedBroadcastAsUser(mLightIdleIntent, UserHandle.ALL, null, mIdleStartedDoneReceiver, null, 0, null, null); } decActiveIdleOps(); } break;

Standby模式

Doze模式是針對整個系統的耗電優化模式,而Standby模式,即應用羣組待機模式是針對單個應用的耗電優化模式,它是Android7.0引入的,當應用處於閒置狀態時,系統會根據應用應用最近使用的時間和頻率,設置成對應的羣組,不同的羣組下,jobs,alarm和network的使用限制程度不一樣。

Standby模式的進入和退出

當用户有一段時間未觸摸應用時,系統便會判斷進入Standby模式,以下條件下不適用或者會退出Standby模式:

  1. 用户主動啟動該App;
  1. 該App當前有一個前台進程(或包含一個活動的前台服務,或被另一個activity或前台service使用);
  1. 在鎖定屏幕或通知欄中看到的通知
  1. 系統應用
  1. 充電狀態

Standby模式優化策略

應用在進入Standby後,會根據該應用所屬的狀態,對Jobs,Alarms和Network進行相應的限制,應用的狀態分為五個等級

  1. Activie:如果用户當前正在使用應用,應用將被歸到“atcitive”狀態中
  1. WORKING_SER:如果應用經常運行(12至24小時內使用過),但當前未處於活躍狀態,它將被歸到“工作集”羣組中。 例如,用户在大部分時間都啟動的某個社交媒體應用可能就屬於“工作集”羣組。 如果應用被間接使用,它們也會被升級到“工作集”羣組中 。
  1. FREQUENT:如果應用會定期使用,但不是每天都必須使用 (亮屏時間差超過1小時、使用時間差超過24小時) ,它將被歸到“常用”羣組中。 例如,用户在健身房運行的某個鍛鍊跟蹤應用可能就屬於“常用”羣組。
  1. RARE:如果應用不經常使用 (亮屏時間差超過2小時、使用時間差超過48小時) ,那麼它屬於“極少使用”羣組。 例如,用户僅在入住酒店期間運行的酒店應用就可能屬於“極少使用”羣組。如果應用處於“極少使用”羣組,系統將對它運行作業、觸發警報和接收高優先級 FCM 消息的能力施加嚴格限制。系統還會限制應用連接到網絡的能力。 
  1. NEVER:安裝但是從未運行過的應用會被歸到“從未使用”羣組中。 系統會對這些應用施加極強的限制。

下面是對這個五個等級的應用的限制情況

http://developer.android.com/topic/performance/power/power-details

Standby模式實現原理

Standby模式的邏輯實現在AppStandbyController對象中,該對象提供了reportEvent,來讓外部進行app行為變化的通知,如ams,NotificationManagerService等都會調用reportEvent來告知app有行為變化並更新Bucket

AppStandbyController.java

更新Bucket

``` void reportEvent(UsageEvents.Event event, long elapsedRealtime, int userId) { if (!mAppIdleEnabled) return; synchronized (mAppIdleLock) { // TODO: Ideally this should call isAppIdleFiltered() to avoid calling back // about apps that are on some kind of whitelist anyway. final boolean previouslyIdle = mAppIdleHistory.isIdle( event.mPackage, userId, elapsedRealtime); // Inform listeners if necessary if ((event.mEventType == UsageEvents.Event.ACTIVITY_RESUMED || event.mEventType == UsageEvents.Event.ACTIVITY_PAUSED || event.mEventType == UsageEvents.Event.SYSTEM_INTERACTION || event.mEventType == UsageEvents.Event.USER_INTERACTION || event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN || event.mEventType == UsageEvents.Event.SLICE_PINNED || event.mEventType == UsageEvents.Event.SLICE_PINNED_PRIV || event.mEventType == UsageEvents.Event.FOREGROUND_SERVICE_START)) {

        final AppUsageHistory appHistory = mAppIdleHistory.getAppUsageHistory(
                event.mPackage, userId, elapsedRealtime);
        final int prevBucket = appHistory.currentBucket;
        final int prevBucketReason = appHistory.bucketingReason;
        final long nextCheckTime;
        final int subReason = usageEventToSubReason(event.mEventType);
        final int reason = REASON_MAIN_USAGE | subReason;

        //根據使用行為更新bucket
        if (event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN
                || event.mEventType == UsageEvents.Event.SLICE_PINNED) {
            mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                    STANDBY_BUCKET_WORKING_SET, subReason,
                    0, elapsedRealtime + mNotificationSeenTimeoutMillis);
            nextCheckTime = mNotificationSeenTimeoutMillis;
        } else if (event.mEventType == UsageEvents.Event.SYSTEM_INTERACTION) {
            mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                    STANDBY_BUCKET_ACTIVE, subReason,
                    0, elapsedRealtime + mSystemInteractionTimeoutMillis);
            nextCheckTime = mSystemInteractionTimeoutMillis;
        } else if (event.mEventType == UsageEvents.Event.FOREGROUND_SERVICE_START) {
            // Only elevate bucket if this is the first usage of the app
            if (prevBucket != STANDBY_BUCKET_NEVER) return;
            mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                    STANDBY_BUCKET_ACTIVE, subReason,
                    0, elapsedRealtime + mInitialForegroundServiceStartTimeoutMillis);
            nextCheckTime = mInitialForegroundServiceStartTimeoutMillis;
        } else {
            mAppIdleHistory.reportUsage(appHistory, event.mPackage,
                    STANDBY_BUCKET_ACTIVE, subReason,
                    elapsedRealtime, elapsedRealtime + mStrongUsageTimeoutMillis);
            nextCheckTime = mStrongUsageTimeoutMillis;
        }
        //設置延時消息,根據使用時間更新bucket
        mHandler.sendMessageDelayed(mHandler.obtainMessage
                (MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, event.mPackage),
                nextCheckTime);
        final boolean userStartedInteracting =
                appHistory.currentBucket == STANDBY_BUCKET_ACTIVE &&
                prevBucket != appHistory.currentBucket &&
                (prevBucketReason & REASON_MAIN_MASK) != REASON_MAIN_USAGE;
        maybeInformListeners(event.mPackage, userId, elapsedRealtime,
                appHistory.currentBucket, reason, userStartedInteracting);

        if (previouslyIdle) {
            notifyBatteryStats(event.mPackage, userId, false);
        }
    }
}

} ```

reportEvent會根據mEventType進行一次Bucket更新,並根據mEventType設置一次延時任務,這個延時任務中會再次根據應用的使用行為再次更新Bucket。其中Notification類型的消息的延遲時間為12小時,SYSTEM_INTERACTION為10分鐘,其他的mStrongUsageTimeoutMillis為1小時

MSG_CHECK_PACKAGE_IDLE_STATE的handler消息主要根據使用時長更新Bucket

``` static final int[] THRESHOLD_BUCKETS = { STANDBY_BUCKET_ACTIVE, STANDBY_BUCKET_WORKING_SET, STANDBY_BUCKET_FREQUENT, STANDBY_BUCKET_RARE };

static final long[] SCREEN_TIME_THRESHOLDS = { 0, 0, COMPRESS_TIME ? 120 * 1000 : 1 * ONE_HOUR, COMPRESS_TIME ? 240 * 1000 : 2 * ONE_HOUR };

static final long[] ELAPSED_TIME_THRESHOLDS = { 0, COMPRESS_TIME ? 1 * ONE_MINUTE : 12 * ONE_HOUR, COMPRESS_TIME ? 4 * ONE_MINUTE : 24 * ONE_HOUR, COMPRESS_TIME ? 16 * ONE_MINUTE : 48 * ONE_HOUR };

long[] mAppStandbyScreenThresholds = SCREEN_TIME_THRESHOLDS; long[] mAppStandbyElapsedThresholds = ELAPSED_TIME_THRESHOLDS;

@StandbyBuckets int getBucketForLocked(String packageName, int userId, long elapsedRealtime) { int bucketIndex = mAppIdleHistory.getThresholdIndex(packageName, userId, elapsedRealtime, mAppStandbyScreenThresholds, mAppStandbyElapsedThresholds); return THRESHOLD_BUCKETS[bucketIndex]; } ```

AppIdleHistory.java

``` int getThresholdIndex(String packageName, int userId, long elapsedRealtime, long[] screenTimeThresholds, long[] elapsedTimeThresholds) { ArrayMap userHistory = getUserHistory(userId); AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName, elapsedRealtime, false); if (appUsageHistory == null) return screenTimeThresholds.length - 1;

//app最後一次亮屏使用到現在,已經有多久的亮屏時間
long screenOnDelta = getScreenOnTime(elapsedRealtime) - appUsageHistory.lastUsedScreenTime;
//app最後一次使用到現在的時間點
long elapsedDelta = getElapsedTime(elapsedRealtime) - appUsageHistory.lastUsedElapsedTime;
for (int i = screenTimeThresholds.length - 1; i >= 0; i--) {
    if (screenOnDelta >= screenTimeThresholds[i]
        && elapsedDelta >= elapsedTimeThresholds[i]) {
        return i;
    }
}
return 0;

} ```

App耗電分析

Battery Historian

Android官方提供了Battery Historian來進行電量使用的分析,Battery Historian 圖表會顯示一段時間內與電源相關的事件。

從上面的圖也可以看到,進入到Doze後,BLE scanning,GPS等就無行為了,並且cpu,wakelock等活動的頻率也變低了

我們還能通過Battery Historian獲取應用的

  • 在設備上的估計耗電量。
  • 網絡信息。
  • 喚醒鎖定次數。
  • 服務
  • 進程信息

官方文檔已經講的非常詳細,就不在這兒細説了

http://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn

Slardar

Slardar電量相關的統計指標項包括

  • app處於前台時,提供電流作為耗電指標
  • 通過採集app的cpu、流量和gps等的使用,來計算出一個加權和作為耗電指標
  • 電池温度,作為衡量耗電的輔助參考

歸因項有

  • 高CPU可以通過cpu菜單查看高耗CPU的堆棧
  • gps(location),alarm和wakelock使用在超過指定持有時間和頻次後,會上報當時的採集堆棧

雖然Slardar有上報很多功耗相關指標,但是目前還只能作為整體功耗的參考,並且很多指標波動起伏大,沒法對更細化的治理提供幫助

飛書耗電治理

治理目標

  1. 消除主流手機的高功耗提醒
  1. 建立健全的功耗監控及防劣化體系

治理方案

在前面我們已經知道耗電=模塊功率✖️模塊耗時,所以治理本質就是在不影響性能和功能的情況下,減少飛書中所使用到的模塊的耗時,並且我們瞭解了系統進行耗電優化的策略,在飛書的耗電治理中,也可以同樣的參考對應的策略。

治理方案主要分為監控的完善和耗電的治理

功耗治理

為了能體系化的進行功耗治理,這裏分為了針對耗電模塊進行治理和針對狀態進行執行兩大類

分模塊治理

模塊的耗電治理主要體現在下面幾個方面

  • 1.CPU
    • 死循環函數,高頻函數,高耗時函數,無效函數等不必要的cpu消耗或消耗較多的函數治理
    • cpu使用率較高的場景及業務治理
  • 2.GPU和Display
    • 過度繪製,過多的動畫,不可見區域的動畫等浪費GPU的場景治理
    • 主動降低屏幕亮度,使用深色UI等方案降低屏幕電量消耗
  • 3.網絡
    • 不影響業務和性能前提下,降低網絡訪問頻率
    • Doze狀態時減少無效的網絡請求
  • 4.GPS
    • 對使用GPS的場景,如小程序等,合理的降低精度,減少請求頻率
  • 5.Audio、Camera、Video等項

除了分模塊治理,還針對狀態進行治理,主要狀態有這幾種

分狀態治理

1.前台狀態
  • 渲染場景優化
  • 音視頻等場景優化
  • ……
2.後台狀態
  • task任務降頻或者丟棄
  • 網絡訪問降頻,適配Doze模式
  • 減少cpu消耗較多的函數執行
  • 減少gps等高功耗場景

完善功耗分析和監控體系

為了能更好的進行治理,完善的功耗分析和監控體系是不可避免的,不然就會出現無的放矢的狀態。在這一塊主要建設的點有

  • 1. 完善的CPU消耗監控

    • 前後台高cpu消耗場景監控,高cpu消耗線程監控(slardar已有)
    • 高頻task,高耗時task,後台task監控(已有)
    • 消耗較高,耗時較高的函數監控
  • 2. GPU和Display消耗監控

    • 動畫場景,過度繪製檢測,View層級檢測,屏幕電量消耗監控等
  • 3. 網絡

    • Rust,OkHttp及其他網絡請求場景,頻率,消耗監控
    • 後台網絡訪問監控
  • 4. GPS

    • GPS使用場景,時長,電量消耗監控
  • 5. Audio、Camera、Video

    • 使用場景,時長,電量消耗監控
  • 6. 整體和場景的電量消耗

    • 飛書整體的電量消耗和不同場景的電量消耗,用來度量版本功耗的質量