Android 系统电量统计

本文主要通过跟踪 Android 设置 APP 的源码分析如何统计手机各个软件的电量。

·PowerProfile 简介

PowerProfile 这个类对手机的各个子系统运行时的平均电流(mA)和基本状态做了初步统计,所有信息都存放在 sPowerMap 这个 Map 里,这个 Map 实际是从 power_profile.xml 文件中读取的,PowerProfile 提供接口如 getAveragePower(String type) 之类,供我们去取该XML里的各项的value,是个很短的类,也很容易理解。

PowerProfile 是 android 的 internal 类,默认是 hidden 的,即我们第三方应用只能通过反射调、或者自己做一个完整的 android.jar 的 jar 包,或者源码下编译。对于如何做自制的android 完整 sdk 包(包含internal类)请参考https://devmaze.wordpress.com/2011/01/18/using-com-android-internal-part-1-introduction/这一系列文章。本文分析的源码的环境均为 android-14 即 android4.0。

·PowerProfile读取基本电量状态的具体流程

  1. PowerProfile 类构造函数
    clipboard.png

    如上图所示为 PowerProfiles 类的构造函数,且只有这一种构造方式,从构造函数就可以看出这个类的作用,即从XML中读取电量值,并把结果放入 sPowerMap 中。(左边的行号为实际源码的行号,以后的截图也是)。

  2. 转到这个 readPowerValuesFromXml 函数
    clipboard1.png

    如上图所示为 readPowerValuesFromXml 函数的开头,可以看出是从一个系统文件 power_profile.xml 中读取数据,这个函数就是通过引入一个叫 XmlUtils 的 internal 类来完成的(其实就是封装了 beginDocument 和 nextElement 这样控制读取游标的函数)。

  3. 那么 power_profile.xml 是个什么样的文件呢?从 AndroidXRef的4.0.3源码中查找这个文件,发现有五个:
    clipboard2.png

    从路径可以大体看出,不同的手机厂商可以会有不同的 power_profile.xml(这很好理解,手机耗电直接与硬件挂钩,硬件具体的耗电如何也就是只有厂商自己能够弄清楚了),而这个 /frameworks/base/core/res/res/xml/power_profile.xml(点击可以查看),就是一个大体的格式了。
    这里面的一些标签对应到 PowerProfile 里常量对应关系和具体解释如下:

    POWER_NONE = "none"
    即没有功耗(power consumption),一般是0

    POWER_SCREEN_ON = "screen.on";
    屏幕亮的时候的功耗(不考虑背光的电量)

    POWER_BLUETOOTH_ACTIVE = "bluetooth.active";
    蓝牙驱动在发送/接收数据时候的功耗

    POWER_BLUETOOTH_ON = "bluetooth.on"
    蓝牙为开启状态时的能耗

    POWER_SCREEN_FULL = "screen.full"
    背光为最亮的时候的能耗,不是最亮的时候耗电为线性关系,即 50% 亮度时这个值应该乘以 0.5

    POWER_WIFI_ON = "wifi.on"
    WiFi 为开启状态时的能耗

    POWER_WIFI_ACTIVE = "wifi.active"
    WiFi 驱动在发送/接收数据时的能耗

    POWER_WIFI_SCAN = "wifi.scan"
    WiFi 在扫描可用网络时的功耗

    POWER_AUDIO = "dsp.audio"
    播放后台音频内容时音频硬件产生的功耗,除了 CPU 外,也可能由 DSP 或 amplifier 引起(数字信号处理相关的硬件)

    POWER_VIDEO = "dsp.video"
    播放后台多媒体内容时的多媒体硬件产生的功耗,除了 CPU 外,也可能由 DSP 引起

    POWER_RADIO_ACTIVE = "radio.active"
    接通电话状态下的功耗

    POWER_RADIO_SCANNING = "radio.scanning"
    移动网络(cell radio 可以翻译为蜂窝移动网络) 在搜寻信号的时候的功耗

    POWER_GPS_ON = "gps.on"
    GPS 为打开状态时的功耗

    POWER_RADIO_ON = "radio.on"
    移动网络为开启时但不是在打电话时的功耗,数组,对应不同的信号强度

    POWER_CPU_SPEEDS = "cpu.speeds"
    CPU 的几种频率,数组,支持变频的手机会有多个,400000 即 400MHz

    POWER_CPU_IDLE = "cpu.idle"
    PowerProfile.java 里的注释为“电池处于休眠模式(power collapse mode)时的 CPU 功耗”,power_profile.xml 里的注释为“cpu 挂起时的功耗”,我认为可以理解成 idle 进程 cpu 功耗。

    POWER_CPU_AWAKE = "cpu.awake"
    CPU wake lock 被持有时的功耗,This should be 0 on devices that can go into full CPU power collapse even when a wake lock is held.

    POWER_CPU_ACTIVE = "cpu.active"
    CPU 在不同频率下的功耗

  4. 解析 power_profile.xml 这个文件的数据放在了 sPowerMap 中,并对外提供了四个接口:getAveragePower(String type) 返回指定 type 的功耗,即 power_profile.xml 中对应标签的值,单位为毫安(milliAmps);getAveragePower(String type, int level) 返回指定 type 的数组类型的下标为 level 的值,单位为毫安;getBatteryCapacity() 返回电池容量;getNumSpeedSteps() 返回 "cpu.speeds" 对应的值的数量,即 CPU 可以变频的个数。

·电量统计源码

  1. 电量统计的源码路径在 /packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java
    clipboard3.png

    上图代码段为 onCreate() 获取了 batteryinfo 服务的接口:IBatteryStats

    clipboard4.png

    上图代码段为 onResume() 中 执行refreshStats(),并注册了电池变化的reciever(有变化时刷新)

    clipboard5.png

    上图代码段为 refreshStats() 函数执行了 load 方法

    clipboard6.png

    上图代码段为在 load() 中拿到了电池相关的统计数据(其实就是解析的 batterystats.bin 文件,该文件的说明参考http://jingyan.baidu.com/article/39810a23c090bcb636fda6a6.html),数据为 BatteryStatsImpl 类型,该类比较冗长复杂,我们通过系统电量 apk 的源码来看如何去利用该类的信息。

  2. refreshStats() 基本流程分两步:processAppUsage() 与 processMiscUsage(),分别是统计软件电量与硬件电量,我们这里只看软件电量,也是我们最关心的。

    BatteryStatsImpl 提供的接口基本都是次数和时间,配合 PowerUsage 来计算电量,控制统计时机的有个叫 whitch 的参数贯穿统计过程,它有四种取值:BatteryStats.STATS_SINCE_CHARGED=0 表示统计所有数据,包括以前的数据;BatteryStats.STATS_LAST=1 表示只统计上次的数据、BatteryStats.STATS_CURRENT=2 表示只统计当前这次的数据、BatteryStats.STATS_SINCE_UNPLUGGED=3 表示只统计自从上次拔掉设备之后的数据。

    1. 所有的软件电量数据统计单位为 uid,通过 getUidStats 获取所有软件电量数据:

      clipboard7.png

      (在 android 里,一个 Uid下 可能有来自多个应用的进程甚至是 /system/bin 下的程序,如具有相同签名并设置了 shareduid 的三方应用,特别是一些系统应用,这就导致了电量统计粒度无法非常准确的定位到某一个应用 - 包名)

    2. 接下来遍历该 Map 计算每个 Uid 所耗的电量

      clipboard8.png

      调用 Uid 的 getProcessStats 方法,能够得到该 Uid 下每个进程的 CPU 使用情况,接下来继续遍历这个 processStats:
      clipboard9.png

      通过439行的那句Log可以看出该 Map 的 key 是进程名,其实这样我们可以通过进程名反查其所属包名来统计各个应用的具体情况(但某些情况下,如 shareduid 相同并指定组件的进程名相同时,多个应用也可以共享一个进程,可以简单参考http://mypyg.iteye.com/blog/720406的结论),其中 userTime 和 systemTime 分别为该进程的用户态时间和内核态时间,foregroundTime 是前台运行时间,通过注释可以看出他们三个的单位均为 1/100 秒(但是 getForegroundTime 的方法注释标明返回为微秒,这很奇怪,不过鉴于系统的电量APK都这么做了,那就可以简单的认为方法注释写错了),接下来的 446 - 458 行其实就是按各个 CPU 频率的时间比来统计平均电量(这里的电量计算公式为 功耗(电流mA) * 时间,即 I * t,无法理解,因为从初中就学到电量损耗单位是J,公式为 W = U * I * t = I * I * R * t,其中 U 为 电压,R 为电阻),packageWithHighestDrain 是记录了该 Uid 下的耗电最多的呢个进程名。该 Uid 下的所有进程的 CPU 时间和前台时间都累加起来,分别为 cpuTime 和 cpuFgTime,单位为毫秒。跳出这个遍历 Map 就又出现了一段异常代码:

      clipboard10.png

      如果前台时间大于 CPU 时间,那么强制的把 CPU 时间置为前台时间,至于为什么这么做,以及能不能把前台时间强制置为 CPU 时间,都是未知数,均是电量统计误差导致的。不同的三方应用应该也有自己的矫正计算方法。

    3. 然后是计算 CPU wake lock 持有的电量消耗,通过 Uid.getWakelockStats() 得到:
      clipboard11.png

      在这里他只是考虑了 WAKE_TYPE_PARTIAL 的情况,注释说明因为 WAKE__TYPE_FULL 的 wake lock 在关屏后就会呗取消,所以只需要关心 partial wake lock,不明觉厉 ... 相同的,通过 getAceragePower 得到对应功耗,乘以时间即耗电量。注意下,这个 Map 里的 Key 这是个耗电者的说明,既不是包名也不是进程名(再次导致统计粒度加粗)。

    4. 然后是计算 TCP 数据传输的功耗,首先 getAverageDataCost 函数计算了数据传输的平均耗电,即每传送 1B 数据所耗电量:

      clipboard12.png

      可以看出它包含了 wifi、移动网络的因素,粗略得算了下平均耗电,然后通过 Uid.getTcpBytesReceived 和 Uid.getTcpBytesSent 来获取 TCP 数据传输大小,相乘即得到耗电量:
      clipboard13.png

    5. 接下来是保持 WiFI 运行的电量损耗,通过 Uid.getWifiRunningTime() 得到 WiFi 占有时间:
      clipboard14.png

      与前面计算 CPU 耗电、CPU wake lock 耗电一样,WiFi开启状态时的功耗电流 * 开启的时间。

    6. 系统电量应用统计的软件电量最后一项,即软件的传感器耗电,通过 Uid.getSensorStats() 得到:
      clipboard15.png

      可以看出,GPS 耗电时间能够单独统计出,其他的不能,其实 power_profile.xml 里的感应器也只有 GPS 的功耗,Uid.Sensor 中也只有 GPS 这一个常量。

    7. 至此为止,系统电量应用的软件统计代码已经完了,剩下的是归纳和计算百分比。每个 Uid / 硬件电量基本统计结果存为一个个的BatterySipper,接下来通过判断 Uid 是否为第三方应用来将刚才的统计归类:
      clipboard16.png

标签: none

仅有一条评论

  1. 黑水 黑水

    可能是考虑到电池电压变化不大,当作电压不变处理了,算比例最后会被约分掉。

添加新评论