耗电的背景知识

  1. 电池技术:电池容量,充电时间,寿命,安全性;
  2. 电量和硬件:应用程序不会直接去消耗电池,而是通过使用硬件模块消耗相应的电能;CPU、屏幕、WiFi 和数据网络、GPS 以及音视频通话都是我们日常的耗电大户。
  3. 电量和应用程序:电能 = 电压 * 电流 * 时间,手机电压一般不会改变,所以模块电量(mAh) = 模块电流(mA) * 模块耗时(h);
    • 模块电流应该怎样去获取呢?Android 系统要求不同的厂商必须在 /frameworks/base/core/res/res/xml/power_profile.xml 中提供组件的电源配置文件。
    • Android 系统的电量计算PowerProfile也是通过读取power_profile.xml的数值而已;
    (1). 不同的厂商具体的数值都不太一样,我们可以通过下面的方法获取:1. 从手机中导出/system/framework/framework-res.apk文件。2. 使用反编译工具(如 apktool)对导出文件framework-res.apk进行反编译。3. 查看power_profile.xml文件在framework-res反编译目录路径:/res/xml/power_profile.xml。(2). 系统的电量消耗情况,我们可以通过 dumpsys batterystats 导出:adb shell dumpsys batterystats > battery.txt// 各个Uid的总耗电量,而且是粗略的电量计算估计。Estimated power use (mAh):Capacity: 3450, Computed drain: 501, actual drain: 552-587...Idle: 41.8Uid 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(3).当测试或者其他人反馈耗电问题时,bug report结合Battery Historian是最好的排查方法。//7.0和7.0以后$ adb bugreport bugreport.zip//6.0和6.0之前:$ adb bugreport > bugreport.txt//通过historian图形化展示结果python historian.py -a bugreport.txt > battery.html

Android 耗电的演进历程

  1. 野蛮生长:Pre Android 5.0

    • Android 5.0 之前,系统并不是那么完善,对于电量优化相对还是比较少的。特别没有对应用的后台做严格的限制,多进程、fork native 进程以及广播拉起等各种保活流行了起来。
  2. 逐步收紧:Android 5.0~Android 8.0
    • Android 5.0 专门开启了一个Volta 项目,目标是改善电池的续航。在优化电量的同时,还增加了的 dumpsys batteryst 等工具生成设备电池使用情况统计数据。
      (5.0: Volta项目,JobScheduler,dumpsys batterystats,BatteryHistorian,修复native fork进程保活的bug)
    • 从 Android 6.0 开始,Google 开始着手清理后台应用和广播来进一步优化省电。
      (6.0: 省电功能,Doze低功耗模式,AppStandby应用待机模式)
      (7.0: 优化省电功能,Doze加强版,implicit broadcasts限制,混合编译)
      (8.0: 更多优化省电功能,后台执行限制,后台位置限制)
  3. 最严限制:Android 9.0
    • 从 Android 9.0 开始,Google 对电源管理引入了几个更加严格的限制。
      (9.0: 应用待机分组AppStandbyBueckets,应用后台限制BackgroundRestrictions,省电模式BatterySaver)

耗电优化

什么是耗电优化

  • 所谓的耗电优化不就是减少应用的耗电,增加用户的续航时间吗?
  • 但是落到实践中,如果我们的应用需要播放视频、需要获取 GPS 信息、需要拍照,这些耗电看起来是无法避免的。

从哪些方面优化

1. 后台耗电:
  • 用户对于实际经常使用的应用耗电是有预期的,但是如果一个不常用的应用耗电耗却非常多就会很容易引起关注,所以电优化的第一个方向是优化应用的后台耗电;例如长时间获取 WakeLock、WiFi 和蓝牙的扫描等。
2. 符合系统的规则
  • Android P 是通过 Android Vitals 监控后台耗电,所以我们需要符合 Android Vitals 的规则
后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠:
1. Alarm Manager Wakeup唤醒过多:手机非充电状态时,每小时唤醒次数大于10次;
2. 频繁使用局部唤醒锁:手机非充电状态时,partial wake lock持有超时1小时;
3. 后台网络使用过高:手机非充电状态时,且应用在后台,每小时网络使用量超过50MB;
4. 后台wifi scans过多:手机非充电状态时,且应用在后台,每小时大于4次;
耗电优化的几个问题
  1. 缺乏现场,无法复现;
  2. 信息不全,难以定位;
  3. 无法评估结果;
为什么需要在后台耗电
  1. 某个需求场景。最普遍的场景就是推送,为了实现推送我们只能做各种各样的保活。在需求面前,用户的价值可能被排到第二位。
  2. 代码的 Bug。因为某些逻辑考虑不周,可能导致 GPS 没有关闭、WakeLock 没有释放。

耗电优化的方法

  1. 找到需求场景的替代方案,后台任务的总体指导思想是减少、延迟和合并

    • 推送:

      1. 厂商通道;
      2. 定时拉取最新消息;
      3. foreground service或引导用户加入白名单;
    • 若需要后台运行:
      1. 长时间下载:DownloadManager
      2. 数据同步:SyncAdapter(尽可能的打包所有需要同步的任务在一个周期中执行)
      3. 本地任务:JobScheduler(minApi21,Google官方建议网络请求相关业务放到JobScheduler)
        /*** 开启 JobScheduler*/private void startJobScheduler() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), JobSchedulerService.class.getName()));// 设置仅在 充电和WIFI 下才使用 JobScheduler 进行批量任务处理builder.setRequiresCharging(true).setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);jobScheduler.schedule(builder.build());}}//JobSchedulerService 就是用于进行批量任务处理的服务/*** 用于进行批量任务处理的 JobSchedulerService*/@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)public class JobSchedulerService extends JobService {@Overridepublic boolean onStartJob(JobParameters params) {// 此处执行在主线程// 模拟一些处理:批量网络请求,APM日志上报return false;}@Overridepublic boolean onStopJob(JobParameters params) {return false;}}
    4. 特定时间执行:AlarmManager(持有Wake Lock,时间间隔|重复的任务,不建议网络请求使用)5. 实时通信:FCM、MiPush6. 立刻执行:foreground service
  1. 符合Android规则

    • 系统大部分耗电监控是在没充电时,所以我们可以在充电时才做一些耗电工作;
    IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);Intent batteryStatus = context.registerReceiver(null, ifilter);//获取用户是否在充电的状态或者已经充满电了int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL;
- 尽早适配最新的TargetAPI,版本越高,系统的限制约严格;
  1. 异常情况监控

    • 监控异常情况并且上报日志,便于定位线上问题;

耗电监控

  • 仿照Android Vitals指定自己的规则;
使用Java Hook实现耗电监控:
  1. WakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。类似 Alarm、JobService 也会申请 WakeLock 来完成后台 CPU 操作。
//WakeLock 的核心控制代码都在PowerManagerService中。
// 代理PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);
@Override
public void beforeInvoke(Method method, Object[] args) {// 申请Wakelockif (method.getName().equals("acquireWakeLock")) {if (isAppBackground()) {// 应用后台逻辑,获取应用堆栈等等} else {// 应用前台逻辑,获取应用堆栈等等}// 释放Wakelock} else if (method.getName().equals("releaseWakeLock")) {// 释放的逻辑}
}
  1. Alarm 用来做一些定时的重复任务,它一共有四个类型,其中ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP类型都会唤醒设备。
//Alarm 的核心控制逻辑都在AlarmManagerService中
// 代理AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);
public void beforeInvoke(Method method, Object[] args) {// 设置Alarmif (method.getName().equals("set")) {// 不同版本参数类型的适配,获取应用堆栈等等// 清除Alarm} else if (method.getName().equals("remove")) {// 清除的逻辑}
}
  1. 其他

    • 对于后台 CPU,我们可以使用卡顿监控学到的方法。
    • 对于后台网络,同样我们可以通过网络监控学到的方法。
    • 对于 GPS 监控,我们可以通过 Hook 代理LOCATION_SERVICE。
    • 对于 Sensor,我们通过 Hook SENSOR_SERVICE中的“mSensorListeners”,可以拿到部分信息。
  2. 通过 Hook,我们可以在申请资源的时候将堆栈信息保存起来。当我们触发某个规则上报问题的时候,可以将收集到的堆栈信息、
    电池是否充电、CPU 信息、应用前后台时间等辅助信息也一起带上。
通过插桩实现耗电监控
  • 虽然使用 Hook 非常简单,但是某些规则可能不太容易找到合适的 Hook 点。而且在 Android P 之后,很多的 Hook 点都不支持了。
  • 以WakeLock为例
public class WakelockMetrics {// Wakelock 申请public void acquire(PowerManager.WakeLock wakelock) {wakeLock.acquire();// 在这里增加Wakelock 申请监控逻辑}// Wakelock 释放public void release(PowerManager.WakeLock wakelock, int flags) {wakelock.release();// 在这里增加Wakelock 释放监控逻辑}
}
  • Facebook 也有一个耗电监控的开源库Battery-Metrics
  • 课后练习https://github.com/AndroidAdvanceWithGeektime/Chapter19

电量检测方案

  1. 设置—耗电排行:直观,但没有详细数据,对解决问题帮助不大
  2. 使用广播监听电量变化—ACTION_BATTERY_CHANGED:价值不大:针对手机整体的耗电量,而非单个 App
  3. dumpsys batterystats:adb shell dumpsys batterystats > battery.txt
  4. Battery Historian:https://github.com/google/battery-historian

Gradle 耗电量统计插件中 BatteryCreateMethodVisitor 的核心实现代码

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {// 监控 WakelockString monitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/WakelockMetrics";if (!monitorClass.equals(className)&& "android/os/PowerManager$WakeLock".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "acquire".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/os/PowerManager$WakeLock;J)V",isInterface);return;}if (!monitorClass.equals(className)&& "android/os/PowerManager$WakeLock".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "release".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/os/PowerManager$WakeLock;)V",isInterface);return;}// 监控 GpsmonitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/GpsMetrics";if (!monitorClass.equals(className)&& "android/location/LocationManager".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "requestLocationUpdates".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/location/LocationManager;Ljava/lang/String;JFLandroid/location/LocationListener;)V",isInterface);return;}if (!monitorClass.equals(className)&& "android/location/LocationManager".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "removeUpdates".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/location/LocationManager;Landroid/location/LocationListener;)V",isInterface);return;}// 监控 Alarm ServicemonitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/AlarmMetrics";if (!monitorClass.equals(className)&& "android/app/AlarmManager".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "set".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/app/AlarmManager;IJLandroid/app/PendingIntent;)V",isInterface);return;}if (!monitorClass.equals(className)&& "android/app/AlarmManager".equals(owner)&& opcode == Opcodes.INVOKEVIRTUAL&& "cancel".equals(name)) {mv.visitMethodInsn(Opcodes.INVOKESTATIC,monitorClass,name,"(Landroid/app/AlarmManager;Landroid/app/PendingIntent;)V",isInterface);return;}super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}

参考文章

  • Android开发高手课-耗电优化(上):从电量优化的演进看耗电分析
  • Android开发高手课-耗电优化(下):耗电的优化方法与线上监控
  • 大众点评App的短视频耗电量优化实战
  • Android Vitals
  • Battery Historian
  • Android后台调度任务与省电
  • Android P 电量管理
  • 电源管理限制
  • 深入探索 Android 电量优化

我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

Android高手笔记 - 耗电优化相关推荐

  1. Android高手笔记 - 启动优化

    启动, 打开APP的必经之路, 第一体验,关系到用户留存和转化率等核心数据: 启动分析 启动类型 Android Vitals可以对应用冷,热,温启动时间做监控. 通过adb shell am sta ...

  2. Android高手笔记 - IO优化

    IO 优化不就是不在主线程读写大文件吗,真的只有这么简单吗? IO 基础 IO流程:应用程序 发送逻辑IO命令给文件系统,文件系统发送物理IO命令给存储设备/磁盘 文件系统 文件读(read)过程:应 ...

  3. Android高手笔记-D8, R8编译优化

    在之前的文章Android高手笔记-包体积优化中提到过通过编译优化包体积,涉及到了ProGuard,D8,R8,其中关于ProGuard及包体积优化方案已经进行了详细介绍,那么今天我们来说说D8和R8 ...

  4. Android高手笔记-屏幕适配 UI优化

    Android高手笔记-屏幕适配 & UI优化 屏幕与适配 由于Android碎片化严重,屏幕分辨率千奇百怪,而想要在各种分辨率的设备上显示基本一致的效果,适配成本越来越高: 屏幕适配究其根本 ...

  5. Android高手笔记 - 开篇 崩溃优化

    开篇-焦虑的移动开发者如何破局 移动互联网的发展不知不觉已经十多年了,Mobile First 也已经变成了 AI First.换句话说,我们已经不再是"风口上的猪". 可以说,国 ...

  6. Android高手笔记 - 网络优化

    很难找到一款完全不需要网络的应用,即使是单机应用,也会存在数据上报.广告等各种各样的网络请求 网络基础 Http & Https 定义: https:Hypertext Transfer Pr ...

  7. Android安装包体积优化

    APK瘦身经验小结_crazy_jack-CSDN博客 最近看滴滴开源的Dokit框架中有一个大图监控的功能,可以对图片的文件大小和所占用的内存大小设置一个阈值,当图片超过该值的时候进行提示. And ...

  8. Android耗电优化

    Android耗电优化 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq980106800/article/details/87811924 什么 ...

  9. Android 系统(102)---Android APP耗电优化

    Android APP耗电优化 可能造成耗电的一些原因 网络请求耗电,而且手机数据网络进行http请求比无线网进行http请求更加耗电,因为数据网络调用到一些底层的硬件模块,就如GPS一样,当手机打开 ...

最新文章

  1. Javascript的数据结构与算法(一)
  2. nginx搭建rtmp协议流媒体服务器总结
  3. Windows下Caffe的学习与应用(一)——训练自己的数据模型(GoogleNet)
  4. 【PAT乙级】1056 组合数的和 (15 分)
  5. 关于移动应用APP数据安全的一点见解
  6. 2021互联网医疗行业洞察
  7. mysql部署练习_MySQL主从练习 - osc_b9r67jnt的个人空间 - OSCHINA - 中文开源技术交流社区...
  8. C语言分支语句和循环语句及练习
  9. kettle使用教程(超详细)
  10. 黑马程序员:一些简单的设计模式
  11. 抗议浪潮不断,峰会笑声阵阵
  12. 字节跳动AI lab计算机视觉实习生面试教训
  13. 天才少年稚晖君 | 【保姆级教程】个人深度学习工作站配置指南
  14. 2022-2028年全球与中国插座行业市场深度调研及投资预测分析
  15. javascript停止页面所有计时器
  16. swift学习资料2022
  17. 团队沟通中的误区与技巧(zt)
  18. 入驻宣言:凡所见,皆可杀
  19. iOS使用dpkg删除包提示不存在
  20. C#高效编程 改进C#代码的50个行之有效的办法(第2版)

热门文章

  1. 让GIT BASH支持make
  2. WINUSBSTM32F205-F4WINUSB上位机和下位机源码提供
  3. IDP资料开发平台(Information Development Platform)
  4. 随感-想到哪儿写到哪儿
  5. HTTPS请求 Received fatal alert: handshake_failure异常---与众不同的原因
  6. Chrome无法访问网页(在此情况下,Firefox可以访问网页,QQ可以上网)
  7. xcode/Interface Build(IB)/iPhone模拟器/mac/组合键常用的命令集
  8. 在线电子书翻页效果 Turn.js
  9. js ajax定时器,js定时器的理解
  10. CSV逗号分隔值格式文件(示例分析)