阅读 532

电量优化 - 电量的统计原理与监控

最近有很多同学来催了,到底还讲不讲?由于活水转岗去微信,面试七轮持续了两个月时间,绿色通道答辩持续了差不多半个月的时间。我发现很多时候都到了身不由己,不是不想去做而是真的没有时间了。我们想去做点事,却发现身体和时间真的不够用。

App “耗电综合征”

当我们说一个 App 耗电的时候我们在说什么?

我们可能是指 App 吃 CPU 导致系统掉电快,也可能是在说系统告警 App 后台扫描频繁消耗电量,还可能是在说使用 App 时手机发烫严重…… 是的,相对于 Crash、ANR 等常见的 APM 指标,Android App 电量优化更像是一个综合性的问题。

一方面,造成 App 耗电的原因是多种多样的,比如 CPU/GPU Load、屏幕、传感器以及其他硬件开销等,每个分类的排查思路是大相径庭的,再加上 AOSP 没有 “官方” 的耗电异常检测框架,各个 OEM 厂商自家系统对 App 耗电的监控方案又各不相同(且没有充分的公开文档),所以检测方案需要结合具体 App 项目实际和用户反馈状况,针对具体的耗电类型做出考量和取舍。另一方面,耗电问题也经常是比较 “主观” 的,比如用户感觉 App 新版本掉电比较快了,或者在户外气温比较高的环境使用 App 时感觉设备发烫了,又或只是单纯的因为使用时间变长了导致系统耗电排行靠前了等等,这些通常都是一些比较微妙的主观感受,难以量化问题。

因此,如何检测各种类型的耗电异常,以及如何提炼耗电问题的规则(划红线)是优化电量指标的关键所在。微信 Android 项目在与 App 耗电异常这项 “疑难杂症” 日常斗智斗勇的过程中,产出了一些比较实用的工具和优化思路。本文针对 Anroid App 的耗电问题,本文主讲 “App 电量统计原理”。后面的我们会陆续对“耗电异常监控方案”以及相关的 “优化案例” 两部分进行解析和分享。

App 电量统计原理

电量计算公式

了解 App 电量统计原理之前,有必要先复习一下电量计算公式:

电量 = 功率 × 时间
复制代码

其中需要注意一点的是, 功率 = 电压 × 电流。而在数码产品中,元器件一般对电流比较敏感,而电压基本是恒定的,所以我们直接使用电流来代替功率,这也是我们经常说 “毫安时”(mAh)而不说 “千瓦时 / 度”(kWh)的原因。

Android 硬件模块的电量统计方式

了解计算公式之后,App 的电量统计思路就比较清晰了:

App 电量 = SUM (模块功率 × 模块时间)
复制代码

其中模块主要是指 Android 设备的各种硬件模块,主要可以分为以下三类。

1626867311_24_w456_h382.png

第一类,像 Camera/FlashLight/MediaPlayer/ 一般传感器等之类的模块,其工作功率基本和额定功率保持一致,所以模块电量的计算只需要统计模块的使用时长再乘以额定功率即可。

第二类,像 Wifi/Mobile/BlueTooth 这类数据模块,其工作功率可以分为几个档位。比如,当手机的 Wifi 信号比较弱的时候,Wifi 模块就必须工作在比较高的功率档位以维持数据链路。所以这类模块的电量计算有点类似于我们日常的电费计算,需要 “阶梯计费”。

第三类,也是最复杂的模块,CPU 模块除了每一个 CPU Core 需要像数据模块那样阶梯计算电量之外,CPU 的每一个集群(Cluster,一般一个集群包含一个或多个规格相同的 Core)也有额外的耗电,此外整个 CPU 处理器芯片也有功耗。简单计算的话,CPU 电量 = SUM (各核心功耗) + 各集群(Cluster)功耗 + 芯片功耗 。如果往复杂方向考虑的话,CPU 功耗还要考虑超频以及逻辑运行的信息熵损耗等电量损耗(这方面有兴趣的话可以自行拓展查证,Android 系统 CPU 的电量统计只计算到芯片功耗这一层)。屏幕模块的电量计算就更麻烦了,很难把屏幕功耗合理地分配给各个 App, 因此 Android 系统只是简单地计算 App 屏幕锁(WakeLock)的持有时长,按固定系数增加 App CPU 的统计时长,粗略地把屏幕功耗算进 CPU 里面。

最后,需要特别注意的是,以上提到的各种功率和时间在 Android 系统上的统计都是估算的,可想而知最终计算出来的电量数值可能与实际值相差巨大,Facebook 的工程师对此也有所吐槽:Mistrusting OS Level Battery Levels,这点大家心里要有一点概念。

Android 系统电量统计服务

Android 系统的电量统计工作,是由一个叫 BatteryStatsService 的系统服务完成的。先了解一下其中四个比较关键的角色:

  • 功率:power_profile.xml,Android 系统使用此文件来描述设备各个硬件模块的额定功率,包括上面提到的多档位功率和 CPU 电量算需要到的各种参数值。
  • 时长:StopWatch & SamplingCounter,其中 StopWatch ⏱ 是用来计算 App 各种硬件模块的使用时长,而 SamplingCounter 则是用来采样统计 App 在不同 CPU Core 和不同 CpuFreq 下的工作时长。
  • 计算:PowerCalculators,每个硬件模块都有一个相应命名的 PowerCalculator 实现,主要是用来完成具体的电量统计算法。
  • 存储:batterystats.bin,电量统计服务相关数据的持久化文件。
工作流程

BatteryStatsService 的工作流程大致可以分为两个部分:时长统计 & 功耗计算。

1626867325_59_w878_h444.png

BatteryStatsService 时长统计流程

BatteryStatsService 框架的核心是 ta 持有的一个叫 BatteryStats 的类,BatteryStats 又持有一个 Uid [] 数组,每一个 Uid 实例实际上对应一个 App,当我们安装或者卸载 App 的时候,BatteryStats 就会更新相应的 Uid 元素以保持最新的映射关系。同时 BatteryStats 持有一系列的 StopWatch 和 SamplingCounter,当 App 开始使用某些硬件模块的功能时,BatteryStats 就会调用相应 Uid 的 StopWatch 或 SamplingCounter 来统计其硬件使用时长。

这里以 Wifi 模块来举例:当 App 通过 WifiManager 系统服务调用 Wifi 模块开始扫描的时候,实际上会通过 WifiManager#startScan () --> WifiScanningServiceImp --> BatteryStatsService#noteWifiScanStartedFromSource () --> BatteryStats#noteWifiScanStartedLocked (uid) 等一连串的调用,通知 BatteryStats 开启 App 相应 Uid 的 Wifi 模块的 StopWatch 开始计时。当 App 通过 WifiManager 停止 Wifi 扫描的时候又会通过类似的流程调用 BatteryStats#noteWifiScanStoppedLocked (uid) 结束 StopWatch 的计时,这样一来就通过 StopWatch 完成 App 对 Wifi 模块使用时长的统计。

BatteryStatsService 功耗计算流程

具体电量计算方面,BatteryStats 是通过 ta 依赖的一个 BatteryStatsHelper 的辅助类来完成的。BatteryStatsHelper 通过组合使用 Uid 里的时长数据、PoweProfile 里的功率数据(power_profile.xml 的解析实例)以及具体各个模块的 PowerCalculator 算法,计算出每一个 App 的综合电量消耗,并把计算结果保存在 BatterySipper [] 数组里(按计算值从大到小排序)。

还是以 Wifi 模块来举例:当需要计算 App 电量消耗的时候,BatteryStats 会通过调用 BtteryStatsHelper#refreshStats () --> #processAppUsage () 来刷新 BatterySipper [] 数组以计算最新的 App 电量消耗数据。而其中 Wifi 模块单独的电量统计就是在 processAppUsage 方法中通过 WifiPowerCalculator 来完成的:Wifi 模块电量 = PowerProfile 预置的 Idle 功率 × Uid 统计的 Wifi Idle 时间 + 上行功率 × 上行时间 + 下行功率 × 下行时间。

public class WifiPowerCalculator extends PowerCalculator {

    @Override
    public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
                             long rawUptimeUs, int statsType) {
        ...
        app.wifiPowerMah =
                ((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
                / (1000*60*60);
    }
}
复制代码

应用场景

作为补充,这里罗列几个 BatteryStatsService 系统服务的应用场景来说明其工作方式。

Android 系统 App 耗电排行

通过以上分析,我们其实已经知道 Android 系统 App 耗电排行是通过读取 BatteryStatsHelper 里的 BatterySipper [] 数据来实现排行的。一般情况下,BatteryStats 的统计口径是 STATS_SINCE_CHARGED, 也就距离上次设备充满电到现在的状态。不过个别 OEM 系统上这里的统计细节有所不同,有的 Android 设备系统可以显示最近数天甚至一周以上的 App 的电量统计数据,具体实现细节不得而知,姑且推断是根据 BatteryStatsHelper 自行定制的服务。

adb dumpsys batterystats & adb bugreport

或许你已经知道怎么通过 adb dumpsys batterystats 或者 adb bugreport Dump 出系统的电量统计数据,以及如何配合 Battery Historian 工具来分析这些数据,实际上这些 adb 命令都是通过 BatteryStatsService 查询 BatteryStats 里持有的 Uid [] 来获得相应的电量统计数据,具体实现可以参考 com.android.server.am.BatteryStatsService#dump

CPU Load/Usage

“CPU Load xx% yy% zz%” 之类的数据相信大家都或多或少见过,ANR 的 traces.txt、以上的 batterystats 和 bugreport Dump 出来的数据,以及 adb top 命令里都会显示类似的 CPU 负载数据,实际上这个数据也是通过 CPU 模块的统计时长来计算:CPU Load = SUM (App CPU Core 时长时间) / CPU 工作时间。需要注意的是 App CPU 时长是按 CPU Core 为单位分开计算的,所以计算结果完全可能超过 100%,比如一个 8 核心的 CPU 计算结果的理论上限是 800%。

后面的我们会陆续对“耗电异常监控方案”以及相关的 “优化案例” 两部分进行解析和分享。最后想送我们一句话,是《大学》中讲的:物有本末事有终始,知所先后则近道矣

视频链接:pan.baidu.com/s/1GJJCQfL5…

视频密码:3bqh

文章分类
Android