Android电池
Android电池
Android电池系列之BatteryStatsService
前置文章
- 《Android电池系列之Android电池概述》
- 《Android电池系列之Android电池守护进程》
- 《Android电池系列之BatteryService》
前言
在文章《Android电池系列之Android电池概述》中,学习了 Android 电池的一些基本信息以及查询方法。在文章 《Android电池系列之Android电池守护进程》中,学了守护进程 healthd 如果接收电池的状态变化以及如果上报到 Framework 或 APP。在文章《Android电池系列之BatteryService》中,学习了 Framework 如果接收 healthd 上报的电池状态,以及广播电池状态变化等基本事务。在本文章,将继续带领读者深入学习,电池状态变化后,有关寿命的计算。
BatteryStatsService概述
BatteryStatsService 和文章《Android电池系列之Android电池概述》中的BatteryService 是相同级别的系统服务, BatteryStatsService 和 BatteryService 两个服务区分开来,各自完成不一样的事务,可见在这方面的事务处理并不简单。
首先,先来了解 BatteryStatsService 的定义
public final class BatteryStatsService extends IBatteryStats.Stub implements PowerManagerInternal.LowPowerModeListener, BatteryStatsImpl.PlatformIdleStateCallback { .....}
这个类定义在文件 frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java 中。
BatteryStatsService 的定义非常简单,继承 IPC 通信的接口,实现两个接口。有关 BatteryStatsService 如何运作,读者可阅读文章 《Android系统之System Server大纲》 了解更多 Android 系统服务方面的知识。BatteryStatsService 到底完成什么事务呢?一句话,BatteryStatsService 收集所有影响电池寿命的信息。何为电池寿命?不是电池生产出来到报废的时间,而是电池的总电量能给设备(手机、平板等)续航多久。何为影响电池寿命?中电量的减少,各器件耗电等等。
电池状态变化
电池状态变化对电池寿命影响可能会是非常大的,比如充电状态的变化,插上充电器,不出意外,电池的寿命肯定是不断延长;比如电池电量减少一格,电池寿命也相应的产生变化。回顾文章 《Android电池系列之BatteryService》,在方法 processValuesLocked() 中,BatteryService 把状态变化传递给 BatteryStatsService,如下
private void processValuesLocked(boolean force) { ..... try { mBatteryStats.setBatteryState(mBatteryProps.batteryStatus, mBatteryProps.batteryHealth, mPlugType, mBatteryProps.batteryLevel, mBatteryProps.batteryTemperature, mBatteryProps.batteryVoltage, mBatteryProps.batteryChargeCounter); } catch (RemoteException e) { // Should never happen. } .....}
这个方法定义在文件 frameworks/base/services/core/java/com/android/server/BatteryService.java 中。
BatteryStatsService 紧接着,传给了 BatteryStatsImpl,如下
public void setBatteryState(final int status, final int health, final int plugType, final int level, final int temp, final int volt, final int chargeUAh) { ..... mStats.setBatteryStateLocked(status, health, plugType, level, temp, volt, chargeUAh); .....}
这个方法定义在文件 frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java 中。
由此看来,BatteryStatsService 只是提供对外接口,真正完成计算的是 BatteryStatsImpl。在 BatteryStatsImpl 的 setBatteryStateLocked() 中,有很多繁琐的细节需要处理,本文就不一一啰嗦它们,仅分析断开充电器的单一场景。
public void setBatteryStateLocked(int status, int health, int plugType, int level, int temp, int volt, int chargeUAh) { // Is connect to charger? final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; if (!mHaveBatteryLevel) { ..... } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) { // record app consume power on daily, // write to "/data/system/batterystats-daily.xml" file recordDailyStatsIfNeededLocked(level >= 100 && onBattery); } int oldStatus = mHistoryCur.batteryStatus; ..... // record last time state startRecordingHistory(elapsedRealtime, uptime, true); ..... if (onBattery != mOnBattery) { // hanldle whether connect to charger or not ..... } else { boolean changed = false; if (mHistoryCur.batteryLevel != level) { // update state to mHistoryCur var // omit other var mHistoryCur.batteryLevel = (byte)level; } ..... // Don't connect to charger if (onBattery) { changed |= setChargingLocked(false); if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { // use battery, consume one level, analyse it below mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, modeBits, elapsedRealtime); // daily tracker mDailyDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, modeBits, elapsedRealtime); mLastDischargeStepLevel = level; mMinDischargeStepLevel = level; mInitStepMode = mCurStepMode; mModStepMode = 0; } } else { if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { // Connect to charger, level change tracker mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, modeBits, elapsedRealtime); // daily tracker mDailyChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, modeBits, elapsedRealtime); mLastChargeStepLevel = level; mMaxChargeStepLevel = level; mInitStepMode = mCurStepMode; mModStepMode = 0; } } }}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsImpl.java 中。
在上面的代码中,由于代码边幅较长,主要的地方已经给出了注释,不再一一阐述了每行代码的功能和作用了。这里,重点关注一下 mDischargeStepTracker 这个对象,对象的实质是 LevelStepTracker 的实例。mDischargeStepTracker 对象完成在不连接充电器的情况下,消耗一格(一般指电池总共100格,一格也就是1%)电量的跟踪和计算。
LevelStepTracker
LevelStepTracker 表示表示每格电量变化的跟踪者,在 BatteryStats 中,有多个 LevelStepTracker 的实例,如 mDischargeStepTracker,mDailyDischargeStepTracker,mChargeStepTracker,mDailyChargeStepTracker。本文以 mDischargeStepTracker 为例,阐述 LevelStepTracker 的作用。承接上一章节的代码片段,mDischargeStepTracker.addLevelSteps() 方法如下:
public void addLevelSteps(int numStepLevels, long modeBits, long elapsedRealtime) { // 记录 Level 次数,这里表示掉了几格电 int stepCount = mNumStepDurations; // 上次记录的时间 final long lastStepTime = mLastStepTime; if (lastStepTime >= 0 && numStepLevels > 0) { // 每格持续的时间记录,即每掉一格电所用的时间 final long[] steps = mStepDurations; // 本次记录所持续的时间,如果 numStepLevels = 1, 则表示这次消耗一格所用的时间 long duration = elapsedRealtime - lastStepTime; // 将最新的记录放到数据的第一的位置,即 steps[0] for (int i=0; i0, steps, 1, steps.length-1); long thisDuration = duration / (numStepLevels-i); duration -= thisDuration; if (thisDuration > STEP_LEVEL_TIME_MASK) { thisDuration = STEP_LEVEL_TIME_MASK; } steps[0] = thisDuration | modeBits; } // 计数加 1 stepCount += numStepLevels; if (stepCount > steps.length) { stepCount = steps.length; } } mNumStepDurations = stepCount; mLastStepTime = elapsedRealtime;}
这个方法定义在文件 frameworks/base/core/java/android/os/BatteryStats.java 中。
在 LevelStepTracker 对象中,主要持有两个对象,一个是 mNumStepDurations,一个是 mStepDurations,mNumStepDurations 表示跟踪记录的次数,mStepDurations 表示跟踪记录的结果,结果以持续时间为单位记录,所谓持续时间,即在 mDischargeStepTracker 中,表示每消耗一格电量所持续的时间。如当前手机有 100 格电量,用了 30 分钟,还有 99 格电,则持续时间为 30 分钟,当然,实际单位要转换成毫秒。
那么,LevelStepTracker 做这些计算有什么用呢?对于 mDischargeStepTracker 而言,就是耗电和时间的关系,这样可以估算出当前剩余电量能续航多长时间,即剩余可用时间。对于 mChargeStepTracker 而言,就是充电和时间的关系,可以估算出充满电所需要的时间。从计算过程来说,这个估算会存在比较大的误差,在系统估算过程中,会取 mNumStepDurations 和 mNumStepDurations 的平均值来估算,因此,相比而言,LevelStepTracker 跟踪的有效数据越多,越准确,估算出来的时间就约接近真实情况。
软件硬件耗电统计
BatteryStatsService 除了上述的功能,还有一个很重要的就是各个软件,各个硬件,各个软件使用各个硬件所消耗的电量的统计。这里统称它们为耗电者。如一个 APP 是耗电者,WIFI 是一个耗电者。
获取所有耗电者
要获取所有耗电者,先完成以下几步
1.需要借助 BatteryStatsHelper 这个类,首先获取 BatteryStatsHelper 的实例,很简单,new 一个对象,传入参数 Context。如下:
public BatteryStatsHelper(Context context) { this(context, true);}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
2.借助 BatteryStatsHelper, 创建 BatteryStats 对象实例
public static BatteryStats statsFromFile(Context context, String fname) { ..... return getStats(IBatteryStats.Stub.asInterface( ServiceManager.getService(BatteryStats.SERVICE_NAME)));}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
在上文中,BatteryStatsImpl 是电池状态计算和统计的核心,通过 BatteryStatsHelper 的 statsFromFile() 获取到的 BatteryStats 实质是 BatteryStatsImpl 的一个“映射”。
3.BatteryStatsHelper 和 BatteryStats 融合
public void create(BatteryStats stats) { mPowerProfile = new PowerProfile(mContext); mStats = stats;}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
这里需要说明以下 PowerProfile-电源配置文件。即配置各个器件(硬件)工作时的耗电大小。这个一般由设备厂商去配置。文件路径在 frameworks/base/core/res/res/xml/power_profile.xml。
<device name="Android"> <item name="none">0item> <item name="screen.on">119item> <item name="screen.full">291item> <item name="bluetooth.active">63item> <item name="bluetooth.on">3item> <item name="wifi.on">2item> <item name="wifi.active">172item> <item name="wifi.scan">17item> .....device>
4.刷新耗电者耗电数据
public void refreshStats(int statsType, int asUser) { SparseArray users = new SparseArray<>(1); users.put(asUser, new UserHandle(asUser)); refreshStats(statsType, users);}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
5.获取所有耗电者
public List getUsageList() { return mUsageList;}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
总的大致代码是这样子的
BatteryStatsHelper bsh = new BatteryStatsHelper(context);BatteryStats stats = BatteryStatsHelper.statsFromFile(context, "stats");bsh.create(stats);bsh.refreshStats(BatteryStats.STATS_SINCE_CHARGED, UserHandle.USER_ALL);List batterySippers = bsh.getUsageList();
获取到得所有的耗电者被封装到 List 中,其中每个 BatterySipper 就是一个耗电者,在 Android 系统中,耗电者有多种分类,分别是:
public enum DrainType { IDLE,//待机 CELL,//无线电 PHONE,//数据 WIFI,//局域网 BLUETOOTH,//蓝牙 FLASHLIGHT,//闪光灯 SCREEN,//屏幕 APP,//应用 USER,//用户服务 UNACCOUNTED,//没有统计的范围 OVERCOUNTED,//超出统计范围 CAMERA//相机}
这个枚举定义在文件 frameworks/base/core/java/com/android/internal/os/BatterySipper.java 中。
获取到每个耗电者实例后,便可以了解每个耗电者的耗电情况,具体接口本文就不在阐述他们了,读者可以查看 BatterySipper.java 了解。
如何统计耗电者
获取到耗电者,就可以知道耗电者耗电细节,那问题来了,系统如果统计各个耗电者的耗电情况?大致思路是这样,统计耗电者操作时间(如使用 WiFi时间,使用屏幕时间),通过 PowerProfile 中的电流值,计算耗电量。那问题又来了,如果统计耗电者操作时间?回到 BatteryStatsImpl 中,有如下这些变量
LongSamplingCounter[] mNetworkByteActivityCounters;LongSamplingCounter[] mNetworkPacketActivityCounters;LongSamplingCounter mMobileRadioActiveTime;LongSamplingCounter mMobileRadioActiveCount;StopwatchTimer mAudioTurnedOnTimer;StopwatchTimer mVideoTurnedOnTimer;StopwatchTimer mFlashlightTurnedOnTimer;StopwatchTimer mCameraTurnedOnTimer;StopwatchTimer mForegroundActivityTimer;StopwatchTimer mBluetoothScanTimer;StopwatchTimer[] mProcessStateTimer;BatchTimer mVibratorOnTimer;Counter[] mUserActivityCounters;StopwatchTimer mWifiRunningTimer;StopwatchTimer mFullWifiLockTimer;StopwatchTimer mWifiScanTimer;LongSamplingCounter mUserCpuTime;LongSamplingCounter mSystemCpuTime;LongSamplingCounter mCpuPower;LongSamplingCounter[][] mCpuClusterSpeed;.....
这些变量定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsImpl.java 中。
以上变量笔者没有全部粘贴出来,这些变量有什么作用呢?就是统计各个器件在不同的场景下的使用时间。
以 CPU 为例,有 mUserCpuTime 和 mSystemCpuTime,以及 mCpuPower。mCpuClusterSpeed是具体到每个 CPU 的核心上,本文就不展开论述这些细节了。mUserCpuTime 是用户态代码执行时间,mSystemCpuTime 在内核态执行时间。在 BatteryStatsImpl 中,有这样一个方法 updateCpuTimeLocked() 用于更新 CPU 时间,这个方法在电池状态变化时会被调用。
public void updateCpuTimeLocked() { mKernelUidCpuTimeReader.readDelta(!mOnBatteryInternal ? null : new KernelUidCpuTimeReader.Callback() { @Override public void onUidCpuTime(int uid, long userTimeUs, long systemTimeUs, long powerMaUs) { final Uid u = getUidStatsLocked(mapUid(uid)); // Accumulate the total system and user time. mTempTotalCpuUserTimeUs += userTimeUs; mTempTotalCpuSystemTimeUs += systemTimeUs; ..... u.mUserCpuTime.addCountLocked(userTimeUs); u.mSystemCpuTime.addCountLocked(systemTimeUs); u.mCpuPower.addCountLocked(powerMaUs); ..... } });}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsImpl.java 中。
在上面的代码中,通过 mKernelUidCpuTimeReader 的 readDelta() 方法,加载 CPU 使用情况,包括uid(耗电者), 用户空间执行时间,内核态执行时间,耗电量,然后通过 u.mUserCpuTime.addCountLocked(userTimeUs) 等记录使用情况。因此,继续跟踪 readDelta() 就可以跟踪到 userTimeUs 和 systemTimeUs 的来源,也就知道耗电者使用 CPU 的时间了。
readDelta() 的加载过程如下
public void readDelta(@Nullable Callback callback) { long nowUs = SystemClock.elapsedRealtime() * 1000; // 加载文件 sProcFile try (BufferedReader reader = new BufferedReader(new FileReader(sProcFile))) { }}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/KernelUidCpuTimeReader.java 中。
从上面的代码可知,CPU 的使用时间从一个文件中加载,这个文件定义如下
private static final String sProcFile = "/proc/uid_cputime/show_uid_stat";
这个变量定义在文件 frameworks/base/core/java/com/android/internal/os/KernelUidCpuTimeReader.java 中。
这个文件的部分记录如下:
//uid: userTime systemTime powerUs0: 131430000 1504900000 050048: 30000 40000 01021: 190000 1330000 050137: 50000 20000 050082: 30000 40000 01000: 136650000 269730000 050116: 50000 20000 050061: 30000 40000 050150: 70000 10000 010032: 8280000 2420000 050095: 40000 50000 02000: 67710000 92400000 050040: 130000 160000 01013: 1580000 1580000 050129: 40000 30000 0
详情可查看设备的 /proc/uid_cputime/show_uid_stat 文件。
对于其它的器件的使用时间的跟踪,这里就不一一阐述了,虽然每个器件的统计路径和过程都不一样,但是基本可以举一反三。绝大多数的器件统计使用时间的接口在 BatteryStatsImpl 中,如 Wifi 关闭的接口
public void noteWifiOffLocked() { .....}
其它的接口都类似 note***() 这种命名,本文就不一一贴出来了。
详细统计耗电详情
在章节“获取所有耗电者”中的第 4 步“刷新耗电者耗电数据”中会计算耗电者的耗电详细数据
public void refreshStats(int statsType, SparseArray asUsers, long rawRealtimeUs, long rawUptimeUs) { ..... if (mCpuPowerCalculator == null) { mCpuPowerCalculator = new CpuPowerCalculator(mPowerProfile); } ..... processAppUsage(asUsers); processMiscUsage(); .....}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
这里省略其它很多计算的过程,一文计算过程都比较繁杂,无法一一阐述。本文就论述比较特出的一部分,接着看
processAppUsage () 方法。
private void processAppUsage(SparseArray asUsers) { BatterySipper osSipper = null; final SparseArray<? extends Uid> uidStats = mStats.getUidStats(); final int NU = uidStats.size(); for (int iu = 0; iu < NU; iu++) { final Uid u = uidStats.valueAt(iu); final BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, 0); //计算CPU耗电 mCpuPowerCalculator.calculateApp(app, u, mRawRealtimeUs, mRawUptimeUs, mStatsType); //省略其它器件耗电计算 ..... final double totalPower = app.sumPower(); ..... } else { //添加到耗电者列表 mUsageList.add(app); } }}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/BatteryStatsHelper.java 中。
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,long rawUptimeUs, int statsType) { //获取CPU使用时间,参考上文 app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000; double cpuPowerMaMs = 0; for (int cluster = 0; cluster < numClusters; cluster++) { final int speedsForCluster = mProfile.getNumSpeedStepsInCpuCluster(cluster); for (int speed = 0; speed < speedsForCluster; speed++) { final double ratio = (double) u.getTimeAtCpuSpeed(cluster, speed, statsType) / totalTime; // 计算耗电大小 final double cpuSpeedStepPower = ratio * app.cpuTimeMs * mProfile.getAveragePowerForCpu(cluster, speed); cpuPowerMaMs += cpuSpeedStepPower; } } //时间单位转换 app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000); .....}
这个方法定义在文件 frameworks/base/core/java/com/android/internal/os/CpuPowerCalculator.java 中。
关于如何计算耗电者耗电详情就阐述到这里,虽然本文多以 CPU 的耗电计算作为例子,但是其它器件计算耗电都差不多,掌握大的计算方法,和统计思路,很容易看懂这个过程。
总结
本文论述由于电池状态变化导致系统需要对电量消耗情况的记录和统计耗电者的耗电详情。BatteryStatsService 是接收状态的接口,BatteryStatsImpl 是记录和统计计算的核心,BatteryStatsHelper 是计算耗电能手。对于耗电的各种记录、耗电统计以及耗电计算,虽然不难,但是非常零碎和很多细节,非常多的场景,所以…掌握大方向才能更好理解电池状态模块。
更多相关文章
- Android: 向最强看齐, 反编译 愤怒的小鸟.apk
- webpack 配置文件webpack.config.js
- webpack 中使用 axios 方法总结及初识 vue
- 如何在 Ubuntu 20.04 上添加交换空间
- Java基于BIO实现文件上传功能
- Docker —— 简介与镜像用法
- 初识 NPM 及 webpack 包管理工具
- 小鸟云服务器FTP上传中断是什么原因?解决方法总结
- TP5+VUE+AXIOS项目搭建初试