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”文件获得的。