Android电量/功耗优化指南

基础知识

电能 = 电压 * 电流 * 时长
模块电量(mAh) = 模块电流(mA) * 时长(h)

系统电量优化红线规则
image.png
华为电量优化红线规则
image.png

电量分析

Android 系统电量计算

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

  • 功率:power_profile.xml,Android 系统使用此文件来描述设备各个硬件模块的额定功率,包括上面提到的多档位功率和 CPU 电量算需要到的各种参数值。

Android 系统要求不同的厂商必须在 /frameworks/base/core/res/res/xml/power_profile.xml 中提供组件的电源配置文件。

  • 从手机中导出 /system/framework/framework-res.apk 文件。
  • 使用反编译工具(如 apktool)对导出文件 framework-res.apk 进行反编译。
  • 查看 power_profile.xml 文件在 framework-res 反编译目录路径:/res/xml/power_profile.xml。
<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
  <!-- 显示相关配置 -->
  <!-- 环境模式的平均电流消耗,包含背光 -->
  <!-- 环境模式主要应用在 TV 上,类似电脑中的屏幕保护。 -->
  <item name="ambient.on.display0">0.1</item>  <!-- ~100mA -->
  <!-- 环境模式的平均电流消耗,不包含背光 -->
  <item name="screen.on.display0">0.1</item>  <!-- ~100mA -->
  <!-- 全亮度时背光的平均电流消耗 -->
  <item name="screen.full.display0">0.1</item>  <!-- ~100mA -->
  
  <!-- 蓝牙数据传输 -->
  <item name="bluetooth.active">0.1</item> <!-- ~10mA -->
  <!-- 蓝牙开启并可连接,但未连接状态 -->
  <item name="bluetooth.on">0.1</item> <!-- ~0.1mA -->
  <!-- WIFI 开启 -->
  <item name="wifi.on">0.1</item> <!-- ~3mA -->
  <!-- WIFI 数据传输 -->
  <item name="wifi.active">0.1</item> <!-- ~200mA -->
  <!-- WIFI 扫描 -->
  <item name="wifi.scan">0.1</item> <!-- ~100mA -->
  <!-- 音频 -->
  <item name="audio">0.1</item> <!-- ~10mA -->
  <!-- 视频 -->
  <item name="video">0.1</item> <!-- ~50mA -->
  <!-- 相机闪光灯 -->
  <item name="camera.flashlight">0.1</item> <!-- ~160mA -->
  <!-- 相机平均值 -->
  <item name="camera.avg">0.1</item> <!-- ~550mA -->
  <!-- GPS 开启 -->
  <item name="gps.on">0.1</item> <!-- ~50mA -->
  
  <!-- 移动通信模块相关值。对于固件中没有能源报告支持的调制解调器,请使用 radio.active、radio.scanning 和 radio.on。 -->
  <!-- 移动通信模块传输 -->
  <item name="radio.active">0.1</item> <!-- ~200mA -->
  <!-- 移动通信模块信号扫描 -->
  <item name="radio.scanning">0.1</item> <!-- ~10mA -->
  <!-- 移动通信模块传输时不同信号强度下的电流消耗 -->
  <array name="radio.on"> <!-- 信号强度 0 到 BINS-1 -->
      <value>0.2</value> <!-- ~2mA -->
      <value>0.1</value> <!-- ~1mA -->
  </array>

  <!--CPU 运行时的额外功耗(不包括集群和核心)-->
  <array name="cpu.active">
      <value>0.1</value>
  </array>
  <!-- 每个 CPU 集群的核心数 -->
  <!-- CPU 集群就是常说的 CPU 大小核。
    例如下面就列举了,有两个 CPU 集群(暂时可以理解为 大核+小核,4个大核,2个小核)。
       <array name="cpu.clusters.cores">
         <value>4</value> // cluster 0 has cpu0, cpu1, cpu2, cpu3
         <value>2</value> // cluster 1 has cpu4, cpu5
       </array>
  -->
  <array name="cpu.clusters.cores">
      <value>1</value> <!-- cluster 0 has cpu0 -->
  </array>
  <!-- 不同 CPU 集群的运行速率-->
  <array name="cpu.speeds.cluster0">
      <value>400000</value> <!-- 400 MHz CPU speed -->
  </array>
  <!--- CPU 集群运行时电流消耗 --> 
  <array name="cpu.active.cluster0">
      <value>0.1</value>  <!-- ~100mA -->
  </array>
  <!-- CPU 空闲时电流消耗 -->
  <item name="cpu.idle">0.1</item>
  
  <!-- 内存组件(内存总线、内存控制器、RAM 等)电流消耗 -->
  <array name="memory.bandwidths">
    <value>22.7</value> <!-- mA for bucket: 100mb/s-1.5 GB/s memory bandwidth -->
  </array>

  <!-- 标压下电池容量,单位 mAh -->
  <item name="battery.capacity">1000</item>

  <!-- WIFI 相关配置 -->
  <!-- WIFI 控制器(负责管理 Wi-Fi 连接、数据传输和接收等操作)空闲时电流消耗,默认为 0 -->
  <item name="wifi.controller.idle">0</item>
  <!-- WIFI 控制器接收时电流消耗,默认为 0 -->
  <item name="wifi.controller.rx">0</item>
  <!-- WIFI 控制器传输时电流消耗,默认为 0 -->
  <item name="wifi.controller.tx">0</item>
  <!-- WIFI 控制器在不同传输功率下的电流消耗,默认为空 -->
  <!-- 不同传输功率通常与 WIFI 信号强度、距离有关 -->
  <array name="wifi.controller.tx_levels"> <!-- mA -->
  </array>
  <!-- WIFI 控制器工作电压,单位 mV,默认为 0 -->
  <item name="wifi.controller.voltage">0</item>
  <!-- WIFI 控制器在批量扫描时的电流消耗 -->
  <array name="wifi.batchedscan"> <!-- mA -->
    <value>.0002</value> <!-- 1-8/hr -->
    <value>.002</value>  <!-- 9-64/hr -->
    <value>.02</value>   <!-- 65-512/hr -->
    <value>.2</value>    <!-- 513-4,096/hr -->
    <value>2</value>    <!-- 4097-/hr -->
  </array>

  <!-- 移动通信模块(如蜂窝网络、LTE、5G)相关配置-->
  <modem>
    <!-- 睡眠状态电流消耗,单位 mA -->
    <sleep>0</sleep>
    <!-- 空闲状态电流消耗,单位 mA -->
    <idle>0</idle>
    <!-- 可根据不用模式指定多个 <active> 标签 -->
    <!-- 可用属性:
          rat: 指定移动通信技术:LTE、NR、DEFAULT
          nrFrequency: NR 处于活动状态时频率级别:"LOW"、"MID"、"HIGH"、"MMWAVE"、"DEFAULT",
            LOW 表示 <1GHz
            MID 表示 1GHz - 3GHz
            HIGH 表示 3GHz - 6GHz
            MMWAVE 表示 >6GHz
    -->
    <active rat="DEFAULT">
      <!-- 接收时电流消耗,单位 mA -->
      <receive>0</receive>
      <!-- 传输时电流消耗,单位 mA。必须定义所有级别(0 到 4) -->
      <transmit level="0">0</transmit>
      <transmit level="1">0</transmit>
      <transmit level="2">0</transmit>
      <transmit level="3">0</transmit>
      <transmit level="4">0</transmit>
    </active>
  </modem>
  <!-- 移动通信模块(调制解调器)工作电压,单位 mV -->
  <item name="modem.controller.voltage">0</item>

  <!-- GPS 相关配置 -->
  <!-- GPS 信号强度 -->
  <array name="gps.signalqualitybased"> <!-- 强度 0 to 1 -->
    <value>0</value>
    <value>0</value>
  </array>
  <!-- GPS 工作电压,单位 mV -->
  <item name="gps.voltage">0</item>
</device>
  
  • 时长:StopWatch & SamplingCounter,其中 StopWatch ⏱ 是用来计算 App 各种硬件模块的使用时长,而 SamplingCounter 则是用来采样统计 App 在不同 CPU Core 和不同 CpuFreq 下的工作时长。
  • 计算:PowerCalculators,每个硬件模块都有一个相应命名的 PowerCalculator 实现,主要是用来完成具体的电量统计算法。
  • 存储:batterystats.bin,电量统计服务相关数据的持久化文件。

image.png

统计

BatteryStatsService 持有 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 模块使用时长的统计。

计算

BatteryStats 是通过依赖的一个 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 时间 + 上行功率 × 上行时间 + 下行功率 × 下行时间。

工具

工具优点缺点
手机耗电排行直观没有详细数据
监听电池状态可知电池状态、电量只是整体耗电情况、非实时
dumpsys batterystats可知App整体耗电量、各功能模块耗电量可读性比较差、非实时
Battery Historian结果直观、有耗电量详情适应 Android 5.0 及以上系统、无堆栈信息、非实时
BatteryCanary实时监测、支持问题堆栈

手机耗电排行

image.png

监听电池状态

获取充电状态、电池电量
监控电池电量和充电状态

IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryState = registerReceiver(null, intentFilter);
int chargeType = batteryState.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
Log.e("qingshan","充电方式 = " + chargeType); //1.交流电;2.USB;4.无线充电
int batteryPower = batteryState.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
Log.e("qingshan","电量 = " + batteryPower);

dumpsys batterystats

通过 dumpsys batterystats 导出。

$ adb shell dumpsys batterystats your.package.name > battery.txt

# 各个Uid的总耗电量,而且是粗略的电量计算估计。
# Estimated power use (mAh):
#     Capacity: 3450, Computed drain: 501, actual drain: 552-587
#     ...
#     Idle: 41.8
#     Uid 0: 135 ( cpu=103 wake=31.5 wifi=0.346 )
#     Uid u0a208: 17.8 ( cpu=17.7 wake=0.00460 wifi=0.0901 )
#     Uid u0a65: 17.5 ( cpu=12.7 wake=4.11 wifi=0.436 gps=0.309 )
#    ...

# reset电量统计
$ adb shell dumpsys batterystats --reset

Battery Historian

  • docker 部署
docker run -p <port>:9999 gcr.io/android-battery-historian/stable:3.0
  • 获取 bugreport

# 重置电量信息
adb shell dumpsys batterystats --reset
# 开始记录电量
adb shell dumpsys batterystats --enable full-wake-history
# 导出电量信息
$ adb bugreport bugreport.zip # 7.0和7.0以后
$ adb bugreport > bugreport.txt # 6.0和6.0之前:
  • 分析

打开 http://localhost:,上传 bugreport.zip,等待分析结果
image.png
实战:通过场景前后对比耗电差异 -> 大众点评App的短视频耗电量优化实战

BatteryCanary

Matrix-Battery Canary-Demo参考配置
image.png
image.png

优化

重点模块:CPU、屏幕、WIFI、数据网络、GPS、音视频通话

WakeLock

  • acquire 与 release 成对出现,一定要及时释放。
  • 使用带参数的 acquire,设置超时时间。
  • 在 try-catch 中,确保在 finally 中释放。

GPS

  • 根据业务场景,区分精度定位和粗略定位,非强需求推荐粗略定位。
  • 根据业务场景,区分网络定位与 GPS 定位,非强需求推荐网络定位。
  • 降低 GPS 请求频率,及时关闭。

蓝牙

  • 避免后台频繁扫描。
  • startScan 与 stopScan 成对出现,添加超时停止。

网络

  • 监听网络连接,网络可用时发起请求。
  • 添加网络缓存,避免重复请求。
  • 推荐在 WIFI 场景下进行大数据传输、请求。
  • 使用 JSON、Protobuf、gzip 压缩请求数据。
  • 避免后台频繁 WIFI 扫描。
  • 推荐使用厂商 push 替换长链接,或优化长链接,数据业务下通常是 5min,WIFI 网络下通常可以达到 20min 或更久。
  • 禁止轮询。

后台任务调度

合理使用后台任务框架。
Android后台调度任务与省电
后台工作概览 | Background work | Android Developers

前台交互

  • 及时释放资源,如停止动画、释放音频。
  • 推荐使用暗黑模式(即深色系)。
  • 减少页面刷新区域、过度绘制。
  • 使用 SurfaceView 替代 TextureView 播放视频。
  • 对 App、Activity、Windows、View 精确控制硬件绘制(硬件绘制功耗更高)。

参考





Hi,我是“青杉”,您可以通过如下方式关注我:

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值