移动性能测试 详解 Android 耗电量 API

易寒 · 2015年10月20日 · 最后由 jb 回复于 2017年10月31日 · 3863 次阅读
本帖已被设为精华帖!

参考文章:Android 应用的耗电量统计
深入浅出 Android App 耗电量统计
Battery stats - CPU total vs CPU foreground
深入浅出 Android App 耗电量统计
浅析 Wakelock 机制与 Android 电源管理

耗电量 API

Android系统中很早就有耗电量的API,只不过一直都是隐藏的,Android系统的设置-电池功能就是调用的这个API,该API的核心部分是调用了com.android.internal.os.BatteryStatsHelper类,利用PowerProfile类,读取power_profile.xml文件,我们一起来看看具体如何计算耗电量,首先从最新版本 6.0 开始看

6.0 的 API

源码

BatteryStatsHelper
其中计算耗电量的方法为 490 行的processAppUsage,下来一步一步来解释该方法。

App 耗电量的计算探究

private void processAppUsage(SparseArray<UserHandle> asUsers) {

方法的参数是一个SparseArray数组,存储的对象是UserHandle,官方文档给出的解释是,代表一个用户,可以理解为这个类里面存储了用户的相关信息.

final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);

然后判断该次计算是否针对所有用户,通过UserHandleUSER_ALL值来判断,该值为-1,源码的地址在https://github.com/DoctorQ/platform_frameworks_base/blob/android-6.0.0_r1/core/java/android/os/UserHandle.java.

mStatsPeriod = mTypeBatteryRealtime;

然后给公共变量 int 类型的mStatsPeriod赋值,这个值mTypeBatteryRealtime的计算过程又在 320 行的refreshStats方法中:

mTypeBatteryRealtime = mStats.computeBatteryRealtime(rawRealtimeUs, mStatsType);

这里面用到了BatteryStats(mStats)类中的computeBatteryRealtime方法,该方法计算出此次统计电量的时间间隔。好,歪楼了,回到BatteryStatsHelper中。

BatterySipper osSipper = null;
final SparseArray<? extends Uid> uidStats = mStats.getUidStats();
final int NU = uidStats.size();

首先创建一个BatterySipper对象osSipper,该对象里面可以存储一些后续我们要计算的值,然后通过BatteryStats类对象mStats来得到一个包含Uid的对象的SparseArray组数,然后计算了一下这个数组的大小,保存在变量 NU 中。

for (int iu = 0; iu < NU; iu++) {
final Uid u = uidStats.valueAt(iu);
            final BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, 0);

然后for循环计算每个Uid代表的App的耗电量,因为BatterySipper可计算的类型有三种:应用, 系统服务, 硬件类型,所以这个地方传入的是DrainType.APP,还有其他可选类型如下:

public enum DrainType {
        IDLE,
        CELL,
        PHONE,
        WIFI,
        BLUETOOTH,
        FLASHLIGHT,
        SCREEN,
        APP,
        USER,
        UNACCOUNTED,
        OVERCOUNTED,
        CAMERA
    }

列举了目前可计算耗电量的模块。

mCpuPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mWakelockPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mWifiPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mSensorPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mCameraPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);
            mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType);

其中mStatsType的值为BatteryStats.STATS_SINCE_CHARGED,代表了我们的计算规则是从上次充满电后数据,还有一种规则是STATS_SINCE_UNPLUGGED是拔掉 USB 线后的数据。而mRawRealtime是当前时间,mRawUptime是运行时间。6.0 的对各个模块的消耗都交给了单独的类去计算,这些类都继承于PowerCalculator抽象类:

蓝牙耗电:BluetoothPowerCalculator.java
摄像头耗电:CameraPowerCalculator.java
Cpu耗电:CpuPowerCalculator.java
手电筒耗电:FlashlightPowerCalculator.java
无线电耗电:MobileRadioPowerCalculator.java
传感器耗电:SensorPowerCalculator.java
Wakelock耗电:WakelockPowerCalculator.java
Wifi耗电:WifiPowerCalculator.java

这一部分我一会单独拿出来挨个解释,现在我们还是回到BatteryStatsHelper继续往下走

final double totalPower = app.sumPower();

BatterySipper#sumPower方法是统计总耗电量,方法详情如下,其中usagePowerMah这个值有点特殊,其他的上面都讲过.

/**
     * Sum all the powers and store the value into `value`.
     * @return the sum of all the power in this BatterySipper.
     */
    public double sumPower() {
        return totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah +
                sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah +
                flashlightPowerMah;
    }

然后根据是否是 DEBUG 版本打印信息,这个没啥可说的,然后会把刚才计算的电量值添加到列表中:

// Add the app to the list if it is consuming power.
           if (totalPower != 0 || u.getUid() == 0) {
               //
               // Add the app to the app list, WiFi, Bluetooth, etc, or into "Other Users" list.
               //
               final int uid = app.getUid();
               final int userId = UserHandle.getUserId(uid);
               if (uid == Process.WIFI_UID) {
                   mWifiSippers.add(app);
               } else if (uid == Process.BLUETOOTH_UID) {
                   mBluetoothSippers.add(app);
               } else if (!forAllUsers && asUsers.get(userId) == null
                       && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) {
                   // We are told to just report this user's apps as one large entry.
                   List<BatterySipper> list = mUserSippers.get(userId);
                   if (list == null) {
                       list = new ArrayList<>();
                       mUserSippers.put(userId, list);
                   }
                   list.add(app);
               } else {
                   mUsageList.add(app);
               }

               if (uid == 0) {
                   osSipper = app;
               }
           }

首先判断totalPower的值和当前uid号是否符合规则,规则为总耗电量不为 0 或者用户 id 为 0.当uid表明为 WIFI 或者蓝牙时,添加到下面对应的列表中,一般情况下正常的应用我们直接保存到下面的mUsageList中就行就行,但是也有一些例外:

/**
     * List of apps using power.
     */
    private final List<BatterySipper> mUsageList = new ArrayList<>();

    /**
     * List of apps using wifi power.
     */
    private final List<BatterySipper> mWifiSippers = new ArrayList<>();

    /**
     * List of apps using bluetooth power.
     */
    private final List<BatterySipper> mBluetoothSippers = new ArrayList<>();

如果我们的系统是单用户系统,且当前的userId号不在我们的统计范围内,且其进程id号是大于Process.FIRST_APPLICATION_UID(10000,系统分配给普通应用的其实 id 号),我们就要将其存放到mUserSippers数组中,定义如下:

private final SparseArray<List<BatterySipper>> mUserSippers = new SparseArray<>();

最后判断uid为 0 的话,代表是Android操作系统的耗电量,赋值给osSipper(494 行定义) 就可以了,这样一个app的计算就完成了,遍历部分就不说了,保存这个osSipper是为了最后一步计算:

if (osSipper != null) {
            // The device has probably been awake for longer than the screen on
            // time and application wake lock time would account for.  Assign
            // this remainder to the OS, if possible.
            mWakelockPowerCalculator.calculateRemaining(osSipper, mStats, mRawRealtime,
                                                        mRawUptime, mStatsType);
            osSipper.sumPower();
        }

主流程我们已经介绍完了,下面来看各个子模块耗电量的计算

Cpu 耗电量

CpuPowerCalculator.java

Cpu 的计算要用到PowerProfile类,该类主要是解析power_profile.xml:


<device name="Android">
  <!-- Most values are the incremental current used by a feature,
       in mA (measured at nominal voltage).
       The default values are deliberately incorrect dummy values.
       OEM's must measure and provide actual values before
       shipping a device.
       Example real-world values are given in comments, but they
       are totally dependent on the platform and can vary
       significantly, so should be measured on the shipping platform
       with a power meter. -->
  <item name="none">0</item>
  <item name="screen.on">0.1</item>  <!-- ~200mA -->
  <item name="screen.full">0.1</item>  <!-- ~300mA -->
  <item name="bluetooth.active">0.1</item> <!-- Bluetooth data transfer, ~10mA -->
  <item name="bluetooth.on">0.1</item>  <!-- Bluetooth on & connectable, but not connected, ~0.1mA -->
  <item name="wifi.on">0.1</item>  <!-- ~3mA -->
  <item name="wifi.active">0.1</item>  <!-- WIFI data transfer, ~200mA -->
  <item name="wifi.scan">0.1</item>  <!-- WIFI network scanning, ~100mA -->
  <item name="dsp.audio">0.1</item> <!-- ~10mA -->
  <item name="dsp.video">0.1</item> <!-- ~50mA -->
  <item name="camera.flashlight">0.1</item> <!-- Avg. power for camera flash, ~160mA -->
  <item name="camera.avg">0.1</item> <!-- Avg. power use of camera in standard usecases, ~550mA -->
  <item name="radio.active">0.1</item> <!-- ~200mA -->
  <item name="radio.scanning">0.1</item> <!-- cellular radio scanning for signal, ~10mA -->
  <item name="gps.on">0.1</item> <!-- ~50mA -->
  <!-- Current consumed by the radio at different signal strengths, when paging -->
  <array name="radio.on"> <!-- Strength 0 to BINS-1 -->
      <value>0.2</value> <!-- ~2mA -->
      <value>0.1</value> <!-- ~1mA -->
  </array>
  <!-- Different CPU speeds as reported in
       /sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state -->
  <array name="cpu.speeds">
      <value>400000</value> <!-- 400 MHz CPU speed -->
  </array>
  <!-- Current when CPU is idle -->
  <item name="cpu.idle">0.1</item>
  <!-- Current at each CPU speed, as per 'cpu.speeds' -->
  <array name="cpu.active">
      <value>0.1</value>  <!-- ~100mA -->
  </array>
  <!-- This is the battery capacity in mAh (measured at nominal voltage) -->
  <item name="battery.capacity">1000</item>

  <array name="wifi.batchedscan"> <!-- mA -->
      <value>.0002</value> <!-- 1-8/hr -->
      <value>.002</value>  <!-- 9-64/hr -->
      <value>.02</value>   <!-- 65-512/hr -->
      <value>.2</value>    <!-- 513-4,096/hr -->
      <value>2</value>    <!-- 4097-/hr -->
  </array>
</device>

这个里面存储了 Cpu(cpu.speeds) 的主频等级,以及每个主频每秒消耗的毫安 (cpu.active),好,现在回到CpuPowerCalculator中,先来看构造方法

public CpuPowerCalculator(PowerProfile profile) {
        final int speedSteps = profile.getNumSpeedSteps();
        mPowerCpuNormal = new double[speedSteps];
        mSpeedStepTimes = new long[speedSteps];
        for (int p = 0; p < speedSteps; p++) {
            mPowerCpuNormal[p] = profile.getAveragePower(PowerProfile.POWER_CPU_ACTIVE, p);
        }
    }

第一步获得Cpu有几个主频等级,因为不同等级消耗的电量不一样,所以要区别对待,根据主频的个数,然后初始化mPowerCpuNormalmSpeedStepTimes,前者用来保存不同等级的耗电速度,后者用来保存在不同等级上耗时,然后给mPowerCpuNormal的每个元素附上值。构造方法就完成了其所有的工作,现在来计算方法calculateApp,

final int speedSteps = mSpeedStepTimes.length;

        long totalTimeAtSpeeds = 0;
        for (int step = 0; step < speedSteps; step++) {
            mSpeedStepTimes[step] = u.getTimeAtCpuSpeed(step, statsType);
            totalTimeAtSpeeds += mSpeedStepTimes[step];
        }
        totalTimeAtSpeeds = Math.max(totalTimeAtSpeeds, 1);

首先得到Cpu主频等级个数,然后BatteryStats.Uid得到不同主频上执行时间,计算Cpu总耗时保存在totalTimeAtSpeeds中,

app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;

Cpu的执行时间分很多部分,但是我们关注UserKernal部分,也就是上面的UserCpuTimeSystemCpuTime

double cpuPowerMaMs = 0;
        for (int step = 0; step < speedSteps; step++) {
            final double ratio = (double) mSpeedStepTimes[step] / totalTimeAtSpeeds;
            final double cpuSpeedStepPower = ratio * app.cpuTimeMs * mPowerCpuNormal[step];
            if (DEBUG && ratio != 0) {
                Log.d(TAG, "UID " + u.getUid() + ": CPU step #"
                        + step + " ratio=" + BatteryStatsHelper.makemAh(ratio) + " power="
                        + BatteryStatsHelper.makemAh(cpuSpeedStepPower / (60 * 60 * 1000)));
            }
            cpuPowerMaMs += cpuSpeedStepPower;
        }

上面的代码就是将不同主频的消耗累加到一起,但是其中值得注意的是,他并不是用各个主频的消耗时间 * 主频单位时间内消耗的电量,而是用一个 radio 变量来计算得到各个主频段执行时间占总时间的百分比,然后用cpuTimeMs来换算成各个主频的 Cpu 实际消耗时间,这比 5.0 的 API 多了这么一步,我估计是发现了计算的不严谨性,这也是Android迟迟不放出统计电量方式的原因,其实 google 自己对这块也没有把握,所以才会造成不同API计算方式的差异。好,计算完我们的总消耗后,是不是就算完事了?如果你只需要得到一个 App 的耗电总量,上面的讲解已经足够了,但是 6.0 的 API 计算了每个 App 的不同进程的耗电量,这个我们就只当看看就行,暂时没什么实际意义。

// Keep track of the package with highest drain.
        double highestDrain = 0;

        app.cpuFgTimeMs = 0;
        final ArrayMap<String, ? extends BatteryStats.Uid.Proc> processStats = u.getProcessStats();
        final int processStatsCount = processStats.size();
        for (int i = 0; i < processStatsCount; i++) {
            final BatteryStats.Uid.Proc ps = processStats.valueAt(i);
            final String processName = processStats.keyAt(i);
            app.cpuFgTimeMs += ps.getForegroundTime(statsType);

            final long costValue = ps.getUserTime(statsType) + ps.getSystemTime(statsType)
                    + ps.getForegroundTime(statsType);

            // Each App can have multiple packages and with multiple running processes.
            // Keep track of the package who's process has the highest drain.
            if (app.packageWithHighestDrain == null ||
                    app.packageWithHighestDrain.startsWith("*")) {
                highestDrain = costValue;
                app.packageWithHighestDrain = processName;
            } else if (highestDrain < costValue && !processName.startsWith("*")) {
                highestDrain = costValue;
                app.packageWithHighestDrain = processName;
            }
        }

        // Ensure that the CPU times make sense.
        if (app.cpuFgTimeMs > app.cpuTimeMs) {
            if (DEBUG && app.cpuFgTimeMs > app.cpuTimeMs + 10000) {
                Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time");
            }

            // Statistics may not have been gathered yet.
            app.cpuTimeMs = app.cpuFgTimeMs;
        }

上面统计同一App下不同的进程的耗电量,得到消耗最大的进程名,保存到BatterySipper对象中,然后得出AppCpuforeground消耗时间,将foreground时间与之前计算得到的cpuTimeMs进行比较,如果foreground时间比cpuTimeMs还要大,那么就将cpuTimeMs的时间改变为foreground的值,但是这个值的变化对之前耗电总量的计算没有丝毫影响。

// Convert the CPU power to mAh
        app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000);

最后的最后,将耗电量用 mAh 单位来表示,所以在毫秒的基础上除以60*60*1000

总结:Cpu耗电量的计算是要区分不同主频的,频率不同,单位时间内消耗的电量是有区分的,这一点要明白。还有一点就是不同主频上的执行时间不是通过BatteryStats.Uid#getTimeAtCpuSpeed方法得到的,二十是通过百分比和BatteryStats.Uid#getUserCpuTimeUsgetSystemCpuTimeUs计算得到cpuTimeMs乘积得到的。最后一点就是,cpuTimeMs时间是会在计算完毕后进行比较,比较的对象是CPUforeground时间。

WakeLock 耗电量的计算

WakelockPowerCalculator.java

从构造方法开始,

public WakelockPowerCalculator(PowerProfile profile) {
        mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
    }

首先得到power_profile.xmlcpu.awake表示的值,保存在mPowerWakelock变量中。构造方法只做了这么点事,下面进入calculateApp方法。

@Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        long wakeLockTimeUs = 0;
        final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelockStats =
                u.getWakelockStats();
        final int wakelockStatsCount = wakelockStats.size();
        for (int i = 0; i < wakelockStatsCount; i++) {
            final BatteryStats.Uid.Wakelock wakelock = wakelockStats.valueAt(i);

            // Only care about partial wake locks since full wake locks
            // are canceled when the user turns the screen off.
            BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL);
            if (timer != null) {
                wakeLockTimeUs += timer.getTotalTimeLocked(rawRealtimeUs, statsType);
            }
        }
        app.wakeLockTimeMs = wakeLockTimeUs / 1000; // convert to millis
        mTotalAppWakelockTimeMs += app.wakeLockTimeMs;

        // Add cost of holding a wake lock.
        app.wakeLockPowerMah = (app.wakeLockTimeMs * mPowerWakelock) / (1000*60*60);
        if (DEBUG && app.wakeLockPowerMah != 0) {
            Log.d(TAG, "UID " + u.getUid() + ": wake " + app.wakeLockTimeMs
                    + " power=" + BatteryStatsHelper.makemAh(app.wakeLockPowerMah));
        }
    }

首先获得Wakelock的数量,然后逐个遍历得到每个Wakelock对象,得到该对象后,得到BatteryStats.WAKE_TYPE_PARTIAL的唤醒时间,然后累加,其实wakelock有 4 种,为什么只取partial的时间,具体代码google也没解释的很清楚,只是用一句注释打发了我们。得到总时间后,就可以与构造方法中的单位时间waklock消耗电量相乘得到Wakelock消耗的总电量。

Wifi 耗电量的计算

首先来看构造方法,来了解一下 WIFI 的耗电量计算用到了power_profile.xml中的哪些属性:

public WifiPowerCalculator(PowerProfile profile) {
        mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
        mTxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX);
        mRxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX);
    }

我们去PowerProfile.java找到上面三个常量代表的属性:

public static final String POWER_WIFI_CONTROLLER_IDLE = "wifi.controller.idle";
   public static final String POWER_WIFI_CONTROLLER_RX = "wifi.controller.rx";
   public static final String POWER_WIFI_CONTROLLER_TX = "wifi.controller.tx";

知道对应的 xml 的属性后我们直接看calculateApp方法:

@Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        final long idleTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_IDLE_TIME,
                statsType);
        final long txTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_TX_TIME, statsType);
        final long rxTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_RX_TIME, statsType);
        app.wifiRunningTimeMs = idleTime + rxTime + txTime;
        app.wifiPowerMah =
                ((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
                / (1000*60*60);
        mTotalAppPowerDrain += app.wifiPowerMah;

        app.wifiRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_RX_DATA,
                statsType);
        app.wifiTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_TX_DATA,
                statsType);
        app.wifiRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_RX_DATA,
                statsType);
        app.wifiTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_TX_DATA,
                statsType);

        if (DEBUG && app.wifiPowerMah != 0) {
            Log.d(TAG, "UID " + u.getUid() + ": idle=" + idleTime + "ms rx=" + rxTime + "ms tx=" +
                    txTime + "ms power=" + BatteryStatsHelper.makemAh(app.wifiPowerMah));
        }
    }

这里的计算方式也是差不多,先根据 Uid 得到时间,然后乘以构造方法里对应的 wifi 类型单位时间内消耗电量值,没什么难点,就不一一分析,需要注意的是,这里面还计算了wifi传输的数据包的数量和字节数。

蓝牙耗电量的计算

蓝牙关注的power_profile.xml中的属性如下:

public static final String POWER_BLUETOOTH_CONTROLLER_IDLE = "bluetooth.controller.idle";
   public static final String POWER_BLUETOOTH_CONTROLLER_RX = "bluetooth.controller.rx";
   public static final String POWER_BLUETOOTH_CONTROLLER_TX = "bluetooth.controller.tx";

但是还没有单独为 App 计算耗电量的,所以这个地方是空的。

@Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        // No per-app distribution yet.
    }

摄像头耗电量的计算

CameraPowerCalculator.java

摄像头的耗电量关注的是power_profile.xmlcamera.avg属性代表的值,保存到mCameraPowerOnAvg,

public static final String POWER_CAMERA = "camera.avg";

计算方式如下:

@Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {

        // Calculate camera power usage.  Right now, this is a (very) rough estimate based on the
        // average power usage for a typical camera application.
        final BatteryStats.Timer timer = u.getCameraTurnedOnTimer();
        if (timer != null) {
            final long totalTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
            app.cameraTimeMs = totalTime;
            app.cameraPowerMah = (totalTime * mCameraPowerOnAvg) / (1000*60*60);
        } else {
            app.cameraTimeMs = 0;
            app.cameraPowerMah = 0;
        }
    }

先计算摄像头打开的时间totalTime,然后根据这个值乘以mCameraPowerOnAvg得到摄像头的耗电量。

手电筒耗电量的计算

FlashlightPowerCalculator.java

public static final String POWER_FLASHLIGHT = "camera.flashlight";

跟摄像头类似,也是先得到时间,然后乘积,不想说了,没意思。

无线电耗电量的计算

MobileRadioPowerCalculator.java

关注的是power_profile.xml中如下三个属性:

/**
     * Power consumption when screen is on, not including the backlight power.
     */
    public static final String POWER_SCREEN_ON = "screen.on";

    /**
     * Power consumption when cell radio is on but not on a call.
     */
    public static final String POWER_RADIO_ON = "radio.on";

    /**
     * Power consumption when cell radio is hunting for a signal.
     */
    public static final String POWER_RADIO_SCANNING = "radio.scanning";

当无限量连接上时,根据信号强度不同,耗电量的计算是有区别的,所以在构造方法,当无线电的状态为 on 时,是要特殊处理的,其他两个状态 (active 和 scan) 就正常取值就可以了。

/**
     * Power consumption when screen is on, not including the backlight power.
     */
    public static final String POWER_SCREEN_ON = "screen.on";

    /**
     * Power consumption when cell radio is on but not on a call.
     */
    public static final String POWER_RADIO_ON = "radio.on";

    /**
     * Power consumption when cell radio is hunting for a signal.
     */
    public static final String POWER_RADIO_SCANNING = "radio.scanning";

计算的方式分两种,以无线电处于active状态的次数为区分,当active大于 0,我们用处于active状态的时间来乘以它的单位耗时。另一种情况就要根据网络转化的数据包来计算耗电量了。

传感器耗电量的计算

SensorPowerCalculator.java

只关注一个属性:

public static final String POWER_GPS_ON = "gps.on";

计算方式如下:

@Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        // Process Sensor usage
        final SparseArray<? extends BatteryStats.Uid.Sensor> sensorStats = u.getSensorStats();
        final int NSE = sensorStats.size();
        for (int ise = 0; ise < NSE; ise++) {
            final BatteryStats.Uid.Sensor sensor = sensorStats.valueAt(ise);
            final int sensorHandle = sensorStats.keyAt(ise);
            final BatteryStats.Timer timer = sensor.getSensorTime();
            final long sensorTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
            switch (sensorHandle) {
                case BatteryStats.Uid.Sensor.GPS:
                    app.gpsTimeMs = sensorTime;
                    app.gpsPowerMah = (app.gpsTimeMs * mGpsPowerOn) / (1000*60*60);
                    break;
                default:
                    final int sensorsCount = mSensors.size();
                    for (int i = 0; i < sensorsCount; i++) {
                        final Sensor s = mSensors.get(i);
                        if (s.getHandle() == sensorHandle) {
                            app.sensorPowerMah += (sensorTime * s.getPower()) / (1000*60*60);
                            break;
                        }
                    }
                    break;
            }
        }
    }

当传感器的类型为 GPS 时,我们计算每个传感器的时间然后乘以耗电量,和所有的耗电量计算都是一样,不同的是,当传感器不是 GPS 时,这个时候计算就根据SensorManager得到所有传感器类型,这个里面保存有不同传感器的单位耗电量,这样就能计算不同传感器的耗电量。

总结

至此我已经把 APP 耗电量的计算讲完了 (还有硬件的没讲),前后花费 3 天时间,好痛苦 (此处一万只草泥马),不过好在自己也算对这个耗电量的理解有了一定的认识。google 官方对耗电量的统计给出的解释都是不能代表真实数据,只能作为参考值,因为受power_profile.xml的干扰太大,如果手机厂商没有严格设置这个文件,那可想而知出来的值可能是不合理的。

提示

腾讯的 GT 团队前几天推出了耗电量的计算 APK,原理是一样的,大家可以试用下GT

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 30 条回复 时间 点赞

非常不错,收藏起来慢慢学习!

干货牛 B,大师又升级了哦

🐂逼!

膜拜大师

忘记了,其实早上我想说,你这个文章可以把 GT 怎么用写一下。GT 很难用啊。

#7 楼 @lihuazhang 耗电量是一个单独的 APK,可能他也知道 GT 很难用了

:plus1:

我了解到的信息是 gt 基本只支持 google 出厂的手机

匿名 #11 · 2015年10月21日

#8 楼 @doctorq 其实先有 PowerStat 然后 GT 把 PowerStat 整合进去的

易寒 #12 · 2015年10月21日 Author

#10 楼 @kasi 我用的小米,可以用

易寒 #13 · 2015年10月21日 Author

#11 楼 @kendydrm 内部的事不清楚,只知道 powerstat 刚退出来~~

有没有计算耗电量和接近理想状态下的实际耗电量的比较?

易寒 #15 · 2015年10月27日 Author

#14 楼 @tbya adb shell dumpsys batterystats 命令行下给了一个实际的范围值,可以看出来计算值和实际值的差值。

#15 楼 @doctorq 这个数字只有对比了才有意义。否则给老板看毫安时,感觉在做物理题

不错 大赞

收藏了

兄弟,耗电量 api 开放,是 android6.0 才开放的吗?

易寒 #21 · 2016年01月25日 Author

#20 楼 @oscar 现在也没开放,只是你可以通过系统权限去调用,GT 就是这么做的。

我想问下,第三方 apk 能不能利用调用这个 API?

易寒 #24 · 2016年10月10日 Author

#23 楼 @jira 不针对 app,针对系统,gt 就是第三方的 app,但是开发的时候有一些权限的东西

#24 楼 @DoctorQ 是指需要调用隐藏 API 吗,我尝试过,但是编译会失败,还未找到原因

易寒 #26 · 2016年10月10日 Author

#25 楼 @jira 不知道现在的 android 版本有没有开放出来,可以看一下 GT 如何操作的。GT&Powerstat

#26 楼 @DoctorQ GT 分离出来的电量工具只在小米手机上能用,其它都是会闪退,我想看下源码,有没有 PowerStat For Android4.x: PowerStat_For4.x.apk 的源码?您刚发的是 GT 的源码吧

#8 楼 @DoctorQ 小米 miui V8 按照 PowerStat 用户手册操作,出现如下图信息,请问知道是怎么回事吗?

匿名 #29 · 2017年02月07日

#28 楼 @kuroky 要按照 Powerstat 的要求,将其作为系统应用运行才行的。

恒温 感谢 fir.im —— 结果公布 中提及了此贴 06月05日 13:20

mark,大赞,很详细

codeskyblue [該主題已被刪除] 中提及了此贴 12月09日 16:17
codeskyblue 通过电池电量评测安卓 App 的耗电水平 中提及了此贴 12月09日 19:39
需要 登录 後方可回應,如果你還沒有帳號按這裡 注册