0
点赞
收藏
分享

微信扫一扫

Android高手笔记 - 耗电优化

参考文章

耗电的背景知识


  1. 电池技术:电池容量,充电时间,寿命,安全性;
  2. 电量和硬件:应用程序不会直接去消耗电池,而是通过使用硬件模块消耗相应的电能;CPU、屏幕、WiFi 和数据网络、GPS 以及音视频通话都是我们日常的耗电大户。
  3. 电量和应用程序:电能 = 电压 * 电流 * 时间,手机电压一般不会改变,所以模块电量(mAh) = 模块电流(mA) * 模块耗时(h);
  4. (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.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 (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 系统要求不同的厂商必须在 /frameworks/base/core/res/res/xml/power_profile.xml 中提供组件的电源配置文件。
  • Android 系统的电量计算PowerProfile也是通过读取power_profile.xml的数值而已;


Android 耗电的演进历程


  1. 野蛮生长:Pre Android 5.0

  • Android 5.0 之前,系统并不是那么完善,对于电量优化相对还是比较少的。特别没有对应用的后台做严格的限制,多进程、fork native 进程以及广播拉起等各种保活流行了起来。

  1. 逐步收紧:Android 5.0~Android 8.0
  2. (5.0: Volta项目,JobScheduler,dumpsys batterystats,BatteryHistorian,修复native fork进程保活的bug)
  3. (6.0: 省电功能,Doze低功耗模式,AppStandby应用待机模式) (7.0: 优化省电功能,Doze加强版,implicit broadcasts限制,混合编译) (8.0: 更多优化省电功能,后台执行限制,后台位置限制)

  • 从 Android 6.0 开始,Google 开始着手清理后台应用和广播来进一步优化省电。

  1. 最严限制:Android 9.0
  2. (9.0: 应用待机分组AppStandbyBueckets,应用后台限制BackgroundRestrictions,省电模式BatterySaver)

  • 从 Android 9.0 开始,Google 对电源管理引入了几个更加严格的限制。


耗电优化

什么是耗电优化


  • 所谓的耗电优化不就是减少应用的耗电,增加用户的续航时间吗?
  • 但是落到实践中,如果我们的应用需要播放视频、需要获取 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或引导用户加入白名单;

  • 若需要后台运行:
  • /** * 开启 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 { @Override public boolean onStartJob(JobParameters params) { // 此处执行在主线程 // 模拟一些处理:批量网络请求,APM日志上报 return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 复制代码

  1. 特定时间执行:AlarmManager(持有Wake Lock,时间间隔|重复的任务,不建议网络请求使用)
  2. 实时通信:FCM、MiPush
  3. 立刻执行:foreground service


  1. 符合Android规则
  2. 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) {
// 申请Wakelock
if (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) {
// 设置Alarm
if (method.getName().equals("set")) {
// 不同版本参数类型的适配,获取应用堆栈等等
// 清除Alarm
} else if (method.getName().equals("remove")) {
// 清除的逻辑
}
}
复制代码

  1. 其他

  • 对于后台 CPU,我们可以使用卡顿监控学到的方法。
  • 对于后台网络,同样我们可以通过网络监控学到的方法。
  • 对于 GPS 监控,我们可以通过 Hook 代理LOCATION_SERVICE。
  • 对于 Sensor,我们通过 Hook SENSOR_SERVICE中的“mSensorListeners”,可以拿到部分信息。

  1. 通过 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
  • 课后练习​​github.com/AndroidAdva…​​

电量检测方案


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

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

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
// 监控 Wakelock
String 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;
}
// 监控 Gps
monitorClass = "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 Service
monitorClass = "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 电量优化​​

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

举报

相关推荐

0 条评论