飞书最近在进行耗电治理的专项优化,本篇文章将分析 Android 系统的耗电原理,分享飞书的耗电治理规划。
我们先了解一下 Android 系统是如何进行耗电的统计的,最精确的方式当然是使用电流仪来进行统计,但是正常状态下手机硬件不支持,所以系统统计耗电时,使用的基本是模块功率 × 模块耗时这个公式来进行的,但不同的模块还是会有一些差别。这种统计方式没法做到非常的精确,但是也基本能反应出各应用电量的消耗大小。
我们先来看看模块功率,每个模块的耗电功率都是不一样的,以计算方式来分,又分为下面三类:
每个模块的功耗大小位于 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文件中。
我们来详细看看下面几个模块的是如何进行统计的:
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);
}
}
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);
}
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,通话等等。
这里以 CameraPowerCalculator 这个简单的模块看看它是如何统计电量的:
可以看到,里面只是简单的用了 totalTime * mCameraPowerOnAvg,mCameraPowerOnAvg 则是从 power_profile.xml 读取出来,其他教负责的如 CPU 模块的计算,感兴趣的可以自己看看,就不在这里说了。
杂项电量用来统计一些没有特定 UID 的耗电,如蓝牙,屏幕等等,计算方式也是类似的。
Doze 模式也被称为低电耗模式,是针对整个系统进行一个耗电优化策略,进入 Doze 模式后会暂停所有的 Jobs,Alarm 和 Network 活动并推迟到窗口期执行,以及其他的一些限制来节约电量。
Doze 模式分为 Deep Doze 和 Light Doze 两种模式,Doze 模式是在 Android6.0 引入的,也就是 Deep Doze 模式,Light Doze 是 Android7.0 引入的,两者进入的条件不一样,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;
这七种状态的转换关系如下:
根据上图,他们的关系总结如下:
从上面可以看到想要进入 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 个状态的转换关系如下:
根据上图,他们的转换关系总结如下:
了解了 Doze 模式的进入和退出策略,我们再来看一下在 Doze 模式中,会做哪些策略来优化耗电。
当系统处于 Doze 模式下,系统和白名单之外的应用将受到以下限制:
无法访问网络
Wake Locks 被忽略
AlarmManager 闹铃会被推迟到下一个 maintenance window 响应
使用 setAndAllowWhileIdle 或 SetExactAndAllowWhileIdle 设置闹铃的闹钟则不会受到 Doze 模式的影响
setAlarmClock 设置的闹铃在 Doze 模式下仍然生效,但系统会在闹铃生效前退出 Doze
系统不执行 Wi-Fi/GPS 扫描;
系统不允许同步适配器运行;
系统不允许 JobScheduler 运行;
Deep Doze 也提供了白名单,位于白名单中的应用可以:
Light Doze 的限制没有 Deep Doze 这么严格,主要有下面几种:
Deep Doze 和 Light Doze 的总结对比如下:
Deep Doze 和 Light Doze 都需要达到一定条件后才能进入,并且进入后会定期提供窗口期来解除限制。
它们的对比如下:
前面已经了解了 Doze 模式了,下面就在通过 Android 中的 Doze 机制的源码,深入了解 Doze 的实现原理。Doze 机制相关的源码都在 DeviceIdleController 这个类中。
从 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 向下一个状态的改变:
从 INACTIVE 状态开始,Light Doze 和 Deep Doze 转换的入口就不一样了,所以下面会分开讲解。
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 分钟的延时任务。
当 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 分钟。
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 个小时。
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 分钟。
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 的处理逻辑中,做了这几件事:
后续状态也全部是通过scheduleLightAlarmLocked来设置定时任务,然后在stepLightIdleStateLocked函数中处理状态的转换和对应状态的逻辑。
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 休眠。
剩下的几种状态函数转换都在上面的函数中有注释,就不详细讲解了。
我们接着看 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 等进行不同的优化策略。这里主要做的事情有这几件:
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;
Doze 模式是针对整个系统的耗电优化模式,而 Standby 模式,即应用群组待机模式是针对单个应用的耗电优化模式,它是 Android7.0 引入的,当应用处于闲置状态时,系统会根据应用应用最近使用的时间和频率,设置成对应的群组,不同的群组下,jobs,alarm 和 network 的使用限制程度不一样。
当用户有一段时间未触摸应用时,系统便会判断进入 Standby 模式,以下条件下不适用或者会退出 Standby 模式:
应用在进入 Standby 后,会根据该应用所属的状态,对 Jobs,Alarms 和 Network 进行相应的限制,应用的状态分为五个等级:
下面是对这个五个等级的应用的限制情况:
https://developer.android.com/topic/performance/power/power-details
Standby 模式的逻辑实现在 AppStandbyController 对象中,该对象提供了 reportEvent,来让外部进行 app 行为变化的通知,如 ams,NotificationManagerService 等都会调用 reportEvent 来告知 app 有行为变化并更新 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;
}
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 有上报很多功耗相关指标,但是目前还只能作为整体功耗的参考,并且很多指标波动起伏大,没法对更细化的治理提供帮助。
在前面我们已经知道耗电=模块功率 × 模块耗时,所以治理本质就是在不影响性能和功能的情况下,减少飞书中所使用到的模块的耗时,并且我们了解了系统进行耗电优化的策略,在飞书的耗电治理中,也可以同样的参考对应的策略。
治理方案主要分为监控的完善和耗电的治理。
为了能体系化地进行功耗治理,这里分为了针对耗电模块进行治理和针对状态进行执行两大类。
模块的耗电治理主要体现在下面几个方面:
1.CPU
2.GPU 和 Display
3.网络
4.GPS
5.Audio、Camera、Video 等项
除了分模块治理,还针对状态进行治理,主要状态有这几种:
1.前台状态
2.后台状态
为了能更好地进行治理,完善的功耗分析和监控体系是不可避免的,不然就会出现无的放矢的状态。在这一块主要建设的点有:
1. 完善的 CPU 消耗监控
2. GPU 和 Display 消耗监控
3. 网络
4. GPS
5. Audio、Camera、Video
6. 整体和场景的电量消耗
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8