Android BatteryStats服务功耗统计流程详解
简介
BatteryStatsService和BatterStatsImpl是系统中用于估算电流消耗的关键对象,能够估算并存储软件功耗和硬件功耗。其中主要流程分为事件回调时记录耗电信息、触发读取时计算并统计耗电信息两大流程。本文介绍耗电信息的读取和统计过程。
BatteryStatsService和BatteryStatsImpl
BatteryStatsService提供App和硬件的功耗,是通过BatteryStatsImpl的接口getUidStats()
方法取得的,在分析该方法的流程前先简单介绍一些关键成员。
BatteryStatsHelper关键成员:
- BatterySipper:按照Uid和drain type来过滤存储的功耗记录,从BatteryStatsImpl中提取。可以理解为从系统记录的全部功耗信息中,按照Uid和Drain type提取出来。其中Drain type是耗电类型,如屏幕耗电、Wi-Fi耗电、App耗电等
- mUsageList:以BatterySipper的形式记录各Uid的功耗
- mWifiSippers、mBluetoothSippers:Wi-Fi、蓝牙专用的功耗记录(也是BatterySipper的形式)
- mUserSippers:mUsageList只存储
refresh
方法传入的参数内的uid的耗电。当uid不在refresh方法指定的uid范围内时,不存储到mUsageList而是存储到这里 - mMobilemsppList:各App的milliseconds per packets:各App的每个数据包的平均时间
- mRawRealtimeUs mRawUptimeUs : refresh()调进来的传入的elapsed time和uptime
- mBatteryUptimeUs mBatteryRealtimeUs mRaw*timeUs下当前battery*time
- mTypeBatteryUptimeUs mTypeBatteryRealtimeUs:当前版本的Android只支持上一次充满电后的记录,因此等同于mBattery*timeUs
- mBatteryTimeRemainingUs 根据上一次充满电后的掉电量/掉电时间估算出来还需要多久用光电池
- mChargeTimeRemainingUs 根据充电量/充电时间估算出来的还需要多久充满电池
- mMinDrainedPower mMaxDraingedPower 上次充电后的最小/最大的一次掉电量范围
- BatteryStatsImpl.TimeBase:拔掉电源、熄屏等各有一个TimeBase,封装了成员用于计算这些事件后的事件
BatteryStatsHelper.refreshStats:获取功耗记录主入口
refreshStats()
方法用于获取系统记录的耗电情况,流程如下。
- 功耗记录是借助Binder从BatteryStatsService获取的:BatteryStatsService.getStats()->getActiveStatistics()获取BatteryStatsImpl(binder parcel传递)
- 调用processAppusage计算各Uid App的电耗
- 调用processMiscUsage()计算硬件功耗
- 计算mUsageList内记录的最高功耗到mMaxRealPower、mMaxPower
- 计算mUsageList内记录的各Uid的总功耗到mComputedPower、mTotalPower
- 根据mMinDrainedPower和mMaxDrainedPower,确定UNAACCOUNTED、OVERCOUNTED的电耗量,更新mTotalPower和mMaxPower,将over count和un count的电耗量加入到mUsageList中
- 进行电耗摊派。部分电耗属于系统需要隐藏的(shouldHideSipper),或者是要分摊给Uid们的。其中,要分摊的种类是需要隐藏的种类的子集,也就是说部分需要隐藏的电耗是不需要摊派的(或者之前已经摊派了,不用重复分摊)。其中屏幕功耗的分摊方式是,根据几个sipper的前台时间来算比例,根据前台时间的多少来承担对应比例的屏幕功耗,将屏幕功耗按前台时间分出去;承担了屏幕功耗的sipper会将自己分担的屏幕功耗记录到proportionalSmearMah,并调用sumPower()更新自己的总功耗
- 至此,详细功耗信息就记录到了mUsageList中了,通过BatteryStatsHelper.getUsageList()即可取得,遍历该集合的BatterySipper即可取得各uid的耗电记录
processAppUsage():读取并按uid来计算App耗电
该方法根据uid和drain type来统计App的耗电情况。
- BatteryStatsImpl.getUsageStats()取得所有Uid,遍历processAppUsage()传入指定的需要统计的uid
- 创建BatterySipper;BatterySipper用于提取Uid里面指定消耗类型;因为Uid里面记录了App、硬件等不同类型的消耗,这里只统计App自己的消耗(排除硬件);Sipper的作用其实就是从Uid里面提取Type对应的数据并存储下来
- 通过几大类的PowerCalculator.calculateApp()将Uid内的信息提取出来,进行计算,将数据存储进sipper
- 最后调用sipper.sumPower()将几大类的功耗全部加起来得到总功耗
- 接着当这个Uid sumpower > 0时,就根据uid来添加到对应的sipper集合中。
- Wifi、蓝牙对应的Uid会独立存储在它们的特定的sipper集合
- 当该Uid不是processAppUsage()参数指定的要读取的uid时、且该UID是App的Uid(≥FISTST_APPLICATION_UID)时,存储到mUserSippers中
- 都不是时,存储到mUsageList这个sipper集合中
- UID == 0(ROOT)时,处理为uid为0的实体的Wakelock的功耗,相当于将uid为0的drain type为App的情况算作系统的唤醒功耗(设备唤醒的时间可能比屏幕亮起时间和应用程序唤醒锁定时间要长。如果可能,将此余数分配给操作系统);记录到uid为0的drain type为app的记录时,表明应用可能都已经停止耗电,但系统还有服务或逻辑在运行,或进入睡眠前的一段间隔,这些内容会算作系统的功耗,并在Uid记录中体现
PowerCalculator:9大耗电类别计算App耗电
PowerCalculator是abstract类,用于App的9大类别的耗电计算,分别为:CPU、WakeLock、移动网络、Wifi、蓝牙、传感器、摄像头、闪光灯、媒体:9大类。
主要方法:
- reset():清除掉本计算器内的所有状态和数据
- calculateApp(BatterySipper,BatteryStats.Uid,rawRealtimeUs,rawUptimeUs,statsType):计算App在这个类别下消耗的电量;其中sipper用于从Uid提取对应的信息,可以理解成一个提供过滤能力的吸管,用于存储返回给调用者的信息,包括类型、uid、各项细节功耗值;real/upTime:系统当前的real time和uptime;realtime就是linux系统记录的系统启动以来经过的时间,uptime是realtime去掉休眠的时间;statsType是如何统计,Android Q以后只有一种方式:从上一次充满电后开始的所有数据
- calculateRemaining(),参数与calApp()完全一致;作用是当Uid内部记录的电量消耗不能全部算给App时,剩余的电量消耗
class Uid
BatteryStats有个内部类Uid
,它封装了各uid(应用、系统等)以及它们的运行时长、耗电信息、网络收发情况、CPU时间等等一切和功耗有关的信息。各个PowerCalculator基于这个对象内存储的数据来进行计算。
CpuPowerCalculator:计算App消耗在CPU上的功耗
BatterySipper与CPU相关的主要是:cpuTimeMs、cpuFgTimeMs、cpuPowerMah、packageWithHighestDrain,CpuPowerCalculator主要就是读取并计算这些值,存储到上述sipper的这些成员中,最后存储到mUsageList或mUserSippers。
calculateApp:
- 调用Uid的方法取得Uid记录的uid对应的CPU时间(分不同的cluster(簇,不同的簇,因大小核、架构而功耗不一样)、不同的CPU速度),将这些数据与power_profile.xml记录的硬件功耗相乘后相加,得到Uid在CPU上消耗的时间和电量。其中CPU电量的计算公式是:总电耗=总CPU时间*CPU激活状态下的功耗 + 各频率运行时长*各频率对应额外运行功耗
- 一个uid可以有多个进程,Uid.Proc对象表示一个进程的唤醒时长等统计信息,同一个uid的进程的信息对应的若干Proc存储在Uid内的ArrayMap中;取得Uid内所有Proc的foreground time求和得到uid对应的总的cpuFdTimeMs,存入sipper;并总时间(user time、system time、fg time)最大的Proc存储到sipper的packageWithHighestDrain用于表示该Uid下CPU消耗最大的Proc
calculateRemaining:空实现
WakeLockPowerCalculator:计算App持有唤醒锁的时间和功耗
calculateApp:
- WakeLock专门存储于一个集合,用Uid.Wakelock表示,里面记录的Wake time取出后累加记录到sipper的wakeLockTimeMs
- 电量计算公式为:wakeLockTimeMs * CPU idle电耗
calculateRemaining:
- calculateApp只统计了Uid自己的wakelock时长对应的电耗,当Uid内记录的电池时间大于sum(WakeLockTime + ScreenOnTime)时,大于的部分算作app wake,同样算入wakeLockTimeMs、及对应电耗
MobileRationPowerCalculator:计算App进行的移动网络射频功耗
calculateApp:根据App收发的数据包数量结合power_profile计算电耗
calculateRemaining:根据信号强度、扫描次数结合power_profile计算电耗
WifiPowerCalculator:计算App进行的Wifi收发功耗
calculateApp:根据收发时长、空闲时长结合power_profile计算电耗;获取Uid存储的收发字节数、数据包数量存储到sipper
MediaPowerCalculator:计算App音视频功耗
calculateApp:Audio、Video时长*对应功耗得出电耗
processMiscUsage():统计硬件功耗
该方法用于统计硬件的功耗,与App功耗一致,都是根据Uid、Drain type借助sipper来计算的。
addUserUsage
前面统计App用量时提到,如果不是App的uid或没有在refreshStats方法传入的参数中没包含这个uid,那么不会存到mUsageList里面,而是在mUserSippers内;这个方法将mUserSippers内的DrainType为User的sipper提取出来放入mUsageList内。
注意前面processAppUsage处理的DrainType是APP,这里是USER。
addPhoneUsage
添加”Phone On”类型的功耗;这里统计的是Ratio功能active的功耗,可以认为是不开飞行模式时、上了基站的通话网络情况下功耗;形象理解为:设备正常放着,不进行基带射频、扫描、上网、通话时的功耗
addScreenUsage
添加屏幕的功耗,DrainType.Screen加入到mUsageList
addAmbientDisplayUsage
添加屏幕微光情况下的功耗(Display Doze),DrainType.Ambient加入到mUsageList
addWiFiUsage
统计除分摊给App以外的剩余Wifi消耗,从mWifiSippers中提取,以DrainType.WIFI的新的sipper加入到mUsageList。
addBluetoothUsage
蓝牙消耗的电量不会像WiFi一样会分摊给各个使用的App,它没有进行摊派全部统计在这里。该方法统计蓝牙的整体消耗;同WIFI,DrainType.BLUETOOTH加入mUsageList
addMemoryUsage
类似蓝牙的情况
addIdleUsage
统计设备休眠、空闲时的AP(App Processor,即CPU)的功耗——也就是说,不包含BP(基带)的功耗,由于外设已经关闭或挂起、系统已经休眠,可以认为是深度待机状态下的整机功耗,可以说是开机状态下设备的最低功耗了
DrainType.IDLE sipper加入到mUsageList
addRadioUsage
射频也和Wifi一样进行了摊派,同样是将没有摊派出去的独立出来到DrainType.CELL。其中,射频会额外关注信号强度,信号强度对射频功率、模组功耗影响大,进而影响对电耗的估算
总结
BatteryStatsService从系统各服务、驱动节点、Linux文件系统节点中取得的数据,在需要时,通过BatteryStatsImpl.refresh()取出、过滤、计算。其中,按照App电耗、硬件电耗两大部分来分别计算。耗电信息从Uid中取得,经过PowerCalculator处理后放入BatterySipper。一些类别的硬件电耗会分摊到使用它的几个App中,最终会以uid和Drain Type的形式来存储到多个BatterySipper中,交给调用者完成整个功耗信息获取过程。