AndroidO Battery saver省电助手实现原理

1. 功能概述

Battery saver是Google在Android L上新增的选项,这个功能是在Setting -> Battery (–> more (androidO以前的路径)) –> Battery saver,这个功能主要是为了在相同电量下能够更长时间的使用手机,简称:“省电助手”。

打开之后手机将处于省电模式,省电模式下电池使用量将大大降低,一些不必要的耗电操作将禁止。属于一个很实用的功能。

2. 实现原理分析

下面我们来介绍一下Battery saver的实现原理与其做了什么电源管理类型的优化。

2.1 Battery saver界面显示与功能

android省电模式检测 安卓省电神器_android


图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会对此操作进行一下界面上的更新

android省电模式检测 安卓省电神器_saver_02


图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 -->

android省电模式检测 安卓省电神器_battery_03


图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、打开关闭省电模式各自取一个结果

=>关闭省电模式的结果

android省电模式检测 安卓省电神器_android省电模式检测_04


图4.1 关闭省电模式的结果=>打开省电模式的结果

android省电模式检测 安卓省电神器_android_05


图4.2 打开省电模式的结果

或许大家会问我们还有什么其它省电方案可以使用吗?优化部分涉及技术核心这个就不在本文讨论范畴内。