【小木箱成长营】功耗优化系列文章:
功耗优化 · 工具篇 · 常用的7款功耗优化工具
功耗优化 · 实战篇 · 100万日活APP功耗优化实践
Tips: 关注小木箱成长营公众号, 回复"功耗优化"可免费获取功耗优化思维导图。
一、序言
Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享功耗优化 · 方案篇 · Android功耗优化指导规范。小木箱从五个维度将Android功耗优化指导规范解释清楚。
Android功耗优化指导规范第一部分内容是5W2H分析功耗优化。第二部分内容是功耗优化技术方案。第三部分内容是功耗优化方案分析。第四部分内容是功耗APM监控建设。最后一部分内容是总结与展望。
其中,功耗优化技术方案分为三部分内容,第一部分内容是功耗优化工具,第二部分内容是功耗优化指南,最后一部分内容是功耗优化SOP。
其中,功耗优化方案分析主要分为六部分内容。第一部分内容是CPU异常SOP,第二部分内容是Camera功耗,第三部分内容是低功耗,第四部分内容是热缓解,第五部分内容是动态帧率,最后一部分内容是SurfaceView替换TextureView。
如果学完小木箱功耗优化的入门篇、工具篇、方案篇和实战篇,那么任何人做功耗优化都可以拿到结果。
二、5W2H分析功耗优化
首先我们说说我们的第一部分内容,5W2H 分析功耗优化,5W2H 分析功耗优化提出了 6 个高价值问题
- What : 功耗优化定义
- Why: 高功耗的原因
- How: 功耗优化流程
- Who: 功耗痛点治理
- How Much: 功耗优化价值
- Where: 方案使用场景
2.1 What: 功耗优化定义
Android功耗优化是指优化Android APP在运行过程中的电量消耗,以获得更长的电池续航时间和更优秀的用户体验。功耗优化包括:设备的动态调频、网络优化、电池感知、界面优化、后台活动优化和内存优化等
2.2 How Much: 功耗优化价值
下面的思维导图小木箱从性能价值、用户价值和社会价值三个维度分析了功耗优化价值。
2.3 Why: 高功耗的原因
高功耗是因为设备在运行过程中需要使用大量的电力。
因为CPU高负载、高亮度显示器、频繁GPS定位、后台运行程序、高频率网络通信和不必要动画等原因会导致设备高功耗
下面思维导图小木箱简单总结高功耗6个原因。
2.4 Who: 功耗痛点治理
该如何做功耗优化呢?
首先, 高功耗任务要分析设备功耗优化的占比情况,然后逐一进行排查
影响高功耗的设备主要有NetWorrk、GPS、Audio、Camera、Display、GPU、BlueTooth和Sensor等。详见下图:
小木箱将所有高功耗设备按照MECE原则进行分类如下表格:
设备 | 功耗影响因子 |
Display | 亮度、显示内容、刷新率 |
CPU | 工作频率、工作负载、运算内容 |
GPU | 工作频率、工作负载、绘制内容、分辨率、绘制帧率 |
WIFI | 信号、协议(影响收发速率)、工作频率(2.4G , 5G) |
Audio | 音量、音效算法 |
GPS | 频率、定位精度 |
Modem | 天线信号强度、协议(如5g功耗远高于4g)、环境(如高铁上会频繁测量搜网) |
Camera | 摄像头(主摄、微距、广角、前置、tof等)工作模式(预览、拍照和录像)分辨率、录制帧率 |
小木箱建议大家使用Android的电池监控工具来监测APP的电池使用情况
根据业务表现不同特征, 尽量使用系统 API、减少后台进程、避免让手机长时间处于高亮度模式、合理使用 GPS 和网络、优化图像和视频、尽量避免定时高功耗任务和避免频繁的唤醒 CPU方式进行优化功耗。
Android常见的高功耗设备可以参考下图:
那么, 该如何找出正在运行的高耗设备?
查找高功耗电Android设备,需要使用Android电池管理API,详细代码如下:
BatteryStats batteryStats = BatteryStatsHelper.getBatteryStats(context, batteryInfo); Map<String, ? extends BatteryStats.Uid> batteryData = batteryStats.getUidStats(); for (Map.Entry<String, ? extends BatteryStats.Uid> entry : batteryData.entrySet()) { BatteryStats.Uid uid = entry.getValue(); int uidBatteryUsage = uid.getBatteryPercentOfTotal(); if (uidBatteryUsage > 0) { // Use this information to identify which APPs are consuming the most battery power. } }
首先,高功耗任务需要新增android.permission.BATTERY_STATS
权限,方便使用电池管理 API。
然后,使用Android电池管理API获取APP的电池使用情况,并循环遍历每个APP,最后, 确定它们对总电池使用情况的贡献。
最后,如果APP对总电池使用情况的贡献大于零,那么可以将该APP标识为消耗大量电力的APP。
减少设备使用方式详见有如下思维导图:
下面我们逐一讲解着五种减少设备使用的方式
2.4.1 降低亮度
WindowManager.LayoutParams layout = getWindow().getAttributes(); layout.screenBrightness = 0.5F; getWindow().setAttributes(layout);
在上面的代码中,通过获取当前窗口的布局参数,并将其中的屏幕亮度设置为 0.5F。请注意,此亮度值仅在 0 到 1 之间。
因此,如果希望更低的亮度,则可以将此值设置为更小的值。
请注意,需要在活动中调用此代码,以便更改当前活动的屏幕亮度。
此外,还需要检查是否具有 android.permission.WRITE_SETTINGS
权限,以便更改系统设置。
2.4.2 降低动画帧率
getWindow().setWindowAnimationsScale(0.5f);
首先,请确保有SYSTEM_ALERT_WINDOW
设置的权限。
然后,通过调用 setWindowAnimationsScale
方法并将其参数设置为0.5f,可以降低当前活动的动画帧率,该值仅在0到1之间。
最后, 需要在Activity中调用上面的代码就可以更改当前Activity的动画帧率
2.4.3 减少数据收发
减少数据收发的方式有四种,分别是
- 使用后台线程:在后台线程中执行网络请求,而不是在主线程中,以避免阻塞主线程。
- 缓存数据:使用本地缓存存储经常使用的数据,以避免不必要的网络请求。
- 压缩数据:在发送数据前对数据进行压缩,以减少数据量。
- 避免不必要的数据收发:使用状态管理,避免对已经存在的数据进行不必要的收发。
以下是一个使用 Retrofit 和 OkHttp 实现网络请求的代码示例,并将数据缓存到本地:
public class NetworkService { private static final String BASE_URL = "https://github.microkibaco.com/api/"; private static final int CACHE_SIZE = 10 * 1024 * 1024; // 10 MB private static NetworkService instance; private Retrofit retrofit; private OkHttpClient client; private NetworkService() { // 创建缓存目录 File cacheDirectory = new File( Environment.getDownloadCacheDirectory(), "retrofit-cache"); Cache cache = new Cache(cacheDirectory, CACHE_SIZE); // 配置 OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); // 配置 Retrofit retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .client(client) .build(); } public static NetworkService getInstance() { if (instance == null) { instance = new NetworkService(); } return instance; } public Retrofit getRetrofit() { return retrofit; } } 复制代码
2.4.4 降低音量
在 Android 代码层面,可以使用以下代码来降低音量,从而优化电池使用寿命:
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC), 0);
该代码将音频流的音量设置为最低值,从而降低手机的音量。
2.4.5 治理CPU高负载
治理CPU高负载异常并优化电池使用寿命常见方式有四个:
检测 CPU 负载异常可以通过使用 Android 提供的两个工具。
第一, adb shell top
第二, adb shell dumpsys CPUinfo
分析负载异常原因方式也有两种:
第一使用Android Studio的“Profile GPU Rendering”
第二使用“Debug GPU Overdraw”功能。
如果分析结果是APP代码引起的负载异常,那么可以减少不必要的动画、使用多线程等来优化 CPU 负载。
如果分析结果表明是后台服务引起的负载异常,那么可以通过关闭服务来减少CPU负载。
小木箱后文会对CPU负载进行详细分析
2.5 How: 功耗优化流程
当执行高功耗任务时候,业务开发应该反思下面四个问题:
- 高功耗任务是否必须要做?
- 高功耗任务是否必须准时做?
- 高功耗任务是否必须保证效果?
- 高功耗任务必须的方案是否具有唯一性?
关于问题1: 高功耗任务是否必须要做? 如果高功耗任务不是必须要做的,业务开发可以考虑延迟或不执行,达到动画泄漏治理,让后台高功耗任务延迟做
关于问题2: 高功耗任务是否必须准时做?如果高功耗任务不是必须准时做的,高功耗任务业务开发可以考虑分批减少频率,达到MQTT心跳降级,GPS定位频率因此会降低
关于问题3: 高功耗任务是否必须保证效果?如果高功耗任务不是必须保证效果,高功耗任务业务开发可以考虑降低效果,常见的做法是算法降级如:定位降级为网络定位
关于问题4: 高功耗任务必须的方案是否具有唯一性?如果高功耗任务必须的方案不具有唯一性,高功耗任务业务开发可以考虑硬解密替代软解密,厂商Push代替自建长链接
功耗设备降级的方式主要有异常治理、正面优化和业务降级四种.
异常治理有三种情况: 死循环治理、资源不释放治理和高频耗时函数优化
正面优化有三种情况: 动态帧率和刷新率、绘制图层优化和长链接心跳优化
业务降级有三种情况: 预加载降级、常驻动画降级和超分算法降级
后文小木箱会逐一进行讲解
2.6 Where: 方案使用场景
方案使用场景方案使用场景小木箱这里只会带大家了解一下异常治理和无损降级,后文会讲解有感降级。
2.6.1 异常治理
异常处理不是直接解决Android功耗优化的方法,但是通过异常处理可以避免不必要的资源浪费,从而减少功耗消耗。
高功耗任务可以通过避免不必要的资源分配、避免不必要计算、不必要数据库操作等异常处理解决Android功耗问题。
2.6.2 业务降级(无感知)
使用JobScheduler代替AlarmManager:JobScheduler可以在系统空闲时执行高功耗任务,因此可以降低功耗消耗。
使用后台服务降级:对于需要长时间运行的后台服务,可以通过降级方式(例如,从运行在前台的服务切换到后台定时高功耗任务)来减少功耗消耗。
使用Doze模式优化:Doze模式可以在系统进入空闲状态时减少后台活动,从而减少功耗消耗。
使用Firebase Cloud Messaging(FCM)代替定时高功耗任务:FCM可以在系统空闲时向APP发送消息,从而减少功耗消耗。
使用Battery Optimizations API:Battery Optimizations API可以在系统空闲时减少APP的活动,从而减少功耗消耗。
三、功耗优化技术方案
功耗优化技术方案主要分为四个部分,第一个部分是功耗优化工具,第二个部分是功耗优化指南,第三个部分是功耗优化SOP。
3.1 功耗优化工具
功耗优化工具有八个可以聊聊,分别是电流仪、Battery Historian、Energy Profiler、Dumpsys Batterystats、Battery-Metrics、BatteryCanary、Diggo、和Systrace
3.1.1 电流仪
优点
最准确的测试方法是稳压电源+电流仪, 稳压电源+电流仪优点是可以测试整机电流,并且数据精准
缺点
因为稳压电源+电流仪需要准备硬件工具,稳压电源+电流仪测试操作复杂,并且稳压电源+电流仪不能测试APP消耗电量,一般没有人在移动开发中使用稳压电源+电流仪
使用场景
ROM厂商整机电流科学实验
3.1.2 Battery Historian
传送门: 使用 Battery Historian 分析耗电情况
使用指南(待更新): space.bilibili.com/455432199
参数说明
Historian
参数 | 说明 |
Battery Level | 电量 |
Userpace WakeLock | 用户唤醒锁 |
Long WakeLocks | 长期持有唤醒锁事件 |
GPS | 是否使用GPS |
Wifi radio | 是否正在通过wifi传输数据 |
Network Connectivity | 网络连接状态(wifi、mobile是否连接) |
Mobile signal strength | 移动信号强度(great/good/moderate/poor) |
Mobile network type | 移动信号类型 |
Audio | 音频是否开启 |
Camera | 相机是否在工作 |
Video | 是否在播放视频 |
Temperature | 电池温度 |
App Selection
参数名 | 说明 |
Device estimated power use | 预计耗电量 |
Vibrator use | 手机振动器 |
Wakelocks | 唤醒锁事件 |
Services | app开启的服务 |
优点
- Battery Historian以图表形式展示,对APP耗电情况有详细数据展示,同步还提供了手机当前的状况,方便分析APP的具体原因
- Battery Historian有耗电量的详细数据如: APP的使用时间、电池消耗、唤醒锁定和CPU使用情况
- Battery Historian可以追踪应用程序的后台运行时间、网络使用情况、设备唤醒频率等
- Battery Historian可以确定哪些APP或服务高功耗
缺点
- Battery Historian只适用于Android5.0及以上系统
- Battery Historian无法定位具体是什么方法或者功能引起的耗电异常,无法获取APP异常运动时的堆栈信息
- Battery Historian收集的数据是历史数据,非实时电耗情况。
- Battery Historian收集 CPU 使用情况、网络数据传输等需要 root 权限。
使用场景
- 诊断应用程序、系统服务和硬件组件的电池消耗问题
- 评估Android设备电池的使用寿命信息
- 比较不同设备、不同应用程序或不同操作系统版本的电池消耗情况
3.1.3 Energy Profiler
传送门: developer.android.com/studio/prof…
使用指南(待更新): space.bilibili.com/455432199
优点
- Energy Profiler 可以提供实时的电池消耗
- Energy Profiler 可以针对特定的应用程序进行电池消耗分析,而不是整台设备
- Android Studio自带Buff,无需安装插件
- Energy Profiler提供CPU 使用情况、传感器使用情况和网络数据传输能力
- Energy Profiler支持历史数据分析
缺点
- 无法多端复用
- 只支持Android 8.0以上版本
- 不提供高功耗解决方案
使用场景
- 测试电池寿命
- 比较不同版本的应用程序
- 确认电池相关问题
3.1.4 Dumpsys Batterystats
传送门: developer.android.com/studio/comm…
使用指南(待更新): space.bilibili.com/455432199
优点
- Dumpsys Batterystats可以监测系统服务和其他应用程序等系统级别的电池消耗
- Dumpsys Batterystats可以监测应用程序在使用电池时的功耗情况,并将其记录在日志文件中
- Dumpsys Batterystats会生成应用程序、系统服务、电池消耗原因和耗电量使用情况报告
缺点
- 无法检测应用程序和服务之间的相互影响
- Dumpsys Batterystats测试只能在设备上运行一段时间后收集数据
- Dumpsys Batterystats方式获取的是原始的耗电记录数据,需要人肉查找关键信息,可读性不高
使用场景
- 电池寿命测试
- 电源管理优化
- Dumpsys Batterystats帮助用户排除设备电池故障
3.1.5 Battery-Metrics
使用指南(待更新): space.bilibili.com/455432199
优点
- Battery-Metrics可以检测到应用程序和服务之间的相互影响
- Battery-Metrics测试功耗方法和技术比Batterystats精准
- Battery-Metrics测试功耗能够检测用户APP使用习惯、网络连接质量、屏幕亮度、环境温度
- Battery-Metrics支持多种设备和操作系统版本
- Battery-Metrics可以通过hook和插桩的方式上报关键节点的电池和手机相关数据,可高度定制化
缺点
Battery-Metrics插件方案Hook点位受到API版本限制
使用场景
不同领域和高功耗优化场景高可用
3.1.6 BatteryCanary
使用指南(待更新): space.bilibili.com/455432199
BatteryCanary也可以通过hook和插桩的方式上报关键节点的电池和手机相关数据,已实现关键点的插桩和hook
优点
- 实时监测
- 易于集成
- 精细化分析
- 易于定位问题
- 开源免费
缺点
- 对测试设备要求较高
- 有些数据可能不够准确
- 导致应用程序的安装包变得更大
使用场景
不同领域和高功耗优化场景高可用
3.1.7 Diggo
使用指南(待更新): space.bilibili.com/455432199
优点
- Diggo 可以对应用程序的每个组件进行功耗采样和统计,得到精确的功耗数据,并可以将功耗数据可视化,便于开发人员分析和优化
- Diggo 提供了一些自动化的功耗优化策略,例如优化启动过程、减少资源使用等
- Diggo可以与Traceview、Jupyter进行集成
缺点
- 侵入业务代码
- 无法处理硬件功耗问题
- 可能会影响应用程序的性能
- 无法跨端使用
使用场景
- 开发人员和测试团队对App进行基准测试
- 优化应用程序的电池寿命
3.1.8 Systrace
传送门: blog.csdn.net/u011578734/…
使用指南(待更新): space.bilibili.com/455432199
优点
精确定位功耗问题:Systrace 可以提供精确到微秒级别的时间戳,定位功耗问题问题时间和位置。
缺点
Systrace 只能监测 Android 系统本身以及应用程序,但不能监测硬件功耗,例如屏幕、摄像头、传感器等硬件设备的功耗
使用场景
识别瞬时功耗:Systrace 可以捕捉瞬时功耗事件,例如启动应用程序、打开相机等操作。
可以识别到不必要的系统服务或者过多的数据同步操作等后台功耗问题
3.2 功耗优化指南
3.2.1 APP功耗统计流程
厂商耗电排行
在Android手机上,可以按照 设置 → 电池 → 电池百分比 步骤查看耗电排行
“耗电排行”显示的是自上次充满电后的一个累计情况,很难满足测试需求
关于耗电有一个很重要的API BatteryStatsHelper
首先, BatteryStatsHelper 通过createFromParcel方法统一耗电口径,然后,再通过addPhoneUsage,最后, 通过refreshStats显示最近数天甚至一周以上的 APP 的电量统计数据。详细源码调用链参考如下:
private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; private static BatteryStatsImpl getStats(IBatteryStats service) { try { ParcelFileDescriptor pfd = service.getStatisticsStream(); if (pfd != null) { //--------------------------------- try { //--------------------------------- BatteryStatsImpl stats = com.android.internal.os.BatteryStatsImpl.CREATOR .createFromParcel(parcel); // 统计口径 //--------------------------------- return stats; } catch (IOException e) { Log.w(TAG, "Unable to read statistics stream", e); } } } catch (RemoteException e) { Log.w(TAG, "RemoteException:", e); } return new BatteryStatsImpl(); } } // 耗电数据集 private void addPhoneUsage() { long phoneOnTimeMs = mStats.getPhoneOnTime(mRawRealtime, mStatsType) / 1000; double phoneOnPower = mPowerProfile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE) * phoneOnTimeMs / (60*60*1000); if (phoneOnPower != 0) { BatterySipper bs = addEntry(BatterySipper.DrainType.PHONE, phoneOnTimeMs, phoneOnPower); } } private void processMiscUsage() { //--------------------------------- addPhoneUsage(); //--------------------------------- } // 有的 Android 设备系统可以显示最近数天甚至一周以上的 APP 的电量统计数据 public void refreshStats(int statsType, SparseArray<UserHandle> asUsers, long rawRealtimeUs, long rawUptimeUs) { // Initialize mStats if necessary. // 计算APP的耗电 processAPPUsage(asUsers); // 计算杂项的耗电 }
电量计算公式
Android APP中,可以使用以下代码获取设备当前的电量信息:
// 获取电池管理器对象 BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE); // 获取当前电量 int currentBatteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); // 获取电池容量(需要 API Level 28 及以上支持) long batteryCapacity = batteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
通过上述代码获取到的电量和电池容量可以使用上面提到的公式计算剩余电量。
注意,电池容量的单位为毫安时(mAh),需要将其转换为标准单位(例如,使用瓦时,1 毫安时 = 0.001 瓦时)。
电量计算公式是:
模块电量(mAh)= 模块电流(mA)*模块耗时(h)
耗电器件模块主要是Android 设备的各种硬件模块,主要可以分为以下三类:
第一类,像 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。
硬件电量统计
功率:power_profile.xml,Android 系统使用此文件来描述设备各个硬件模块的额定功率,包括上面提到的多档位功率和 CPU 电量算需要到的各种参数值。
时长:StopWatch & SamplingCounter,其中 StopWatch 是用来计算 APP 各种硬件模块的使用时长,而 SamplingCounter 则是用来采样统计 APP 在不同 CPU Core 和不同 CPUFreq 下的工作时长。
计算:PowerCalculators,每个硬件模块都有一个相应命名的 PowerCalculator 实现,主要是用来完成具体的电量统计算法。
存储:batterystats.bin,电量统计服务相关数据的持久化文件。
3.2.2 APP功耗配置文件
通常,power_profile.xml位于/system/framework/framework-res.apk中,这是一个android设备的系统apk,高功耗任务可以通过
adb pull /system/framework/framework-res.apk ./
获取当前系统的framework-res apk,这步不需要root即可进行,接着高功耗任务可以通过反编译工具,apktool或者jadx都可以,对该apk进行反编译处理,高功耗任务所需要的功耗文件就在 /res/xml/power_profile.xml 中。
3.2.3 APP功耗优化模式
Android的Doze模式是一种省电模式,旨在减少设备的能耗。当设备进入Doze模式时,Android会暂停一些不必要的后台活动,包括网络访问、同步、定位和其他不必要的服务。这样可以降低设备的能耗并延长电池寿命。Doze模式分为两种:一种是Light Doze,一种是Deep Doze。
Doze模式
Light Doze会在设备未使用时激活,比如用户长时间未操作设备或将设备放置不用,它会限制APP的后台活动和一些系统服务。
而Deep Doze则需要设备完全静止一段时间,只有当设备完全静止一段时间后,才会激活Deep Doze。在Deep Doze下,所有APP和系统服务都将被暂停,直到设备被唤醒。
需要注意的是,Doze模式并不会完全关闭设备,而只是将设备进入一种低功耗状态。当用户开始使用设备时,设备会自动退出Doze模式,并恢复正常的工作状态。
// PowerManager提供的两种判断当前是否处于Doze模式方法 public boolean isDeviceIdleMode(){ try { return mService.isDeviceIdleMode(); } catch (RemoteException e){、3603 throw e.rethrowFromSystemServer(); } @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DEVICE_IDLE_MODE_CHANGED = "android.os.action.DEVICE_IDLE_MODE_CHANGED";
Deep Doze
系统处于息屏状态,并且30分钟不移动的情况下,就会进入到Deep Doze 模式,Deep Doze 机制中有七种状态,分别如下
//mState值,表示设备处于活动状态 private static final int STATE_ACTIVE = 0; //mState值,表示设备处于不交互状态,灭屏、静止 private static final int STATE_INACTIVE = 1; //mState值,表示设备刚结束不交互状态,等待进入IDLE状态 private static final int STATE_IDLE_PENDING = 2; //mState值,表示设备正在感应动作 private static final int STATE_SENSING = 3; //mState值,表示设备正在定位 private static final int STATE_LOCATING = 4; //mState值,表示设备处于空闲状态,也即Doze模式 private static final int STATE_IDLE = 5; //mState值,表示设备正处于Doze模式,紧接着退出Doze进入维护状态 private static final int STATE_IDLE_MAINTENANCE = 6;
- 当设备亮屏或者处于正常使用状态时其就为ACTIVE状态;
- ACTIVE状态下不充电且灭屏设备就会切换到INACTIVE状态;
- INACTIVE状态经过3分钟,期间检测没有打断状态的行为就切换到PRE_IDLE的状态;
- PRE_IDLE状态经过5分钟,期间无打断就进入到IDLE状态
- 进入IDLE状态会根据是否有网络连接选择进入WAITING_FOR_NETWORK还是进入MAINTENANCE窗口期,进入窗口期的时间为:5分钟,10分钟,最后稳定最长为15分钟
- 进入WAITING_FOR_NETWORK会持续5分钟后重新进入到IDLE状态
- 进入MAINTENANCE会解除耗电策略的限制,并在1分钟后重新进入到IDLE状态
Light Doze
从上面可以看到想要进入Doze模式的条件是很苛刻,需要在手机息屏并且没有移动的状态下才能进入,所以Android7.0开始引入了Light Doze,处于息屏状态,但仍处于移动状态可进入Light Doze,LightDoze有7个状态,分别如下:
//mLightState状态值,表示设备处于活动状态 private static final int LIGHT_STATE_ACTIVE = 0; //mLightState状态值,表示设备处于不活动状态 private static final int LIGHT_STATE_INACTIVE = 1; //mLightState状态值,表示设备进入空闲状态前,需要等待完成必要操作 private static final int LIGHT_STATE_PRE_IDLE = 3; //mLightState状态值,表示设备处于空闲状态,该状态内将进行优化 private static final int LIGHT_STATE_IDLE = 4; //mLightState状态值,表示设备处于空闲状态,要进入维护状态,先等待网络连接 private static final int LIGHT_STATE_WAITING_FOR_NETWORK = 5; //mLightState状态值,表示设备处于维护状态 private static final int LIGHT_STATE_IDLE_MAINTENANCE = 6;
根据上图,他们的转换关系总结如下:
- 当设备亮屏或者处于正常使用状态时其就为ACTIVE状态;
- ACTIVE状态下不充电且灭屏设备就会切换到INACTIVE状态;
- INACTIVE状态经过3分钟,期间检测没有打断状态的行为就切换到PRE_IDLE的状态;
- PRE_IDLE状态经过5分钟,期间无打断就进入到IDLE状态
- 进入IDLE状态会根据是否有网络连接选择进入WAITING_FOR_NETWORK还是进入MAINTENANCE窗口期,进入窗口期的时间为:5分钟,10分钟,最后稳定最长为15分钟
- 进入WAITING_FOR_NETWORK会持续5分钟后重新进入到IDLE状态
Standby模式
Android Standby模式是一种类似于Doze模式的省电模式,它会在设备没有活动时将设备置于低功耗状态。在Standby模式下,APP的后台活动和系统服务将被暂停,但仍可以使用特定的API允许部分后台活动和网络访问。
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); if (powerManager.isIgnoringBatteryOptimizations(getPackageName())) { // 已经允许APP忽略电量优化 // 开始执行后台高功耗任务和网络访问 // ... } else { // 请求允许APP忽略电量优化 Intent intent = new Intent(); intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); }
以上代码获取了PowerManager对象,并使用isIgnoringBatteryOptimizations()方法检查APP是否允许忽略电量优化。
如果已经允许,则可以开始执行后台高功耗任务和网络访问。如果没有允许,则需要使用ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS intent请求用户允许APP忽略电量优化。
需要注意的是,与Doze模式类似,启用Standby模式并不意味着所有后台活动和网络访问都将被暂停。
APP需要使用特定的API允许必要的后台活动和网络访问,以确保在Standby模式下能够正常工作。
3.2.4 APP功耗工作流程
BatteryStatsService 的工作流程大致可以分为两个部分:时长统计 & 功耗计算。
BatteryStatsService 时长统计流程
BatteryStats
BatteryStatsService 框架的核心是 BatteryStatsService 持有的一个叫 BatteryStats 的类,BatteryStats 又持有一个 Uid [] 数组,每一个 Uid 实例实际上对应一个 APP,当高功耗任务安装或者卸载 APP 的时候,BatteryStats 就会更新相应的 Uid 元素以保持最新的映射关系。同时 BatteryStats 持有一系列的 StopWatch 和 SamplingCounter,当 APP 开始使用某些硬件模块的功能时,BatteryStats 就会调用相应 Uid 的 StopWatch 或 SamplingCounter 来统计其硬件使用时长。
Wifi 模块
这里以 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 功耗计算流程
BatteryStatsHelper
具体电量计算方面,BatteryStats 是通过 BatteryStats 依赖的一个 BatteryStatsHelper 的辅助类来完成的。BatteryStatsHelper 通过组合使用 Uid 里的时长数据、PoweProfile 里的功率数据(power_profile.xml 的解析实例)以及具体各个模块的 PowerCalculator 算法,计算出每一个 APP 的综合电量消耗,并把计算结果保存在 BatterySipper [] 数组里(按计算值从大到小排序)。
WIFI模块
还是以 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); } public void noteWifiOnLocked() { if (!mWifiOn) { final long elapsedRealtime = mClocks.elapsedRealtime(); final long uptime = mClocks.uptimeMillis(); mHistoryCur.states2 |= HistoryItem.STATE2_WIFI_ON_FLAG; addHistoryRecordLocked(elapsedRealtime,uptime); mWifiOn = true; mWifiOnTimer.startRunningLocked(elapsedRealtime); scheduleSyncExternalStatsLocked("wifi-off",ExternalStatsSync.UPDATE_WIFI); } } }
Audio模块
Activity状态改变
3.3 功耗优化SOP
3.3.1 统计指标
- app处于前台时,提供电流作为耗电指标
- 通过采集app的CPU、流量和GPS等的使用,来计算出一个加权和作为耗电指标
- 电池温度,作为衡量耗电的辅助参考
3.3.2 线下监测
3.3.2.1 监测专项
参数 | 参数说明 |
test_time | 监测时间 |
power_time | 监测耗时 |
power_env | 功耗环境 |
power_consumption | 功耗应用 |
data_src | 数据来源 |
power_consumption_device | 功耗设备 |
device_version | 设备版本 |
total_battery_capacity | 电池容量 |
battery_health_status | 电池健康度 |
power_detector | 功耗监测人 |
3.3.2.2 监测SOP
监测时间段内,在游戏回播业务播放同一个测试回播资源20分钟。
监测时间内,确保应用走完倍速、音量梯度调整等播放主流程。
监测时间内,测量时间段内确保游戏回播全程播放。
监测时间内,确保游戏回播不切换视频资源。
监测过程中,屏幕保持常亮状态,亮度中等。因为屏幕唤醒本身就会有耗电开销。
监测过程中,使用WiFi网络,因为蜂窝测试数据不具备公正性。
监测过程中,把没有用到的传感器关掉,且不要充电。
3.3.2.3 监测结论
器件名称 | 耗电占比 |
WIFI | xxx% |
Camera | xxx% |
Audio | xxx% |
Screen | xxx% |
NetWork | xxx% |
Display | xxx% |
合计 | xxx% |
异常: 电池温度骤增,发热明显
3.3.2.4 监测数据
3.3.2.4.1 手机硬件运行状况及电量消耗曲线图
- 测量过程中,耗电1%需要的时间大概xxx分钟
- 测量过程中,应用耗电曲线平滑,耗电波动稳定
- 测量过程中,保持屏幕常亮,长时间开启Audio和Screen无法进入深度休眠,导致手机耗电严重
3.3.2.4.2 手机各应用及硬件耗能
以上数据为部分应用及硬件的耗电估算数据,仅供参考
3.3.2.5 问题分析
监测期间影响APP高功耗的三个原因是CPU、Audio和Screen三个组件,可从中寻求优化方案
3.3.3 功耗归因
- 高CPU可以通过CPU菜单查看高耗CPU的堆栈
- GPS(location)、Alarm和WakeLock使用在超过指定持有时间和频次后,会上报当时的采集堆栈
3.3.4 治理目标
- 消除主流手机的高功耗提醒
- 建立健全的功耗监控及防劣化体系
3.3.5 耗电治理
3.3.5.1 分模块治理
模块的耗电治理主要体现在下面几个方面
3.3.5.1.1 CPU
死循环函数、高频函数、高耗时函数和无效函数等不必要的CPU消耗或消耗较多的函数治理
CPU使用率较高的场景及业务治理
3.3.5.1.2 GPU&Display
过度绘制、过多的动画和不可见区域的动画等浪费GPU的场景治理
主动降低屏幕亮度,使用深色UI等方案降低屏幕电量消耗
3.3.5.1.3 NetWork
不影响业务和性能前提下,降低网络访问频率
Doze状态时减少无效的网络请求
3.3.5.1.4 GPS
对使用GPS的场景,如小程序等,合理的降低精度,减少请求频率
3.3.5.1.5 Audio、Camera、Video等
3.3.5.2 分状态治理
除了分模块治理,还针对状态进行治理,主要状态有这几种
3.3.5.2.1 Foreground状态
渲染场景优化
音视频等场景优化
……
3.3.5.2.2 Background状态
task任务降频或者丢弃
网络访问降频,适配Doze模式
减少CPU消耗较多的函数执行
减少GPS等高功耗场景
3.3.5.3 防腐化建设
为了能更好的进行治理,完善的功耗分析和监控体系是不可避免的,不然就会出现无的放矢的状态。在这一块主要建设的点有
3.3.5.3.1 CPU监控
前后台高CPU消耗场景监控,高CPU消耗线程监控
高频task、高耗时task和后台task监控
消耗较高、耗时较高的函数监控
3.3.5.3.2 GPU&Display监控
动画场景、过度绘制检测、View层级检测和屏幕电量消耗监控等
3.3.5.3.3 NetWork监控
Rust、OkHttp及其他网络请求场景,频率,消耗监控
后台网络访问监控
3.3.5.3.4 GPS监控
GPS使用场景、时长、电量消耗监控
3.3.5.3.5 Audio、Camera、Video等监控
使用场景、时长、电量消耗监控
3.3.5.3.6 Global电量监控
整体的电量消耗和不同场景的电量消耗,用来度量版本功耗的质量