Doze模式是自Android 6.0开始引入的两项省电功能的其中之一,还有一个就是appstandby,通过管理应用在设备未连接至电源时的行为方式,帮助用户延长电池寿命。当用户长时间未使用设备时,低电耗模式会延迟应用的后台 CPU 和网络活动,从而降低耗电量。应用待机模式会延迟用户近期未与之交互的应用的后台网络活动。
当设备处于低电耗模式时,应用对某些高耗电量资源的访问会延迟到维护期。如果用户未插接设备的电源,在屏幕关闭的情况下,让设备在一段时间内保持不活动状态,那么设备就会进入低电耗模式。在低电耗模式下,系统会尝试通过限制应用访问占用大量网络和 CPU 资源的服务来节省电量。它还会阻止应用访问网络,并延迟其作业、同步和标准闹钟。系统会定期退出低电耗模式一小段时间,让应用完成其延迟的活动。在此维护期内,系统会运行所有待处理的同步、作业和闹钟,并允许应用访问网络。
在每个维护期结束时,系统会再次进入低电耗模式,暂停网络访问并推迟作业、同步和闹钟。随着时间的推移,系统安排维护期的次数越来越少,这有助于在设备未连接至充电器的情况下长期处于不活动状态时降低耗电量。
一旦用户通过移动设备、打开屏幕或连接至充电器唤醒设备,系统就会立即退出低电耗模式,并且所有应用都会恢复正常活动。
在低电耗模式下,您的应用会受到一下限制:
- 暂停访问网络
- 系统忽略唤醒锁定
- 标准 Ala'r'm'Manager闹钟(包括
setExact()
和setWindow()
)推迟到下一个维护期 (如果您需要设置在设备处于低电耗模式时触发的闹钟,请使用setAndAllowWhileIdle()
或setExactAndAllowWhileIdle()
,使用setAlarmClock()
设置的闹钟将继续正常触发,系统会在这些闹钟触发之前不久退出低电耗模式) - 系统不执行 WLAN 扫描
- 系统不允许运行同步适配器
- 系统不允许运行
JobScheduler
测试相关:
运行以下命令,强制系统进入doze模式
adb shell dumpsys deviceidle force-idle
准备就绪后,运行以下命令,使系统退出闲置模式
adb shell dumpsys deviceidle unforce
执行以下命令,重新激活设备
adb shell dumpsys battery reset
以上介绍内容来自:
https://developer.android.com/topic/performance?hl=zh-cn
代码实现简介(基于android R):
Doze模式的控制代码都在DeviceIdleController中,DeviceIdleController作为一个系统服务在systemserver中启动,启动后监听电池广播/应用卸载广播/屏幕亮灭广播,还通过注册传感器监听来获知当前设备是否在请求未知信息,是否有运动,来确定当前设备状态,是否需要进入doze模式。进入doze模式后通过NetworkPolicyManagerService来修改联网策略,通过BatteryStatsService修改电池省电策略,通过PowerManagerService来修改持锁策略,且通过发送广播DEVICE_IDLE_MODE_CHANGED来通知对应的省电模块和jobschedule模块做出相应调整。
Doze模式状态图如下:
Doze模式状态流转如下图:
DeviceIdleController根据不同状态做不同的检测,设置不同的闹钟时间间隔,在最后强制设备进入doze睡眠模式,且会在进入后一段时间自动的退出doze模式,短暂退出后会再次进入,且距离下次退出doze模式时间会逐次拉长。在任意一个非STATA_ACTIVE状态下,如果接收到亮屏或者充电广播,或者是注册的传感器监听到运动信息,都会直接将状态回退到STATE_ACTIVE状态,所以进入doze模式的条件就是在灭屏不充电状态下经过一段时间的静止后才会进入,这个时间通过系统设置。
DeviceIdleController的运行机制主要依赖一下四个模块:
- 数据模块Constants,提供了基础数据,比如各个不同状态直接闹钟的间隔时间
- 广播监听,监听电池变化,亮灭屏,应用卸载,网络变化广播,来确定进入或者退出doze状态模式流转的时机
- 传感器监听,通过监听传感器,获知当前设备活动情况
- 闹钟,各状态间切换,doze模式的开放窗口期进入都是通过闹钟来操作
几个比较重要的方法如下:
初始化:
主要的初始化工作都在onBootPhase和onStart方法中完成,这里主要对一些需要用大的服务做初始化,广播注册,数据初始化,白名单初始化
@Overridepublic void onBootPhase(int phase) {Slog.d(TAG, "onBootPhase phase = " + phase);if (phase == PHASE_SYSTEM_SERVICES_READY) {synchronized (this) {//各种需要用的的服务管理类的初始化mAlarmManager = mInjector.getAlarmManager();mLocalAlarmManager = getLocalService(AlarmManagerInternal.class);mBatteryStats = BatteryStatsService.getService();mLocalActivityManager = getLocalService(ActivityManagerInternal.class);mLocalActivityTaskManager = getLocalService(ActivityTaskManagerInternal.class);mLocalPowerManager = getLocalService(PowerManagerInternal.class);mPowerManager = mInjector.getPowerManager();mActiveIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"deviceidle_maint");mActiveIdleWakeLock.setReferenceCounted(false);mGoingIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"deviceidle_going_idle");mGoingIdleWakeLock.setReferenceCounted(true);mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));mNetworkPolicyManagerInternal = getLocalService(NetworkPolicyManagerInternal.class);mSensorManager = mInjector.getSensorManager();if (mUseMotionSensor) {mMotionSensor = mInjector.getMotionSensor();}mAppStateTracker.onSystemServicesReady();......//广播和广播接收器的初始化和注册mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY| Intent.FLAG_RECEIVER_FOREGROUND);mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY| Intent.FLAG_RECEIVER_FOREGROUND);IntentFilter filter = new IntentFilter();filter.addAction(Intent.ACTION_BATTERY_CHANGED);getContext().registerReceiver(mReceiver, filter);filter = new IntentFilter();filter.addAction(Intent.ACTION_PACKAGE_REMOVED);filter.addDataScheme("package");getContext().registerReceiver(mReceiver, filter);filter = new IntentFilter();filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);getContext().registerReceiver(mReceiver, filter);filter = new IntentFilter();filter.addAction(Intent.ACTION_SCREEN_OFF);filter.addAction(Intent.ACTION_SCREEN_ON);getContext().registerReceiver(mInteractivityReceiver, filter);mLocalActivityManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray);mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);mLocalPowerManager.registerLowPowerModeObserver(ServiceType.QUICK_DOZE,state -> {synchronized (DeviceIdleController.this) {updateQuickDozeFlagLocked(state.batterySaverEnabled);}});updateQuickDozeFlagLocked(mLocalPowerManager.getLowPowerState(ServiceType.QUICK_DOZE).batterySaverEnabled);mLocalActivityTaskManager.registerScreenObserver(mScreenObserver);passWhiteListsToForceAppStandbyTrackerLocked();updateInteractivityLocked();}//根据联网状态判断light模式状态updateConnectivityState(null);}}
@Overridepublic void onStart() {final PackageManager pm = getContext().getPackageManager();synchronized (this) {mLightEnabled = mDeepEnabled = getContext().getResources().getBoolean(com.android.internal.R.bool.config_enableAutoPowerModes);SystemConfig sysConfig = SystemConfig.getInstance();ArraySet<String> allowPowerExceptIdle = sysConfig.getAllowInPowerSaveExceptIdle();for (int i=0; i<allowPowerExceptIdle.size(); i++) {String pkg = allowPowerExceptIdle.valueAt(i);try {ApplicationInfo ai = pm.getApplicationInfo(pkg,PackageManager.MATCH_SYSTEM_ONLY);int appid = UserHandle.getAppId(ai.uid);mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid);mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true);} catch (PackageManager.NameNotFoundException e) {}}ArraySet<String> allowPower = sysConfig.getAllowInPowerSave();for (int i=0; i<allowPower.size(); i++) {String pkg = allowPower.valueAt(i);try {ApplicationInfo ai = pm.getApplicationInfo(pkg,PackageManager.MATCH_SYSTEM_ONLY);int appid = UserHandle.getAppId(ai.uid);// These apps are on both the whitelist-except-idle as well// as the full whitelist, so they apply in all cases.mPowerSaveWhitelistAppsExceptIdle.put(ai.packageName, appid);mPowerSaveWhitelistSystemAppIdsExceptIdle.put(appid, true);mPowerSaveWhitelistApps.put(ai.packageName, appid);mPowerSaveWhitelistSystemAppIds.put(appid, true);} catch (PackageManager.NameNotFoundException e) {}}mConstants = mInjector.getConstants(this, mHandler, getContext().getContentResolver());readConfigFileLocked();updateWhitelistAppIdsLocked();mNetworkConnected = true;mScreenOn = true;mScreenLocked = false;// Start out assuming we are charging. If we aren't, we will at least get// a battery update the next time the level drops.mCharging = true;mActiveReason = ACTIVE_REASON_UNKNOWN;mState = STATE_ACTIVE;mLightState = LIGHT_STATE_ACTIVE;mInactiveTimeout = mConstants.INACTIVE_TIMEOUT;mPreIdleFactor = 1.0f;mLastPreIdleFactor = 1.0f;}mBinderService = new BinderService();publishBinderService(Context.DEVICE_IDLE_CONTROLLER, mBinderService);mLocalService = new LocalService();publishLocalService(DeviceIdleInternal.class, mLocalService);}
电池与卸载广播接收器:
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {@Override public void onReceive(Context context, Intent intent) {switch (intent.getAction()) {case ConnectivityManager.CONNECTIVITY_ACTION: {updateConnectivityState(intent);} break;case Intent.ACTION_BATTERY_CHANGED: {boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);boolean plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;synchronized (DeviceIdleController.this) {updateChargingLocked(present && plugged);}} break;case Intent.ACTION_PACKAGE_REMOVED: {if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {Uri data = intent.getData();String ssp;if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {removePowerSaveWhitelistAppInternal(ssp);}}} break;}}};
两灭屏广播:
private final BroadcastReceiver mInteractivityReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {synchronized (DeviceIdleController.this) {updateInteractivityLocked();}}};
不管是电池变化的广播中调用的updateChargingLocked方法还是亮灭屏广播中的updateInteractivityLocked方法,最终都调用了两个方法:
进入不活跃状态becomeInactiveIfAppropriateLocked,进入活跃状态becomeActiveLocked
void becomeInactiveIfAppropriateLocked() {verifyAlarmStateLocked();final boolean isScreenBlockingInactive =mScreenOn && (!mConstants.WAIT_FOR_UNLOCK || !mScreenLocked);if (!mForceIdle && (mCharging || isScreenBlockingInactive)) {return;}// Become inactive and determine if we will ultimately go idle.if (mDeepEnabled) {if (mQuickDozeActivated) {if (mState == STATE_QUICK_DOZE_DELAY || mState == STATE_IDLE|| mState == STATE_IDLE_MAINTENANCE) {// Already "idling". Don't want to restart the process.// mLightState can't be LIGHT_STATE_ACTIVE if mState is any of these 3// values, so returning here is safe.return;}if (DEBUG) {Slog.d(TAG, "Moved from "+ stateToString(mState) + " to STATE_QUICK_DOZE_DELAY");}mState = STATE_QUICK_DOZE_DELAY;// Make sure any motion sensing or locating is stopped.resetIdleManagementLocked();if (isUpcomingAlarmClock()) {scheduleAlarmLocked(mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime()+ mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);} else {scheduleAlarmLocked(mConstants.QUICK_DOZE_DELAY_TIMEOUT, false);}EventLogTags.writeDeviceIdle(mState, "no activity");} else if (mState == STATE_ACTIVE) {mState = STATE_INACTIVE;if (DEBUG) Slog.d(TAG, "Moved from STATE_ACTIVE to STATE_INACTIVE");resetIdleManagementLocked();long delay = mInactiveTimeout;if (shouldUseIdleTimeoutFactorLocked()) {delay = (long) (mPreIdleFactor * delay);}if (isUpcomingAlarmClock()) {scheduleAlarmLocked(mAlarmManager.getNextWakeFromIdleTime() - mInjector.getElapsedRealtime()+ delay, false);} else {scheduleAlarmLocked(delay, false);}EventLogTags.writeDeviceIdle(mState, "no activity");}}if (mLightState == LIGHT_STATE_ACTIVE && mLightEnabled) {mLightState = LIGHT_STATE_INACTIVE;if (DEBUG) Slog.d(TAG, "Moved from LIGHT_STATE_ACTIVE to LIGHT_STATE_INACTIVE");resetLightIdleManagementLocked();scheduleLightAlarmLocked(mConstants.LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT);EventLogTags.writeDeviceIdleLight(mLightState, "no activity");}}
becomeInactiveIfAppropriateLocked方法主要功能就是设置进入下一状态的闹钟
private void becomeActiveLocked(String activeReason, int activeUid,long newInactiveTimeout, boolean changeLightIdle) {if (mState != STATE_ACTIVE || mLightState != STATE_ACTIVE) {EventLogTags.writeDeviceIdle(STATE_ACTIVE, activeReason);mState = STATE_ACTIVE;mInactiveTimeout = newInactiveTimeout;resetIdleManagementLocked();// Don't reset maintenance window start time if we're in a light idle maintenance window// because its used in the light idle budget calculation.if (mLightState != LIGHT_STATE_IDLE_MAINTENANCE) {mMaintenanceStartTime = 0;}if (changeLightIdle) {EventLogTags.writeDeviceIdleLight(LIGHT_STATE_ACTIVE, activeReason);mLightState = LIGHT_STATE_ACTIVE;resetLightIdleManagementLocked();// Only report active if light is also ACTIVE.scheduleReportActiveLocked(activeReason, activeUid);addEvent(EVENT_NORMAL, activeReason);}}}
becomeActiveLocked的功能是将状态设置回active状态,并且取消所有设置的进入下一状态的闹钟,取消行为监听
doze模式light与deep两种模式之间各个状态之间转换在stepIdleStateLocked与stepLightIdleStateLocked方法中实现,这里详细介绍下stepIdleStateLocked
@VisibleForTestingvoid stepIdleStateLocked(String reason) {......switch (mState) {case STATE_INACTIVE:// We have now been inactive long enough, it is time to start looking// for motion and sleep some more while doing so.startMonitoringMotionLocked();long delay = mConstants.IDLE_AFTER_INACTIVE_TIMEOUT;if (shouldUseIdleTimeoutFactorLocked()) {delay = (long) (mPreIdleFactor * delay);}scheduleAlarmLocked(delay, false);moveToStateLocked(STATE_IDLE_PENDING, reason);break;case STATE_IDLE_PENDING:moveToStateLocked(STATE_SENSING, reason);cancelLocatingLocked();mLocated = false;mLastGenericLocation = null;mLastGpsLocation = null;updateActiveConstraintsLocked();// Wait for open constraints and an accelerometer reading before moving on.if (mUseMotionSensor && mAnyMotionDetector.hasSensor()) {scheduleSensingTimeoutAlarmLocked(mConstants.SENSING_TIMEOUT);mNotMoving = false;mAnyMotionDetector.checkForAnyMotion();break;} else if (mNumBlockingConstraints != 0) {cancelAlarmLocked();break;}mNotMoving = true;// Otherwise, fall through and check this off the list of requirements.case STATE_SENSING:cancelSensingTimeoutAlarmLocked();moveToStateLocked(STATE_LOCATING, reason);scheduleAlarmLocked(mConstants.LOCATING_TIMEOUT, false);LocationManager locationManager = mInjector.getLocationManager();if (locationManager != null&& locationManager.getProvider(LocationManager.NETWORK_PROVIDER) != null) {locationManager.requestLocationUpdates(mLocationRequest,mGenericLocationListener, mHandler.getLooper());mLocating = true;} else {mHasNetworkLocation = false;}if (locationManager != null&& locationManager.getProvider(LocationManager.GPS_PROVIDER) != null) {mHasGps = true;locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 5,mGpsLocationListener, mHandler.getLooper());mLocating = true;} else {mHasGps = false;}// If we have a location provider, we're all set, the listeners will move state// forward.if (mLocating) {break;}// Otherwise, we have to move from locating into idle maintenance.case STATE_LOCATING:cancelAlarmLocked();cancelLocatingLocked();mAnyMotionDetector.stop();// Intentional fallthrough -- time to go into IDLE state.case STATE_QUICK_DOZE_DELAY:// Reset the upcoming idle delays.mNextIdlePendingDelay = mConstants.IDLE_PENDING_TIMEOUT;mNextIdleDelay = mConstants.IDLE_TIMEOUT;// Everything is in place to go into IDLE state.case STATE_IDLE_MAINTENANCE:scheduleAlarmLocked(mNextIdleDelay, true);if (DEBUG) Slog.d(TAG, "Moved to STATE_IDLE. Next alarm in " + mNextIdleDelay +" ms.");mNextIdleDelay = (long)(mNextIdleDelay * mConstants.IDLE_FACTOR);if (DEBUG) Slog.d(TAG, "Setting mNextIdleDelay = " + mNextIdleDelay);mIdleStartTime = SystemClock.elapsedRealtime();mNextIdleDelay = Math.min(mNextIdleDelay, mConstants.MAX_IDLE_TIMEOUT);if (mNextIdleDelay < mConstants.IDLE_TIMEOUT) {mNextIdleDelay = mConstants.IDLE_TIMEOUT;}moveToStateLocked(STATE_IDLE, reason);if (mLightState != LIGHT_STATE_OVERRIDE) {mLightState = LIGHT_STATE_OVERRIDE;cancelLightAlarmLocked();}addEvent(EVENT_DEEP_IDLE, null);mGoingIdleWakeLock.acquire();mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);break;case STATE_IDLE:// We have been idling long enough, now it is time to do some work.mActiveIdleOpCount = 1;mActiveIdleWakeLock.acquire();scheduleAlarmLocked(mNextIdlePendingDelay, false);if (DEBUG) Slog.d(TAG, "Moved from STATE_IDLE to STATE_IDLE_MAINTENANCE. " +"Next alarm in " + mNextIdlePendingDelay + " ms.");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;}moveToStateLocked(STATE_IDLE_MAINTENANCE, reason);addEvent(EVENT_DEEP_MAINTENANCE, null);mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);break;}}
从这里可以清晰的看到doze模式是怎么一步步深入检测的,从息屏断电到进入doze模式,需要经过多次闹钟等待,这期间不能有gps请求或者设备活动,否则会直接回退到最初的active状态,进入doze模式后通过mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
@Override public void handleMessage(Message msg) {switch (msg.what) {case MSG_REPORT_IDLE_ON:case MSG_REPORT_IDLE_ON_LIGHT: {// mGoingIdleWakeLock is held at this pointEventLogTags.writeDeviceIdleOnStart();final boolean deepChanged;final boolean lightChanged;if (msg.what == MSG_REPORT_IDLE_ON) {deepChanged = mLocalPowerManager.setDeviceIdleMode(true);lightChanged = mLocalPowerManager.setLightDeviceIdleMode(false);} else {deepChanged = mLocalPowerManager.setDeviceIdleMode(false);lightChanged = mLocalPowerManager.setLightDeviceIdleMode(true);}try {mNetworkPolicyManager.setDeviceIdleMode(true);mBatteryStats.noteDeviceIdleMode(msg.what == MSG_REPORT_IDLE_ON? BatteryStats.DEVICE_IDLE_MODE_DEEP: BatteryStats.DEVICE_IDLE_MODE_LIGHT, null, Process.myUid());} catch (RemoteException e) {}if (deepChanged) {getContext().sendBroadcastAsUser(mIdleIntent, UserHandle.ALL);}if (lightChanged) {getContext().sendBroadcastAsUser(mLightIdleIntent, UserHandle.ALL);}EventLogTags.writeDeviceIdleOnComplete();mGoingIdleWakeLock.release();} break;
进入后通过设置NetworkPolicyManagerService和BatteryStatsService的标志位来修改应用的联网策略和电视省电策略,并且发送广播通知其他模块进入了doze模式,
其中广播会在DeviceIdleJobsController中对待执行的jobschedler进行延迟操作。
case MSG_REPORT_IDLE_OFF: {// mActiveIdleWakeLock is held at this pointEventLogTags.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);}// Always start with one active op for the message being sent here.// Now we are done!decActiveIdleOps();EventLogTags.writeDeviceIdleOffComplete();} break;
进入maintenance状态所进行的操作和进入idle状态的操作正好相反,将NetworkPolicyManagerService和BatteryStatsService的标志位设置为false,发送广播通知idle状态短暂的结束。被延迟的job会被执行,此时应用也可以访问网络。
至此DeviceIdleController的主要功能就大体分析完成,从谷歌官网提供的信息来看doze模式功能十分强大,但是DeviceIdleController却只是起到一个负责doze的进入与退出,通过一些列的条件判断是否需要进入doze模式,一旦进入真正的各种省电策略都是通过其他模块来具体协助实现,比如联网策略控制,电池状态控制,job任务控制,以进一步减少设备在不活动期间后台电量消耗,达到省电的目地。