AndroidO Battery saver省电助手实现原理
1. 功能概述
Battery saver是Google在Android L上新增的选项,这个功能是在Setting -> Battery (–> more (androidO以前的路径)) –> Battery saver,这个功能主要是为了在相同电量下能够更长时间的使用手机,简称:“省电助手”。
打开之后手机将处于省电模式,省电模式下电池使用量将大大降低,一些不必要的耗电操作将禁止。属于一个很实用的功能。
2. 实现原理分析
下面我们来介绍一下Battery saver的实现原理与其做了什么电源管理类型的优化。
2.1 Battery saver界面显示与功能
图2.1 Battery Saver界面
从图2.1我们可以知道Battery saver有一个switch开关,还有一个选项(是否自动打开battery saver)
1、先来看一下Battery saver的switch开关,当我们打开Battery saver的时候
//packages/apps/Settings/src/com/android/settings/fuelgauge/BatterySaverSettings.java
private void trySetPowerSaveMode(boolean mode) {
if (!mPowerManager.setPowerSaveMode(mode)) {
…
}
2、先来看一下Battery saver的switch开关,当我们打开Battery saver的时候
//frameworks/base/core/java/android/provider/Settings.java
public static final String LOW_POWER_MODE_TRIGGER_LEVEL = "low_power_trigger_level";//电源保护模式的触发门槛值
//packages/apps/Settings/src/com/android/settings/fuelgauge/BatterySaverSettings.java
// battery_saver_trigger_values是电源保护触发门槛值的可选值,’0’代表关闭,’5’代表5%电量的时候自动进入电源保护模式,’15’代表15%电量的时候自动进入电源保护模式
mTriggerPref = new SettingPref(SettingPref.TYPE_GLOBAL, KEY_TURN_ON_AUTOMATICALLY,
Global.LOW_POWER_MODE_TRIGGER_LEVEL,
0, /*default*/
getResources().getIntArray(R.array.battery_saver_trigger_values))
<!-- Battery saver mode: allowable trigger threshold levels. -->
<integer-array name="battery_saver_trigger_values" translatable="false" >
<item>0</item>
<item>5</item>
<item>15</item>
</integer-array>
2.2 进入Power Save Mode的条件
1、当应用调用了PMS的setPowerSaveMode进行电源保护模式,会进入PowerManagerService.java,里面主要包括权限检查和进一步设置
//PowerManagerService.java
public boolean setPowerSaveMode(boolean mode) {
//这个操作需要调用者用有DEVICE_POWER权限
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.DEVICE_POWER, null);
//......
//进行电源保护模式内部设置
return setLowPowerModeInternal(mode);
}
2、电源保护模式内部设置setLowPowerModeInternal,里面包含电源保护标志位LOW_POWER_MODE的设置,电源保护模式mLowPowerModeSetting的设置
private boolean setLowPowerModeInternal(boolean mode) {
……
//此时会设置电源保护标志位LOW_POWER_MODE设置
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.LOW_POWER_MODE, mode ? 1 : 0);
mLowPowerModeSetting = mode;//电源保护模式设置
……
updateLowPowerModeLocked();//电源保护
return true;
}
3、更新电源保护相关设置updateLowPowerModeLocked,该函数包括了大部分电源保护的逻辑处理。
1) 如果在充电中或者”非低电而且没有开完机”情况下是不允许进入电源保护模式
2) 自动打开电源保护的条件是:不在充电状态、设置了自动进入电源保护、用户没有主动在低电下关闭低电模式、低电状态
3) 只要手动设置电源保护mLowPowerModeSetting,或者自动进入电源保护,那么电源保护模式lowPowerModeEnabled将会给打开
4) 进行Power HAL的设置,android默认的default Power HAL是power.default.so,里面的内容默认都是置空,也就是没有任何操作。
5) 进行电源保护模式切换前会发生一个ACTION_POWER_SAVE_MODE_CHANGING的广播,然后回调所有监听了电源保护的监听器,最后切换成功后会发生ACTION_POWER_SAVE_MODE_CHANGED的广播。
6) 会将mLowPowerModeListeners低电模式的监听onLowPowerModeChanged全部回调一遍
void updateLowPowerModeLocked() {
//如果在充电中是不允许进入电源保护模式,其中在androidO上新增在非低电量
//(当电量低于15%属于低电模式,config_lowBatteryWarningLevel可以配置)
//而且还没开完机的时候也是不允许进入该模式的
if ((mIsPowered || !mBatteryLevelLow && !mBootCompleted) && mLowPowerModeSetting) {
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.LOW_POWER_MODE, 0);
mLowPowerModeSetting = false;
}
// autoLowPowerModeEnabled代表是否需要自动打开电源保护,条件是不在充电状态、设置了自动进入电源保护、用户没有主动在低电下关闭低电模式、低电状态
final boolean autoLowPowerModeEnabled = !mIsPowered && mAutoLowPowerModeConfigured
&& !mAutoLowPowerModeSnoozing && mBatteryLevelLow;
//只要手动设置电源保护mLowPowerModeSetting,或者自动进入电源保护,那么电源保护模式lowPowerModeEnabled将会给打开
final boolean lowPowerModeEnabled = mLowPowerModeSetting || autoLowPowerModeEnabled;
if (mLowPowerModeEnabled != lowPowerModeEnabled) {//电源保护模式进行了改变
mLowPowerModeEnabled = lowPowerModeEnabled;//设置当前是否处于电源保护模式
//进行Power HAL的设置,android默认的default Power HAL是power.default.so里面是做空的
powerHintInternal(POWER_HINT_LOW_POWER, lowPowerModeEnabled ? 1 : 0);
//在androidO上是在开机完成后在进行设置
postAfterBootCompleted( new Runnable() {
@Override
public void run() {
Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGING)
.putExtra(PowerManager.EXTRA_POWER_SAVE_MODE, mLowPowerModeEnabled)
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
//发送ACTION_POWER_SAVE_MODE_CHANGING电源保护正在切换的广播
mContext.sendBroadcast(intent);
……
for (int i=0; i<listeners.size(); i++) {
final PowerManagerInternal.LowPowerModeListener listener = listeners.get(i);
final PowerSaveState result = mBatterySaverPolicy.getBatterySaverPolicy(
listener.getServiceType(), lowPowerModeEnabled);
//回调监听了电源保护模式的listeners,androidO在此处回调的方法有更加多的内容,包含多种策略
listener.onLowPowerModeChanged(result);
}
intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
//添加flag ACTION_POWER_SAVE_MODE_CHANGED),这个广播在androidN以后就只会给动态注册者接收
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
// 发送ACTION_POWER_SAVE_MODE_CHANGED电源保护切换成功后的广播
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
// 需要带有相应权限才会受到的ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL广播,这个目前源码没有使用
intent = new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED_INTERNAL);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
Manifest.permission.DEVICE_POWER);
}
});
}
}
接下来讲的是对系统实际的影响
2.3 Power Save Mode对系统的实际影响
2.3.1 屏幕亮度将会变成原来的50%
我们在设置了LOW_POWER_MODE电源保护模式的时候,PowerManagerService本身会对其进行监听,监听器改变后会进行显示设备状态更新updateDisplayPowerStateLocked
private boolean updateDisplayPowerStateLocked(int dirty) {
……
//传入电源保护模式
updatePowerRequestFromBatterySaverPolicy(mDisplayPowerRequest);
……
mDisplayManagerInternal.requestPowerState(mDisplayPowerRequest,
mRequestWaitForNegativeProximity);//更新显示设备的状态请求
……
}
void updatePowerRequestFromBatterySaverPolicy(DisplayPowerRequest displayPowerRequest) {
PowerSaveState state = mBatterySaverPolicy.
getBatterySaverPolicy(ServiceType.SCREEN_BRIGHTNESS, mLowPowerModeEnabled);
//设置低电模式和屏幕亮度降低的比例,一般都是降低50%
displayPowerRequest.lowPowerMode = state.batterySaverEnabled;
displayPowerRequest.screenLowPowerBrightnessFactor = state.brightnessFactor;
}
显示设备状态更新的控制器DisplayPowerController.java
private void updatePowerState() {
……
// 只要打开电源保护模式,屏幕亮度减半
// 当然啦,需要大于最小亮度值
if (mPowerRequest.lowPowerMode) {
if (brightness > mScreenBrightnessRangeMinimum) {
// brightnessFactor就是刚才设置的降低亮度的比例,默认是50%
final int lowPowerBrightness = (int) (brightness * brightnessFactor);
brightness = Math.max(lowPowerBrightness, mScreenBrightnessRangeMinimum);
//默认最小亮度值mScreenBrightnessRangeMinimum一般都是1
brightness = Math.max(brightness / 2, mScreenBrightnessRangeMinimum);
}
……
}
2.3.2 systemui会对此操作进行一下界面上的更新
图2.2 打开电源保护后systemui界面
Ps:上述是androidO以前的界面,androidO的状态栏是红色的。
该橙色/红色是一种status bar的警告Warning的标识(androidO使用的是error标识是红色),代表正处于电源保护模式(在发送ACTION_POWER_SAVE_MODE_CHANGING的时候就会通知status状态栏更新)
//StatusBar.java
private void checkBarMode(int mode, int windowState, BarTransitions transitions) {
final boolean powerSave = mBatteryController.isPowerSave();//check whether you are in power mode
……
if (powerSave && getBarState() == StatusBarState.SHADE) {
mode = MODE_WARNING;//change mode to power mode,when in status bar normol state
}
transitions.transitionTo(mode, anim);
}
//BarTransitions.java
public void draw(Canvas canvas) {
int targetGradientAlpha = 0, targetColor = 0;
if (mMode == MODE_WARNING) {
targetColor = mWarning;//绘制bar的时候,将warning颜色赋值给status bar显示
mWarning = Utils.getColorAttr(context, android.R.attr.colorError);
./res/values/themes.xml
<item name="colorError">@color/red</item>
./res/res/values/colors.xml
//androidO使用的是红色,RGB只有R有颜色
<color name="red">#ffff0000</color>
//f4 51 1e切换成十进制的话244 81 30,androidO以前使用的颜色
<color name="battery_saver_mode_color">#fff4511e</color><!-- deep orange 600 -->
图2.3 battery_saver_mode_color的颜色值
红色就没必要查看了,只有红色分量肯定是红色。
2.3.3 modem进入低电模式
DeviceStateMonitor.java通过接收ACTION_POWER_SAVE_MODE_CHANGED的广播将modem设置成低电模式。(AndroidO新增,说明google也意识到了功耗属于android的通病,在modem这一块也进行优化)
private void updateDeviceState(int eventType, boolean state) {
//..….
case EVENT_POWER_SAVE_MODE_CHANGED:
if (mIsPowerSaveOn == state) return;
mIsPowerSaveOn = state;
//设置设备进入低电模式
sendDeviceState(POWER_SAVE_MODE, mIsPowerSaveOn);
break;
//..….
private void sendDeviceState(int type, boolean state) {
// mPhone一般都是telcom通信相关,mCi是RIL对象,modem相关设置
mPhone.mCi.sendDeviceState(type, state, null);
}
2.3.4 语音互动功能将关闭
SoundTriggerHelper.java会接收POWER_SAVE_MODE_CHANGED的广播,语音互动voiceinteraction功能将关闭
//启动语音识别
private int startRecognitionLocked(ModelData modelData, boolean notify) {
//……
if (!isRecognitionAllowed()) {//判断是否运行识别
//……
MetricsLogger.count(mContext, "sth_start_recognition_not_allowed", 1);
return STATUS_OK;
}
//……
private boolean isRecognitionAllowed() {
//在打电话、该服务禁止掉或者省电模式中不允许语言识别功能
return !mCallActive && !mServiceDisabled && !mIsPowerSaveMode;
}
2.3.5 地理位置信息进行限制访问
GnssLocationProvider.java会接收POWER_SAVE_MODE_CHANGED的广播,限制gps使用,灭屏后会关闭gps
private void updateLowPowerMode() {
// 如果设备是深度睡眠,不再允许使用GPS
boolean disableGps = mPowerManager.isDeviceIdleMode();
final PowerSaveState result =
mPowerManager.getPowerSaveState(ServiceType.GPS);
switch (result.gpsMode) {//如果有相关GPS设置,一般都有
case BatterySaverPolicy.GPS_MODE_DISABLED_WHEN_SCREEN_OFF:
// 默认情况是低电模式在灭屏情况不允许GPS
disableGps |= result.batterySaverEnabled && !mPowerManager.isInteractive();
break;
}
//……
} }
2.3.6 震动将取消
震动将取消(原来如果有震动的话)的操作是在VibratorService.java
当处于电池保护模式下,除了来电显示的震动(有条件的震动),其它都将关闭震动
//启动震动的函数
private void startVibrationLocked(final Vibration vib) {
//判断是否允许震动,关于低电模式是在这里判断
if (!isAllowedToVibrate(vib)) {
//……
//在铃声静音(且设置了来电震动)或者不在震动模式(且没有设置来电震动时)的情况下不允许通知类型的铃声震动
if (vib.mUsageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE &&
!shouldVibrateForRingtone()) {
//……
//当然,如果你没有权限,什么都不行
if (mode != AppOpsManager.MODE_ALLOWED) {
//……
}
//是否允许震动的函数
private boolean isAllowedToVibrate(Vibration vib) {
//如果是非低电模式下是允许震动的
if (!mLowPowerMode) {//onLowPowerModeChanged时会进行设置
return true;
}
//低电模式对于铃声震动不受限制
if (vib.mUsageHint == AudioAttributes.USAGE_NOTIFICATION_RINGTONE) {
return true;
}
//是否允许在低电模式下震动,一般这个值默认是false
if (!mAllowPriorityVibrationsInLowPowerMode) {
return false;
}
//如果允许在低电模式下震动,仅支持alarm,助手、网络电话进行震动
if (vib.mUsageHint == AudioAttributes.USAGE_ALARM ||
vib.mUsageHint == AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY ||
vib.mUsageHint == AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST) {
return true;
}
//其它默认是不允许震动的
return false;
}
2.3.7 对于网络数据链接会进行限制接入
NetworkPolicyManagerService.java会进行相应的限制接入处理,专门设置了一套FIREWALL_CHAIN_POWERSAVE低电模式的防火墙体系.
1、更新白名单范围(允许访问的网络的范围),范围之外的将停止socket网络链接
// 更新防火墙规则,低电模式下enabled==true,chain==FIREWALL_CHAIN_POWERSAVE此模式下防火墙属于白名单模式,除了白名单以外的都不运行访问网络
private void updateRulesForWhitelistedPowerSaveUL(boolean enabled, int chain,
SparseIntArray rules) {
if (enabled) {
// 更新临时白名单、白名单、除了idleapp之外的白名单都将允许网络访问
for (int ui = users.size() - 1; ui >= 0; ui--) {
UserInfo user = users.get(ui);
updateRulesForWhitelistedAppIds(uidRules, mPowerSaveTempWhitelistAppIds, user.id);
updateRulesForWhitelistedAppIds(uidRules, mPowerSaveWhitelistAppIds, user.id);
if (chain == FIREWALL_CHAIN_POWERSAVE) {
updateRulesForWhitelistedAppIds(uidRules,
mPowerSaveWhitelistExceptIdleAppIds, user.id);
}
}
// 如果是进程优先级是前台服务以上的允许网络访问(前台服务一般是PERCEPTIBLE用户可感知的进程优先级以上的服务)
for (int i = mUidState.size() - 1; i >= 0; i--) {
if (isProcStateAllowedWhileIdleOrPowerSaveMode(mUidState.valueAt(i))) {
uidRules.put(mUidState.keyAt(i), FIREWALL_RULE_ALLOW);
}
}
setUidFirewallRulesAsync(chain, uidRules, CHAIN_TOGGLE_ENABLE);
} else {
setUidFirewallRulesAsync(chain, null, CHAIN_TOGGLE_DISABLE);
}
}
2、更新所有应用的访问规则,除了前台进程和在白名单内的进程,都将不允许访问网络
// 更新应用在低电模式下所有app的访问规则
private int updateRulesForPowerRestrictionsULInner(int uid, int oldUidRules, boolean paroled) {
//当uid是media或者drm类型的不需要,或者之前已经授权INTERNET网络访问的app,不许用更新
if (!isUidValidForBlacklistRules(uid)) {
if (LOGD) Slog.d(TAG, "no need to update restrict power rules for uid " + uid);
return RULE_NONE;
}
//是否处于空闲态
final boolean isIdle = !paroled && isUidIdle(uid);
//如果是低电模式会将进入受限模式
final boolean restrictMode = isIdle || mRestrictPower || mDeviceIdleMode;
//是否前台进程,和上面isProcStateAllowedWhileIdleOrPowerSaveMode类似
final boolean isForeground = isUidForegroundOnRestrictPowerUL(uid);
//是否处在电源白名单之内
final boolean isWhitelisted = isWhitelistedBatterySaverUL(uid, mDeviceIdleMode);
final int oldRule = oldUidRules & MASK_ALL_NETWORKS;
int newRule = RULE_NONE;
//......
if (isForeground) {
if (restrictMode) {
//如果是前台进程,就算是受限模式下也会允许访问网络
newRule = RULE_ALLOW_ALL;
}
} else if (restrictMode) {
//其它进程,非白名单将设置成拒绝访问RULE_REJECT_ALL
newRule = isWhitelisted ? RULE_ALLOW_ALL : RULE_REJECT_ALL;
}
//......
}
2.3.8 WindowManagerService的动画将关闭
WindowManagerService.java会将动画关闭(onLowPowerModeChanged),但是此处由于一些第三方应用如果没有考虑省电模式关闭动画的话,可能会出现一些问题(设计时需要考虑动画在低电模式关闭后的效果)。
public void onLowPowerModeChanged(PowerSaveState result) {
synchronized (mWindowMap) {
final boolean enabled = result.batterySaverEnabled;
//mAllowAnimationsInLowPowerMode代表是否在允许在低电模式下继续使用动画(一般是false,就是不允许),如果在低电模式下会把WMS的动画都关闭
if (mAnimationsDisabled != enabled && !mAllowAnimationsInLowPowerMode) {
mAnimationsDisabled = enabled;//是否关闭动画mAnimationsDisabled
dispatchNewAnimatorScaleLocked(null);//将动画设置更新到系统中去
}
}
}
3. 具体使用
我们如果要使用这个新功能还是比较简单的,
1、在AndroidManifest.xml加上权限
<uses-permission android:name="android.permission.DEVICE_POWER" />
2、在源码中调用
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mPowerManager.setPowerSaveMode(mode)//需要DEVICE_POWER权限
4. 实际效果
实验步骤
1、电量是100%
2、登录QQ,然后按home键切换到后台
3、打开或者关闭省电模式
4、设置屏幕超时30分钟,1个小时后观察结果(就是半个小时亮屏半个小时灭屏)
5、打开关闭省电模式各自取一个结果
=>关闭省电模式的结果
图4.1 关闭省电模式的结果=>打开省电模式的结果
图4.2 打开省电模式的结果
或许大家会问我们还有什么其它省电方案可以使用吗?优化部分涉及技术核心这个就不在本文讨论范畴内。