电池技术:电池容量,充电时间,寿命,安全性;
电量和硬件:应用程序不会直接去消耗电池,而是通过使用硬件模块消耗相应的电能;CPU、屏幕、WiFi 和数据网络、GPS 以及音视频通话都是我们日常的耗电大户。
电量和应用程序:电能 = 电压 * 电流 * 时间,手机电压一般不会改变,所以模块电量(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.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 耗电的演进历程
野蛮生长:Pre Android 5.0
Android 5.0 之前,系统并不是那么完善,对于电量优化相对还是比较少的。特别没有对应用的后台做严格的限制,多进程、fork native 进程以及广播拉起等各种保活流行了起来。
逐步收紧: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: 更多优化省电功能,后台执行限制,后台位置限制)
最严限制: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次;
耗电优化的几个问题
缺乏现场,无法复现;
信息不全,难以定位;
无法评估结果;
为什么需要在后台耗电
某个需求场景。最普遍的场景就是推送,为了实现推送我们只能做各种各样的保活。在需求面前,用户的价值可能被排到第二位。
代码的 Bug。因为某些逻辑考虑不周,可能导致 GPS 没有关闭、WakeLock 没有释放。
耗电优化的方法
找到需求场景的替代方案,后台任务的总体指导思想是减少、延迟和合并
厂商通道;
定时拉取最新消息;
foreground service或引导用户加入白名单;
若需要后台运行:
长时间下载:DownloadManager
数据同步:SyncAdapter(尽可能的打包所有需要同步的任务在一个周期中执行)
本地任务: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()));
builder.setRequiresCharging(true)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
jobScheduler.schedule(builder.build());
* 用于进行批量任务处理的 JobSchedulerService
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class JobSchedulerService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
return false;
@Override
public boolean onStopJob(JobParameters params) {
return false;
特定时间执行:AlarmManager(持有Wake Lock,时间间隔|重复的任务,不建议网络请求使用)
实时通信:FCM、MiPush
立刻执行:foreground service
系统大部分耗电监控是在没充电时,所以我们可以在充电时才做一些耗电工作;
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,版本越高,系统的限制约严格;
异常情况监控
监控异常情况并且上报日志,便于定位线上问题;
仿照Android Vitals指定自己的规则;
使用Java Hook实现耗电监控:
WakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。类似 Alarm、JobService 也会申请 WakeLock 来完成后台 CPU 操作。
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);
@Override
public void beforeInvoke(Method method, Object[] args) {
if (method.getName().equals("acquireWakeLock")) {
if (isAppBackground()) {
} else {
} else if (method.getName().equals("releaseWakeLock")) {
Alarm 用来做一些定时的重复任务,它一共有四个类型,其中ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP类型都会唤醒设备。
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);
public void beforeInvoke(Method method, Object[] args) {
if (method.getName().equals("set")) {
} else if (method.getName().equals("remove")) {
对于后台 CPU,我们可以使用卡顿监控学到的方法。
对于后台网络,同样我们可以通过网络监控学到的方法。
对于 GPS 监控,我们可以通过 Hook 代理LOCATION_SERVICE。
对于 Sensor,我们通过 Hook SENSOR_SERVICE中的“mSensorListeners”,可以拿到部分信息。
通过 Hook,我们可以在申请资源的时候将堆栈信息保存起来。当我们触发某个规则上报问题的时候,可以将收集到的堆栈信息、
电池是否充电、CPU 信息、应用前后台时间等辅助信息也一起带上。
通过插桩实现耗电监控
虽然使用 Hook 非常简单,但是某些规则可能不太容易找到合适的 Hook 点。而且在 Android P 之后,很多的 Hook 点都不支持了。
以WakeLock为例
public class WakelockMetrics {
public void acquire(PowerManager.WakeLock wakelock) {
wakeLock.acquire();
public void release(PowerManager.WakeLock wakelock, int flags) {
wakelock.release();
Facebook 也有一个耗电监控的开源库Battery-Metrics
课后练习github.com/AndroidAdva…
电量检测方案
设置—耗电排行:直观,但没有详细数据,对解决问题帮助不大
使用广播监听电量变化—ACTION_BATTERY_CHANGED:价值不大:针对手机整体的耗电量,而非单个 App
dumpsys batterystats:adb shell dumpsys batterystats > battery.txt
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
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
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
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
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
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
isInterface
return
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
Android开发高手课-耗电优化(上):从电量优化的演进看耗电分析
Android开发高手课-耗电优化(下):耗电的优化方法与线上监控
大众点评App的短视频耗电量优化实战
Android Vitals
Battery Historian
Android后台调度任务与省电
Android P 电量管理
电源管理限制
深入探索 Android 电量优化
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章