耗电优化究竟需要做哪些工作?我们如何快速定位代码中的不合理调用,并且持续监控应用的耗电情况呢?

本文主要记载

  • 1 优化哪些耗电行为?
  • 2 耗电优化的难点以及方法
  • 3 耗电监控
    • 3.1 耗电监控都监控什么
    • 3.2 Battery Historian
    • 3.3 Android Vitals
    • 3.4 Java Hook
    • 3.5 插桩
  • 4 耗电量计算原理

1 优化哪些耗电行为?

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

假设这个时候发现某个应用他根本没怎么使用(前台时间很少),但是耗电却非常多。这种情况会跟用户的预期差别很大,他可能就会想去投诉。

  • 所以耗电优化的第一个方向是优化应用的后台耗电。 知道了系统是如何计算耗电的,那反过来看,我们也就可以知道应用在后台不应该做什么,例如长时间获取 WakeLockWiFi 和蓝牙的扫描等。为什么说耗电优化第一个方向就是优化应用后台耗电,因为大部分厂商预装项目要求最严格的正是应用后台待机耗电。

    图片来源于 Android 开发高手课

  • 耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的。Android P 是通过 Android Vitals 监控后台耗电,所以我们需要符合 Android Vitals 的规则,目前它的具体规则如下:

    图片来源于 Android 开发高手课

    虽然上面的标准可能随时会改变,但是可以看到,Android 系统目前比较关心后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠。

2 耗电优化的难点以及方法

难点:

  • 缺乏现场,无法复现。
  • 信息不全,难以定位。
  • 无法评估结果。Android 4.4 开始,我们无法拿到应用的耗电信息。尽管我们解决了某个耗电问题,也很难去评估它是否已经生效,以及对用户产生的价值有多大。

耗电优化的方法和思路:

  • 代码的 Bug。因为某些逻辑考虑不周,可能导致 GPS 没有关闭、WakeLock 没有释放。

  • 找到需求场景的替代方案。

    • 最普遍的场景就是推送,为了实现推送我们只能做各种各样的保活。在需求面前,用户的价值可能被排到第二位。我们是否可以更多地利用厂商通道,或者定时的拉取最新消息这种模式。如果真是迫不得已,是不是可以使用 foreground service 或者引导用户加入白名单。后台任务的总体指导思想是减少、延迟和合并,可以参考微信一个小伙写的《Android 后台调度任务与省电》
  • 符合 Android 规则。首先系统的大部分耗电监控,都是在手机在没有充电的时候。我们可以选择在用户充电时才去做一些耗电的工作,具体方法可查看官方文档 监控电池电量和充电状态 。其次是尽早适配最新的 Target API,因为高版本系统后台限制本来就非常严格,应用在后台耗电本身就变得比较困难了。

    1
    2
    3
    4
    5
    6
    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;
  • 异常情况监控。即使是最严格的 Android P Android P 电量管理 ,系统也会允许应用部分地使用后台网络、Alarm 以及 JobSheduler 事件(不同的分组,限制次数不同)。因此出现异常情况的可能性还是存在的,更不用说低版本的系统。对于异常的情况,我们需要类似 Android Vitals 电量监控一样,将规则抽象出来,并且增加上更多辅助我们定位问题的信息。

3 耗电监控

3.1 耗电监控都监控什么

  • 监控信息。 简单来说系统关心什么,我们就监控什么,而且应该以后台耗电监控为主。类似 Alarm wakeupWakeLockWiFi scansNetwork 都是必须的,其他的可以根据应用的实际情况。如果是地图应用,后台获取 GPS 是被允许的;如果是计步器应用,后台获取 Sensor 也没有太大问题。
  • 现场信息。 监控系统希望可以获得完整的堆栈信息,比如哪一行代码发起了 WiFi scans、哪一行代码申请了 WakeLock 等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU 状态等一些信息也可以帮助我们排查某些问题。
  • 提炼规则。 最后我们需要将监控的内容抽象成规则,当然不同应用监控的事项或者参数都不太一样。

简单规则:

图片来源于 Android 开发高手课

在安卓绿色联盟的会议中,华为公开过他们后台资源使用的 “红线”

图片来源于 Android 开发高手课

3.2 Battery Historian

Battery Historian 的使用与安装

Battery HistorianAndroid5.0 之后 Google 开源的一款用于检测与电池有关的信息和事件的工具,从设备中收集电池数据,然后使用 Battery Historian 可以可视化分析相关指标如耗电比例、Wifi、蜂窝数据量、WakeLock 唤醒次数。随着 Android 6.0 更新了 Battery Historian 2.0 加入引起手机状态变化的应用。

通过 Battery Historian 可以方便的看到各耗电模块随着时间的耗电情况:包含操作类型、执行时间、对应 App 等;还可以进行筛选特定的 App ,给出一个总结性的说明,包括:Network InformationSyncsWakeLockServicesProcess infoScheduled JobSensor Use 等,查看每一个模块的总结,可以看出来每一项的耗时以及执行次数。当发现异常的时候可以针对性的进行排查。

3.3 Android Vitals

Android Vitals 的几个关于电量的监控方案与规则

Alarm Manager wakeup 唤醒过多频繁

使用局部唤醒锁

后台网络使用量过高

后台 WiFi scans 过多

Android VitalsBattery Historian 一样,我们只能拿到 wakeup 的标记的组件,拿不到申请的堆栈,也拿不到当时手机是否在充电、剩余电量等信息。

对于网络、WiFi scans 以及 WakeLock 也是如此。虽然 Vitals 帮助我们缩小了排查的范围,但是依然需要在茫茫的代码中寻找对应的可疑代码。

3.4 Java Hook

  • WakeLockWakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。类似 AlarmJobService 也会申请 WakeLock 来完成后台 CPU 操作。WakeLock 的核心控制代码都在 PowerManagerService 中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 代理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 (isAppForeground()) {
    // 应用前台逻辑,上面的规则
    } else {
    // 应用后台逻辑,上面的规则
    }
    // 释放Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {
    // 释放的逻辑
    }
    }
  • AlarmAlarm 用来做一些定时的重复任务,它一共有四个类型,其中 ELAPSED_REALTIME_WAKEUPRTC_WAKEUP 类型都会唤醒设备。同样,Alarm 的核心控制逻辑都在 AlarmManagerService 中,实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 代理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")) {
    // 清除的逻辑
    }
    }

唯鹿 Chapter19

这个 Demo 使用 Java Hook 实现 AlarmWakeLockGPS 的耗电监控。

动态代理对应的 PowerManagerAlarmManagerLocationManagermService实现,要拦截的方法在 PowerManagerServiceAlarmManagerServiceLocationManagerService 中。

唯鹿原文

3.5 插桩

在使用 Hook 的时候,某些规则可能不太容易找到合适的 Hook 点。而且在 Android P 之后,很多的 Hook 点都不支持了。

出于兼容性考虑,写一个基础类,然后在统一的调用接口中增加监控逻辑。以 WakeLock 为例:

1
2
3
4
5
6
7
8
9
10
11
12
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 ,它监控的数据非常全,包括 AlarmWakeLockCameraCPUNetwork 等,而且也有收集电量充电状态、电量水平等信息。

Battery-Metrics 只是提供了一系列的基础类,在实际使用中,接入者可能需要修改大量的源码。但对于一些第三方 SDK 或者后续增加的代码,我们可能就不太能保证可以监控到了。这些场景也就无法监控了,所以 Facebook 内部是使用插桩来动态替换。

插桩方案使用起来兼容性非常好,并且使用者也没有太大的接入成本。但是它并不是完美无缺的,对于系统的代码插桩方案是无法替换的,例如 JobService 申请 PARTIAL_WAKE_LOCK 的场景。

4 耗电量计算原理

根据物理学的知识,电能的计算公式为 电能 = 电压 * 电流 * 时间。对于手机来说电压一般不会改变,所以在电压恒定的前提下,只需要测量电流和时间就可以确定耗电。

最终不同模块的耗电情况可以通过下面的这个公式计算:模块电量(mAh) = 模块电流(mA) * 模块耗时(h) 模块耗时比较容易理解,但是模块电流应该怎样去获取呢?

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

power_profiler.xml 文件定义了不同模块的电流消耗值以及该模块在一段时间内大概消耗的电量,你也可以参考 Android Developer 文档《Android 电源配置文件》。当然电流的大小和模块的状态也有关系,例如屏幕在不同亮度时的电流肯定会不一样。

图片来源于 Android开发高手课

Android 系统的电量计算 PowerProfile 也是通过读取 power_profile.xml 的数值而已,不同的厂商具体的数值都不太一样,我们可以通过下面的方法获取:

  • 从手机中导出 /system/framework/framework-res.apk 文件。
  • 使用反编译工具(如 apktool)对导出文件 framework-res.apk 进行反编译。
  • 查看 power_profile.xml 文件在 framework-res 反编译目录路径:/res/xml/power_profile.xml

对于系统的电量消耗情况,我们可以通过 dumpsys batterystats 导出。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

BatteryStatsService 是对外的电量统计服务,但具体的统计工作是由 BatteryStatsImpl 来完成的,而 BatteryStatsImpl 内部使用的就是 PowerProfileBatteryStatsImpl 会为每一个应用创建一个 UID 实例来监控应用的系统资源使用情况。

电量的使用也会跟环境有关,例如在零下十度的冬天电量会消耗得更快一些,系统提供的电量测量方法只是提供一个参考的数值。不过通过上面的这个方法,我们可以成功把电量的测量转化为功能模块的使用时间或者次数。

准确的测量电量并不是那么容易,在《大众点评 App 的短视频耗电量优化实战》 一文中,总结了下面几种电量测试的方法。

图片来源于 Android开发高手课

感谢

极客时间 Android开发高手课

Android 6.0新特性之Doze模式

Android性能优化(九)之不可忽视的电量

以及上文中的链接