扣丁書屋

分析 Android 耗電原理后,飛書是這樣做耗電治理的

飛書最近在進行耗電治理的專項優化,本篇文章將分析 Android 系統的耗電原理,分享飛書的耗電治理規劃。

Android 耗電統計原理

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

模塊功率

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

  1. 第一類是 Camera、FlashLight、MediaPlayer 等一般傳感器或設備的模塊。其工作功率基本和額定功率保持一致,所以模塊電量的計算只需要統計模塊的使用時長再乘以額定功率即可。
  2. 第二類是 Wifi、Mobile、BlueTooth 這類數據模塊。其工作功率可以分為不同的檔位,比如,當手機的 Wifi 信號比較弱的時候,Wifi 模塊就必須工作在比較高的功率檔位以維持數據鏈路,所以這類模塊的電量計算有點類似于我們日常的電費計算,需要 “階梯計費”。
  3. 第三類是屏幕,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 如下:

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

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

模塊耗時

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

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

  • 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 這個簡單的模塊看看它是如何統計電量的:

可以看到,里面只是簡單的用了 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 狀態;
  2. ACTIVE 狀態下不充電且滅屏設備就會切換到 INACTIVE 狀態;
  3. INACTIVE 狀態經過 30 分鐘,期間檢測沒有打斷狀態的行為 Doze 就切換到 IDLE_PENDING 的狀態;
  4. 然后再經過 30 分鐘以及一系列的判斷,狀態切換到 SENSING;
  5. 在 SENSING 狀態下會去檢測是否有地理位置變化,沒有的話就切到 LOCATION 狀態;
  6. LOCATION 狀態下再經過 30s 的檢測時間之后就進入了 Doze 的核心狀態 IDLE;
  7. 在 IDLE 模式下每隔一段時間就會進入一次 IDLE_MAINTANCE,此間用來處理之前被掛起的一些任務,這個時間段為一個小時,兩個小時,四個小時,最后穩定為最長為六個小時
  8. IDLE_MAINTANCE 狀態持續 5 分鐘之后會重新回到 IDLE 狀態;
  9. 在除 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 狀態;
  2. ACTIVE 狀態下不充電且滅屏設備就會切換到 INACTIVE 狀態;
  3. INACTIVE 狀態經過 3 分鐘,期間檢測沒有打斷狀態的行為就切換到 PRE_IDLE 的狀態;
  4. PRE_IDLE 狀態經過 5 分鐘,期間無打斷就進入到 IDLE 狀態
  5. 進入 IDLE 狀態會根據是否有網絡連接選擇進入 WAITING_FOR_NETWORK 還是進入 MAINTENANCE 窗口期,進入窗口期的時間為:5 分鐘,10 分鐘,最后穩定最長為 15 分鐘
  6. 進入 WAITING_FOR_NETWORK 會持續 5 分鐘后重新進入到 IDLE 狀態
  7. 進入 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 都需要達到一定條件后才能進入,并且進入后會定期提供窗口期來解除限制。

它們的對比如下:

Doze 模式實現原理

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

進入 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 的狀態轉換都是通過scheduleAlarmLockedstepIdleStateLocked這兩個函數進行的。在 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;
  2. 并發送一個 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 的狀態
  2. 調用 mLocalPowerManager.setLightDeviceIdleMode 設置是否是 Light Doze 的 Idle 狀態
  3. 調用 mNetworkPolicyManager.setDeviceIdleMode(true),通過添加防火墻規則,來進行網絡訪問限制
  4. 調用 BatteryStats.noteDeviceIdleMode 進行狀態變更及耗時統計
  5. 調用 sendBroadcastAsUser 發送廣播,進入 Deep Doze 或者 Light Doze 的 Idle 狀態
  6. 釋放 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;
  2. 該 App 當前有一個前臺進程(或包含一個活動的前臺服務,或被另一個 activity 或前臺 service 使用);
  3. 在鎖定屏幕或通知欄中看到的通知;
  4. 系統應用;
  5. 充電狀態;

Standby 模式優化策略

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

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

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

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

Standby 模式實現原理

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

更新 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<String, AppUsageHistory> 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 獲取應用的:

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

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

https://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. 消除主流手機的高功耗提醒
  2. 建立健全的功耗監控及防劣化體系

治理方案

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

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

功耗治理

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

分模塊治理

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

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. 整體和場景的電量消耗

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

https://mp.weixin.qq.com/s/uKFNjvW6_rZXIyIzo6fHYw

最多閱讀

簡化Android的UI開發 2年以前  |  515088次閱讀
Android 深色模式適配原理分析 1年以前  |  26373次閱讀
Android 樣式系統 | 主題背景覆蓋 1年以前  |  7929次閱讀
Android Studio 生成so文件 及調用 1年以前  |  5542次閱讀
30分鐘搭建一個android的私有Maven倉庫 3年以前  |  4725次閱讀
Android設計與開發工作流 2年以前  |  4391次閱讀
Google Enjarify:可代替dex2jar的dex反編譯 3年以前  |  4374次閱讀
Android多渠道打包工具:apptools 3年以前  |  4012次閱讀
移動端常見崩潰指標 1年以前  |  3933次閱讀
Google Java編程風格規范(中文版) 3年以前  |  3922次閱讀
Android-模塊化-面向接口編程 1年以前  |  3835次閱讀
Android內存異常機制(用戶空間)_NE 1年以前  |  3792次閱讀
Android UI基本技術點 3年以前  |  3777次閱讀
Android死鎖初探 1年以前  |  3720次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>