14、电量统计原理:Android电量计算方式、Power Profile文件、各硬件模块功耗
说到手机电量,大家最熟悉的可能就是设置里那个「电池使用情况」的饼图。但你真的知道这个饼图是怎么算出来的吗?我当年刚接触Android系统时,也以为它是靠硬件电流传感器直接读出来的。后来踩了坑才发现——其实大部分Android设备的电量统计,都是靠「估算」的。
嗯,这里要注意。真正的硬件电量计(Fuel Gauge)当然存在,但系统层上报给应用层的那些「xx应用耗电xx%」,绝大多数是软件算出来的。说白了,就是系统根据每个硬件模块的工作时间,乘以对应的功耗系数,再累加出一个结果。
今天我就带你彻底搞懂这套机制。我们从三个层面来拆解:Power Profile配置文件、各硬件模块的功耗模型、以及最终的电量计算流程。
14.1 Power Profile:硬件功耗的「字典」
Android系统怎么知道CPU跑1秒钟耗多少电?Wi-Fi开1分钟耗多少电?答案就在 /frameworks/base/core/res/res/xml/power_profile.xml 这个文件里。
这个文件由设备厂商提供,里面记录了每个硬件模块在不同状态下的功耗值。我参与过一款平板项目的功耗调优,当时厂商给的power_profile.xml里蓝牙的idle功耗写错了,导致系统统计的待机耗电完全不准。折腾了两天才定位到问题。
来看一个典型的power_profile.xml片段:
<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
<item name="screen.on">200</item>
<item name="screen.full">300</item>
<item name="wifi.active">100</item>
<item name="wifi.scan">120</item>
<item name="bluetooth.active">50</item>
<item name="bluetooth.idle">5</item>
<item name="cpu.idle">10</item>
<item name="cpu.active">200</item>
<item name="cpu.clusters.cores">4</item>
<item name="cpu.clusters.speed">[300, 600, 1000, 1400]</item>
<item name="cpu.clusters.power">[50, 100, 200, 350]</item>
<array name="wifi.batchedscan">
<value>0.3</value>
<value>0.2</value>
<value>0.1</value>
</array>
</device>
这里的单位是 mA(毫安),表示该模块工作1小时的平均电流。比如 screen.on 是200mA,意味着屏幕点亮1小时,平均消耗200mAh的电量。
14.2 各硬件模块的功耗模型
Android系统把硬件模块分成了几大类,每一类的计算方式都不太一样。我一个个说。
14.2.1 CPU 功耗
CPU的功耗计算是最复杂的。因为它有多个频率档位(P-state),还有大小核架构。你想想看,一个八核处理器,大核跑1.8GHz和小核跑500MHz,功耗能差好几倍。
系统会记录每个CPU核心在每个频率下的运行时间,然后乘以对应的功耗值。公式大概是这样:
CPU耗电 = Σ( 核心i在频率j下的运行时间 × 频率j的功耗系数 )
我在项目中遇到过一个问题:某个游戏场景下,系统统计的CPU耗电比实际测量值低很多。后来发现是power_profile.xml里漏掉了GPU的功耗项,导致GPU的耗电被算到了CPU头上。嗯,这种「串户」的情况其实挺常见的。
14.2.2 屏幕功耗
屏幕功耗有两个关键参数:screen.on 和 screen.full。
- screen.on:屏幕点亮但显示全黑时的基础功耗
- screen.full:屏幕显示全白时的最大功耗
实际计算时,系统会根据屏幕的亮度百分比,在两者之间线性插值。比如亮度50%,功耗就是 (on + full) / 2。
14.2.3 无线模块(Wi-Fi / 蓝牙 / 蜂窝)
无线模块的功耗模型稍微复杂一些,因为它们有多个状态:
| 模块 | 状态 | 说明 |
|---|---|---|
| Wi-Fi | active / scan / batchedscan | active是持续传输,scan是扫描,batchedscan是批量扫描(省电模式) |
| 蓝牙 | active / idle | active是连接传输,idle是保持连接但无数据 |
| 蜂窝 | modem.active / modem.idle / modem.scan | 蜂窝的扫描(搜网)非常耗电,尤其是信号差的时候 |
我记得有一次做功耗分析,发现手机待机时蜂窝模块的耗电异常高。查了半天,原来是运营商基站信号弱,手机一直在反复搜网。这个场景下,modem.scan 的功耗系数被频繁触发,导致待机耗电翻倍。
14.2.4 其他传感器和外围设备
像GPS、NFC、摄像头、传感器这些,都有对应的功耗项。GPS尤其特殊,因为它有 acquisition(捕获) 和 tracking(跟踪) 两种模式。捕获模式需要搜索卫星信号,功耗很高;跟踪模式只需要维持定位,功耗低很多。
14.3 电量计算的核心流程
好了,现在我们把所有模块的功耗模型都搞清楚了。那系统到底是怎么算出「微信耗电30%」这个数字的呢?
流程大致分三步:
- 记录时间:系统内核会统计每个硬件模块、每个进程的使用时间。比如CPU跑了多久、Wi-Fi开了多久、GPS用了多久。
- 查表算功耗:用Power Profile里的功耗系数,乘以对应的使用时间,得到每个模块的耗电值(单位mAh)。
- 按应用归因:把硬件模块的耗电,分摊到使用它的应用头上。比如微信调用了GPS 10分钟,那这10分钟的GPS耗电就算到微信头上。
这里有个关键点:归因逻辑。系统怎么知道某个硬件模块是被哪个应用使用的?答案是 UID(用户ID)。每个应用都有唯一的UID,内核在记录硬件使用时间时,会同时记录发起请求的UID。这样最后就能按UID汇总了。
14.4 一张图看懂电量统计原理
下面我用一张SVG流程图,把整个电量统计的链路串起来。你可以对照着看,会更清晰。
14.5 几个容易踩的坑
最后,我分享几个实际项目中遇到的坑,希望能帮你少走弯路。
- Power Profile 数值不准:有些厂商为了「好看」,会把功耗系数写低,导致系统统计的耗电比实际少。用户一看「咦,系统显示只用了50%电,怎么手机就没电了?」——这就是典型的「统计造假」。
- 多核CPU的功耗分摊:系统默认把CPU功耗平均分摊到所有核心上。但实际上,大核和小核的功耗差很多。如果某个应用只跑在小核上,却被分摊了大核的功耗,那它的耗电统计就会偏高。
- 唤醒锁(WakeLock)的遗漏:有些应用持有了唤醒锁,但系统没有正确统计到对应的CPU运行时间。结果就是「手机一直在唤醒状态,但系统显示CPU耗电为0」。我遇到过好几次这种问题,最后都是靠检查内核的wakeup_source节点才定位到的。
adb shell dumpsys batterystats 命令导出原始数据。里面会详细列出每个UID的CPU时间、Wi-Fi时间、GPS时间等。对照Power Profile的数值,自己手动算一遍,就能发现有没有问题了。
好了,关于电量统计的原理,我就讲到这里。这套机制虽然不完美,但胜在通用性强——几乎所有Android设备都遵循这个框架。理解它,你就能看懂设置里的那个饼图到底是怎么来的了。