Android 功耗统计的核心函数是文件BatteryStatsHelper.java中的refreshStats函数,此函数会调用processAppUsage函数和processMiscUsage函数分别计算APP功耗和系统硬件功耗。下面将详细介绍如何计算APP功耗,系统硬件功耗与APP功耗的计算方法相似,就不再介绍了。
在processAppUsage函数中,分别调用了如下函数:
Android 功耗统计的核心函数是文件BatteryStatsHelper.java中的refreshStats函数,此函数会调用processAppUsage函数和processMiscUsage函数分别计算APP功耗和系统硬件功耗。下面将详细介绍如何计算APP功耗,系统硬件功耗与APP功耗的计算方法相似,就不再介绍了。
在processAppUsage函数中,分别调用了如下函数:
1. mCpuPowerCalculator.calculateApp //CPU功耗
2. mWakelockPowerCalculator.calculateApp //持有wakelock锁时的功耗
3. mMobileRadioPowerCalculator.calculateApp//CP(PHONE)模块的功耗
4. mWifiPowerCalculator.calculateApp //WIFI模块的功耗
5. mBluetoothPowerCalculator.calculateApp //蓝牙模块的功耗
6. mSensorPowerCalculator.calculateApp //传感器模块的功耗
7. mCameraPowerCalculator.calculateApp //相机模块的功耗
8. mFlashlightPowerCalculator.calculateApp //闪光灯模块的功耗
各模块功耗的计算方法基本类似,分为如下三步:
1. 从power_profile.xml获取对应模块不同工作模式下的电流;
2. 获取模块在对应工作模式下工作的时间;
3. 将不同模式下的电流乘以对应的时间再求和得到整个模块的功耗。
接下来,我们将以CPU功耗计算为例来进行分析
对CPU模块来,其calculateApp函数在CpuPowerCalculator.java文件中实现,具体代码如下:
public class CpuPowerCalculator extends PowerCalculator {
private static final String TAG = "CpuPowerCalculator";
private static final boolean DEBUG = BatteryStatsHelper.DEBUG;
private final PowerProfile mProfile;
public CpuPowerCalculator(PowerProfile profile) {
mProfile = profile;
}
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;
// Aggregate total time spent on each cluster.
long totalTime = 0;
final int numClusters = mProfile.getNumCpuClusters();
for (int cluster = 0; cluster < numClusters; cluster++) {
final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster);
for (int speed = 0; speed < speedsForCluster; speed++) {
totalTime += u.getTimeAtCpuSpeed(cluster, speed, statsType);
}
}
totalTime = Math.max(totalTime, 1);
double cpuPowerMaMs = 0;
for (int cluster = 0; cluster < numClusters; cluster++) {
final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster);
for (int speed = 0; speed < speedsForCluster; speed++) {
final double ratio = (double) u.getTimeAtCpuSpeed(cluster, speed, statsType) /
totalTime;
final double cpuSpeedStepPower = ratio * app.cpuTimeMs *
mProfile.getAveragePowerForCpu(cluster, speed);
if (DEBUG && ratio != 0) {
Log.d(TAG, "UID " + u.getUid() + ": CPU cluster #" + cluster + " step #"
+ speed + " ratio=" + BatteryStatsHelper.makemAh(ratio) + " power="
+ BatteryStatsHelper.makemAh(cpuSpeedStepPower / (60 * 60 * 1000)));
}
cpuPowerMaMs += cpuSpeedStepPower;
}
}
app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000);
if (DEBUG && (app.cpuTimeMs != 0 || app.cpuPowerMah != 0)) {
Log.d(TAG, "UID " + u.getUid() + ": CPU time=" + app.cpuTimeMs + " ms power="
+ BatteryStatsHelper.makemAh(app.cpuPowerMah));
}
// 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;
}
}
}
在分析代码之前,有一些和CPU架构(ARM)相关的知识需要简单的介绍一下,目前主流的Android手机,基本都是SMP系统,也就是多核系统,咱们就以一个8核系统为例。而ARM中又有big core 和litter core的概念,所谓big core其实就是高性能的CPU,相应的litter core就是性能相对差一些的CPU,为了便于管理,ARM提出了cluster的概念,例如咱们可以将8个core分为两个cluster,第一个cluster管理性能低一些的CPU,第二个cluster管理高性能的CPU,至于每个cluster包含多少个CPU,可从相应的datasheet中获取。另外每个cluster中的CPU可以工作在不同的频率下,而不同频率下的电流是不同的,所以在power_profile.xml文件中需要指明不同频点下的电流值。
言归正传,从代码可以看出,从power_profile.xml文件中读取不同频率下的电流值采用如下接口:
mProfile.getAveragePowerForCpu(cluster, speed);
其中cluster是指明目前获取的是哪一个cluster的电流,speed指明是哪一个频点(频率)。
可以看到,在各个频点下功耗的计算,并不是简单的获取各个频点下的时间乘以对应的电流,而是先计算频点的时间再整个总频点时间的比率,然后再乘以app实际的工作时间得到一个比较精确的特定频点下的工作时间,Android选择这样做应该也是为了提高数据的可靠性吧。
app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;
final double ratio = (double) u.getTimeAtCpuSpeed(cluster, speed, statsType) / totalTime;
final double cpuSpeedStepPower = ratio * app.cpuTimeMs *
mProfile.getAveragePowerForCpu(cluster, speed);
关于getTimeAtCpuSpeed接口的实现,比较复杂,限于篇幅,本文就不分析了。不过最终获取的CPU在频点的工作时间,还是通过kernel的节点获取的,实现在文件KernelCpuSpeedReader.java中,代码如下:
public class KernelCpuSpeedReader {
private static final String TAG = "KernelCpuSpeedReader";
private final String mProcFile;
private final long[] mLastSpeedTimes;
private final long[] mDeltaSpeedTimes;
// How long a CPU jiffy is in milliseconds.
private final long mJiffyMillis;
/**
* @param cpuNumber The cpu (cpu0, cpu1, etc) whose state to read.
*/
public KernelCpuSpeedReader(int cpuNumber, int numSpeedSteps) {
mProcFile = String.format("/sys/devices/system/cpu/cpu%d/cpufreq/stats/time_in_state",
cpuNumber);
mLastSpeedTimes = new long[numSpeedSteps];
mDeltaSpeedTimes = new long[numSpeedSteps];
long jiffyHz = Libcore.os.sysconf(OsConstants._SC_CLK_TCK);
mJiffyMillis = 1000/jiffyHz;
}
/**
* The returned array is modified in subsequent calls to {@link #readDelta}.
* @return The time (in milliseconds) spent at different cpu speeds since the last call to
* {@link #readDelta}.
*/
public long[] readDelta() {
StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
try (BufferedReader reader = new BufferedReader(new FileReader(mProcFile))) {
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' ');
String line;
int speedIndex = 0;
while (speedIndex < mLastSpeedTimes.length && (line = reader.readLine()) != null) {
splitter.setString(line);
Long.parseLong(splitter.next());
long time = Long.parseLong(splitter.next()) * mJiffyMillis;
if (time < mLastSpeedTimes[speedIndex]) {
// The stats reset when the cpu hotplugged. That means that the time
// we read is offset from 0, so the time is the delta.
mDeltaSpeedTimes[speedIndex] = time;
} else {
mDeltaSpeedTimes[speedIndex] = time - mLastSpeedTimes[speedIndex];
}
mLastSpeedTimes[speedIndex] = time;
speedIndex++;
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read cpu-freq: " + e.getMessage());
Arrays.fill(mDeltaSpeedTimes, 0);
} finally {
StrictMode.setThreadPolicy(policy);
}
return mDeltaSpeedTimes;
}
}
可以看到,获取CPU在各个频点下的工作时间,是通过读取”/sys/devices/system/cpu/cpu%d/cpufreq/stats/time_in_state”文件获得的。