耗电量API
Android系统中很早就有耗电量的API,只不过1直都是隐藏的,Android系统的设置-电池功能就是调用的这个API,该API的核心部份是调用了com.android.internal.os.BatteryStatsHelper类,利用PowerProfile类,读取power_profile.xml文件,我们1起来看看具体如何计算耗电量,首先从最新版本6.0开始看
6.0的API
源码
BatteryStatsHelper
其中计算耗电量的方法为490行的processAppUsage,下来1步1步来解释该方法。
App耗电量的计算探究
private void processAppUsage(SparseArrayasUsers) {
方法的参数是1个SparseArray数组,存储的对象是UserHandle,官方文档给出的解释是,代表1个用户,可以理解为这个类里面存储了用户的相干信息.
final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);
然后判断该次计算是不是针对所有用户,通过UserHandle的USER_ALL值来判断,该值为⑴,源码的地址在https://github.com/DoctorQ/platform_frameworks_base/blob/android⑹.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 uidStats = mStats.getUidStats(); final int NU = uidStats.size();
首先创建1个BatterySipper对象osSipper,该对象里面可以存储1些后续我们要计算的值,然后通过BatteryStats类对象mStats来得到1个包括Uid的对象的SparseArray组数,然后计算了1下这个数组的大小,保存在变量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可计算的类型有3种:利用, 系统服务, 硬件类型,所以这个地方传入的是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,代表了我们的计算规则是从上次充满电后数据,还有1种规则是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
这1部份我1会单独拿出来挨个解释,现在我们还是回到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版本打印信息,这个没啥可说的,然后会把刚才计算的电量值添加到列表中:
if (totalPower != 0 || u.getUid() == 0) { 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) { Listlist = 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或蓝牙时,添加到下面对应的列表中,1般情况下正常的利用我们直接保存到下面的mUsageList中就行就行,但是也有1些例外:
/**
* List of apps using power.
*/ private final ListmUsageList = new ArrayList<>(); /**
* List of apps using wifi power.
*/ private final ListmWifiSippers = new ArrayList<>(); /**
* List of apps using bluetooth power.
*/ private final ListmBluetoothSippers = new ArrayList<>();
如果我们的系统是单用户系统,且当前的userId号不在我们的统计范围内,且其进程id号是大于Process.FIRST_APPLICATION_UID(10000,系统分配给普通利用的其实id号),我们就要将其寄存到mUserSippers数组中,定义以下:
private final SparseArraymUserSippers = new SparseArray<>();
最后判断uid为0的话,代表是Android操作系统的耗电量,赋值给osSipper(494行定义)就能够了,这样1个app的计算就完成了,遍历部份就不说了,保存这个osSipper是为了最后1步计算:
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"> <item name="none">0item> <item name="screen.on">0.1item> <item name="screen.full">0.1item> <item name="bluetooth.active">0.1item> <item name="bluetooth.on">0.1item> <item name="wifi.on">0.1item> <item name="wifi.active">0.1item> <item name="wifi.scan">0.1item> <item name="dsp.audio">0.1item> <item name="dsp.video">0.1item> <item name="camera.flashlight">0.1item> <item name="camera.avg">0.1item> <item name="radio.active">0.1item> <item name="radio.scanning">0.1item> <item name="gps.on">0.1item> <array name="radio.on"> <value>0.2value> <value>0.1value> array> <array name="cpu.speeds"> <value>400000value> array> <item name="cpu.idle">0.1item> <array name="cpu.active"> <value>0.1value> array> <item name="battery.capacity">1000item> <array name="wifi.batchedscan"> <value>.0002value> <value>.002value> <value>.02value> <value>.2value> <value>2value> 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); } }
第1步取得Cpu有几个主频等级,由于不同等级消耗的电量不1样,所以要区分对待,根据主频的个数,然后初始化mPowerCpuNormal和mSpeedStepTimes,前者用来保存不同等级的耗电速度,后者用来保存在不同等级上耗时,然后给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的履行时间分很多部份,但是我们关注User和Kernal部份,也就是上面的UserCpuTime和SystemCpuTime。
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;
}
上面的代码就是将不同主频的消耗累加到1起,但是其中值得注意的是,他其实不是用各个主频的消耗时间*主频单位时间内消耗的电量,而是用1个radio变量来计算得到各个主频段履行时间占总时间的百分比,然后用cpuTimeMs来换算成各个主频的Cpu实际消耗时间,这比5.0的API多了这么1步,我估计是发现了计算的不严谨性,这也是Android迟迟不放出统计电量方式的缘由,其实google自己对这块也没有掌控,所以才会造成不同API计算方式的差异。好,计算完我们的总消耗后,是否是就算完事了?如果你只需要得到1个App的耗电总量,上面的讲授已足够了,但是6.0的API计算了每一个App的不同进程的耗电量,这个我们就只当看看就行,暂时没甚么实际意义。
double highestDrain = 0;
app.cpuFgTimeMs = 0; final ArrayMap 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); if (app.packageWithHighestDrain == null ||
app.packageWithHighestDrain.startsWith("*")) {
highestDrain = costValue;
app.packageWithHighestDrain = processName;
} else if (highestDrain < costValue && !processName.startsWith("*")) {
highestDrain = costValue;
app.packageWithHighestDrain = processName;
}
} 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");
} app.cpuTimeMs = app.cpuFgTimeMs;
}
上面统计同1App下不同的进程的耗电量,得到消耗最大的进程名,保存到BatterySipper对象中,然后得出App的Cpu的foreground消耗时间,将foreground时间与之前计算得到的cpuTimeMs进行比较,如果foreground时间比cpuTimeMs还要大,那末就将cpuTimeMs的时间改变成foreground的值,但是这个值的变化对之前耗电总量的计算没有丝毫影响。
// Convert the CPU power to mAh
app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000);
最后的最后,将耗电量用mAh单位来表示,所以在毫秒的基础上除以60*60*1000。
总结:Cpu耗电量的计算是要辨别不同主频的,频率不同,单位时间内消耗的电量是有辨别的,这1点要明白。还有1点就是不同主频上的履行时间不是通过BatteryStats.Uid#getTimeAtCpuSpeed方法得到的,210是通过百分比和BatteryStats.Uid#getUserCpuTimeUs和getSystemCpuTimeUs计算得到cpuTimeMs乘积得到的。最后1点就是,cpuTimeMs时间是会在计算终了落后行比较,比较的对象是CPU的foreground时间。
WakeLock耗电量的计算
WakelockPowerCalculator.java
从构造方法开始,
public WakelockPowerCalculator(PowerProfile profile) {
mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
}
首先得到power_profile.xml中cpu.awake表示的值,保存在mPowerWakelock变量中。构造方法只做了这么点事,下面进入calculateApp方法。
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
long wakeLockTimeUs = 0 final ArrayMap.Uid.Wakelock> wakelockStats =
u.getWakelockStats() final int wakelockStatsCount = wakelockStats.size() for (int i = 0 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 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也没解释的很清楚,只是用1句注释打发了我们。得到总时间后,就能够与构造方法中的单位时间waklock消耗电量相乘得到Wakelock消耗的总电量。
Wifi耗电量的计算
首先来看构造方法,来了解1下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找到上面3个常量代表的属性:
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) { }
摄像头耗电量的计算
CameraPowerCalculator.java
摄像头的耗电量关注的是power_profile.xml中camera.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) { 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中以下3个属性:
/**
* 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
只关注1个属性:
public static final String POWER_GPS_ON = "gps.on";
计算方式以下:
@Override public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs, long rawUptimeUs, int statsType) { final SparseArray 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时,我们计算每一个传感器的时间然后乘以耗电量,和所有的耗电量计算都是1样,不同的是,当传感器不是GPS时,这个时候计算就根据SensorManager得到所有传感器类型,这个里面保存有不同传感器的单位耗电量,这样就可以计算不同传感器的耗电量。
总结
至此我已把App耗电量的计算讲完了(还有硬件),前后花费3天时间,好痛苦(此处1万只草泥马),不过好在自己也算对这个耗电量的理解有了1定的认识。google官方对耗电量的统计给出的解释都是不能代表真实数据,只能作为参考值,由于受power_profile.xml的干扰太大,如果手机厂商没有严格设置这个文件,那可想而知出来的值多是不公道的。
提示
腾讯的GT团队头几天推出了耗电量的计算APK,原理是1样的,大家可以试用下GT