前言
电池电量耗费的源头实在太多,基本Android 设备上任何一个活动都会引起电池电量的消耗。
目前部分手机有 耗电排行的功能, 能显示出App耗电详情排行。虽然谷歌开放sdk 中并没有公开电量统计的API 或者文档,但因为安全中心->省电优化→耗电排行 中就是通过app 能显示出耗电详情排行,所以虽然未公开API但实则有相关的耗电API。耗电名单在主要记录在BatterySipper里面(在frameworks/base/core 下)
概要
我们平常说的手机耗电量,一般涵盖两个方面:硬件层面的功耗和软件层面的电量。
手机有很多硬件模块:CPU,蓝牙,GPS,显示屏,Wifi,射频(Cellular Radio)等,在手机使用过程中,这些硬件模块可能处于不同的状态,譬如Wifi打开或关闭,屏幕是亮还是暗,CPU运行或休眠。 硬件模块在不同的状态下的耗电量是不同的。Android在进行电量统计时,并不是采用直接记录电流消耗量的方式,而是跟踪硬件模块在不同状态下的使用时间,收集一些可用信息,用来近似的计算出电池消耗量。
应用程序的耗电量由很多部分组成,可能使用了GPS,蓝牙等模块,可能应用程序要求长时间亮屏(譬如游戏、视频类应用)。 一个应用程序的电量统计,可以采用累计应用程序使用所有硬件模块时间这种方式近似计算出来。
举一个例子,假定某个APK的使用了GPS,使用时间用 t 表示。GPS模块单位时间的耗电量用 w 表示,那么,这个APK使用GPS的耗电量就可以按照如下方式计算:
耗电量 = 单位时间耗电量(w) × 使用时间(t)
电量统计服务的代码逻辑涉及到以下android源码:
frameworks/base/services/java/com/android/server/SystemServer.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java
frameworks/base/core/java/android/os/BatteryStats.java
frameworks/base/core/java/com/android/internal/os/BatteryStatsImpl.java
frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java
frameworks/base/core/res/res/xml/power_profile.xml
本文介绍的电量统计的原理,并不涉及到硬件层面的功耗设计,仅从软件层面围绕以下几个问题进行分析:
Android如何启动电量统计服务?
电量统计涉及到哪一些硬件模块?
如何计算一个应用程序的耗电量?
电量统计需要完成哪些具体工作?
耗电分类
系统中将耗电总共分成了五大类:App,Wifi,Bluetooth ,User,Mobile。
电量计算大体可以分为两块:软件App功耗、硬件功耗
核心处理只有两个函数:
- processAppUsage:应用程序耗电量计算,是指每一个应用程序使用硬件模块所产生的耗电量
processMiscUsage :其他杂项耗电量计算,所谓杂项,其实就是用户比较关心的一大类,包括:待机的耗电量、亮屏的耗电量、通话的耗电量、Wifi的耗电量等
-
void refreshStats(int statsType, SparseArray asUsers, long rawRealtimeUs,
long rawUptimeUs) {// Initialize mStats if necessary. getStats();
...... //初始化一些PowerCalculato 以及各类时间参数
processAppUsage(asUsers);
.... // 记录移动数据流量到mMobilemsppList 中
processMiscUsage();
Collections.sort(mUsageList);
.... // 对统计数据做一些去杂和优化
}
电量统计服务的启动过程
电量统计服务是一个系统服务,名字为batterystats,在Android系统启动的时候,这个服务就会被启动,其启动时序如下图所示:
电量统计服务是间接由ActivityManagerService(后文简称AMS)来启动,AMS是Android系统最为基础的服务,进入Android系统后,最优先启动的,就是这类服务。
耗电统计触发时机
Android框架层通过一个名为batterystats的系统服务,实现了电量统计的功能。batterystats获取电量的使用信息有两种方式:
- 被动(push):有些硬件模块(wifi, 蓝牙)在发生状态改变时,通知batterystats记录状态变更的时间点
- 主动(pull):有些硬件模块(cpu)需要batterystats主动记录时间点,譬如记录Activity的启动和终止时间,就能计算出Activity使用CPU的时间
收集的信息基本都包含硬件模块的状态和被使用的时间两个维度。为什么仅仅是收集不同硬件模块的使用时间呢? 前面我们说过,手机电压通常是恒定的,耗电量是通过 “单位时间电流量(I) × 使用时间(t)” 来计算,而单位时间电流量是由厂商给定的,定义在power_profile.xml中, 所以,只需要收集不同硬件模块的使用时间,就可以近似的计算出耗电量了
收集信息被组织起来,在内存中的数据结构是由BatteryStats类描述的。 为了能够从不同维度统计耗电量,这个数据结构设计得比较复杂,我们不在这里展开讨论。
记录应用程序中所有Activity从显示状态(Resumed)到消失状态(Paused)的时间,就能够统计应用程序的前台运行时间。Activity状态的切换是由AMS掌控的,因此AMS需要将Activity的状态信息通知给batterystats服务。
除了应用程序前台运行时间,还有很多信息是batterystats服务关注的,包括WakeLock、Sendor、Wifi、Audio、Video等,这些信息的采集方式都会经过以下步骤:
- 由相应的模块发起状态变更的通知
- BatteryStats使用定时器记录起止时间
应用程序可能会使用多个硬件模块,所以,耗电信息收集的策略也被设计得比较复杂,譬如,要使用到很多计时器,就设计出了“计时器池”来提高资源利用率。
电量信息存储
收集到的电量信息,在内存中是由BatteryStats这个类来描述的,Android支持历史电量信息的显示的,如果重新启动Android,那内存中的数据就丢失了, 所以需要把这些信息存储到磁盘上,磁盘上的 /data/system/batterystats.bin 文件中就是电量信息的序列化数据。
batterystats服务启动时,会从 batterystats.bin 这个文件中读取数据,来初始化BatteryStats这个数据结构。
所以,在手机使用的过程中,收集到的电量信息,就会被当作历史信息,不定时的写入到磁盘保存下来,下次batterystats启动时,又会被用到。
软件功耗计算
前面我们提到耗电量是通过计算:
耗电量 = 单位时间的耗电量(w) × 使用时间(t) = 电压(U) × 单位时间电流量(I) × 使用时间(t)
在手机上电压一般是恒定的,所以,计算耗电量只需要知道单位时间电流量即可。有了power_profile.xml这个文件描述的单位时间电流量,再收集硬件模块在不同状态下的使用时间,就能够近似的计算出耗电量了。
软件功耗相关方法
//计算app 消耗的Cpu电量到cpuPowerMah 中
mCpuPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
//计算app 使用的Wakelock电量到wakeLockPowerMah 中
mWakelockPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
// 计算app 使用radio 网络消耗的电量到mobileRadioPowerMah 中
mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
mStatsType);
// 计算app 使用的Wifi电量到wifiPowerMah 中
mWifiPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
// 计算app 使用蓝牙的电量到bluetoothPowerMah 中
mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
mStatsType);
// 计算app 使用的Sensor电量到sensorPowerMah 中
mSensorPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
// 计算app 使用camera的电量到cameraPowerMah 中
mCameraPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType);
// 计算app 使用闪光灯Flashlight 的电量到flashlightPowerMah
mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs,
mStatsType);
totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah + sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah + flashlightPowerMah + bluetoothPowerMah;
至此,我们分析了以下两个问题:
Android如何启动电量统计服务? Android系统启动 -> AMS启动和注册 -> batterystats启动和注册
Android如何计算耗电量? 并不是直接跟踪电流消耗量,而是采用“单位时间电流量(I)×使用时间(t)”来做近似计算。不同硬件模块的单位时间电流量是需要厂商给定的。
硬件功耗计算
硬件功耗计算函数在:processMiscUsage()
private void processMiscUsage() {
addUserUsage(); // 多用户中每个用户的耗电量
// 公式:user_power = user_1_powerMah + user_2_powerMah + … + user_n_powerMah; (n为所有的user的总数)
addPhoneUsage(); // modem通话耗电量
// 公式:phonePower = (phoneOnPower * phoneOnTimeMs ) / (60 * 60 * 1000);
addScreenUsage(); // 屏幕耗电量
// 公式:screenOnPower = screenOnTimeMs * POWER_SCREEN_ON
addWiFiUsage(); // wifi耗电量
// wifiPowerMah = ((totalRunningTimeMs - mTotalAppWifiRunningTimeMs) * mWifiPowerOn) / (1000* 60* 60);
addBluetoothUsage(); // 蓝牙耗电量
// 公式:bluetoohPower = ((idleTimeMs * mIdleMa) + (rxTimeMs * mRxMa) + (txTimeMs * mTxMa)) / (10006060);
addMemoryUsage(); // DDR内存耗电量
// 公式:memoryPower = (mAatRail_1 * timeMs_1 + mAatRail_2 * timeMs_2 + ... + mAatRail_n * timeMs_n) / (1000 * 60 * 60) (mAatRail_n :是该读写速率级别下的功率,timeMs_n:是在mAatRail_n 级别下的时间)
addIdleUsage(); // CPU suspend/idle状态下的耗电量(不包括蜂窝数据空闲功耗)
// 公式:idlePower = (suspendPowerMaMs + idlePowerMaMs) / (60 * 60 * 1000);
if (!mWifiOnly) {//(当只有wifi上网功能的设备时不计算蜂窝数据功耗,如平板,电视等)
addRadioUsage(); //移动数据网络的耗电量
}
}
电量信息统计服务的统计方式可以简单总结为:耗电量 = 模块耗电功率 * 模块耗电时间,其耗电功率中硬件耗电功率由硬件厂商提供过来的Power_profile.xml 中配置好了,模块耗电时间为系统中各种Timer 计时器来统计的。
电量计算流程及公式图
参考文献
1、https://duanqz.github.io/2015-07-21-batterystats-part1#33-%E7%94%B5%E9%87%8F%E8%AE%A1%E7%AE%97