主要参考资料:
开源后台耗电分析工具: battery_alalyze
在实践中,如果我们的应用需要播放视频、获取 GPS 信息、需要拍照,这些耗电看起来是无法避免的。
如果发现某个应用没怎么使用(前台时间很少),但是耗电却非常多。这种情况会跟用户的预期差别很大,这种情况就需要优化。
根据 Android Vitals 定义,影响后台耗电的动作如下:
应用会通过调用带有 PARTIAL_WAKE_LOCK 标记的 acquire() 来获取部分唤醒锁定。当您的应用在后台运行时,如果部分唤醒锁定保持了较长时间,则会变为卡住状态(用户看不到应用的任何部分)。 它会阻止设备进入低功耗状态。部分唤醒锁定仅应在必要时使用,并且在不再需要时立即释放。
Android Vitals 报告部分唤醒锁定卡住的条件是在以下任一时段内至少发生了一次时长达 1 小时的部分唤醒锁定:
(1)所有情况下至少 0.70% 的电池工作时段
或
(2)仅在后台运行时至少 0.10% 的电池工作时段
唤醒是 AlarmManagerAPI 中的一种机制,可让开发者设置闹钟以在指定时间唤醒设备。为设置唤醒闹钟,您的应用会调用 AlarmManager 中某个带有 RTC_WAKEUP 或 ELAPSED_REALTIME_WAKEUP 标记的 set() 方法。当唤醒闹钟触发时,设备会在执行闹钟的 onReceive() 或 onAlarm() 方法期间退出低功耗模式并保持部分唤醒锁定。如果唤醒闹钟触发次数过多,则可能会耗尽设备的电池电量。
唤醒次数过多标准:用户遇到每小时 10 次以上唤醒的电池工作时段数百分比。
当应用在后台执行 WLAN 扫描时,它会唤醒 CPU,从而加快耗电速度。扫描次数过多时,设备的电池续航时间可能会明显缩短。如果某个应用处于 PROCESS_STATE_BACKGROUND 或 PROCESS_STATE_CACHED 状态,则会被视为在后台运行。
WLAN 扫描次数过多的标准:在后台运行时,应用在 0.10% 的电池工作时段内每小时执行的扫描超过 4 次。
建议:如果可能,您的应用执行 WLAN 扫描时应该是在前台运行。前台服务会自动显示通知;在前台执行 WLAN 扫描,从而让用户知道设备上发生 WLAN 扫描的原因和时间。
扫描次数过多优化:如果您的应用无法避免在后台运行期间执行 WLAN 扫描,则可能适合采用偷懒至上策略。“偷懒至上” 包含三种可用于消减 WLAN 扫描次数的方法:“减少”、“推迟” 和 “合并”。如需了解这些方法,请参阅针对电池续航时间进行优化。
当应用在后台连接移动网络时,应用会唤醒 CPU 并开启无线装置。如果反复执行此操作,可能会耗尽设备的电池电量。如果某个应用处于 PROCESS_STATE_BACKGROUND 或 PROCESS_STATE_CACHED 状态,则会被视为在后台运行。
后台网络使用量过高的标准:在后台运行时,应用在 0.10% 的电池工作时段内每小时发送和接收的数据合计达 50 MB。
建议:可以将应用的移动网络使用量移至前台,提醒用户目前正在进行下载,并为他们提供暂停或停止下载的控件。为此,请调用 DownloadManager 并根据情况设置 setNotificationVisibility(int)。
如何让系统认为是正常耗电呢?当耗电指标低于规则时,系统也就认为是正常耗电了。
海外应用主要参考 Google Vitals 的规则。
对于 Google Vitals 的后台耗电过多统计规则中的电池工作时段百分比,对于质量评估来看,较难把握。所以主要关注规则的具体指标,即相对更严格的质量要求:
对于国内应用来说,目前还没有非常通用且权威的后台耗电规则,根据经验,我们将监控的内容抽象成规则。
当然不同应用监控的事项或者参数都不太一样。由于每个应用的具体情况都不太一样。
下面是一些可以用来参考的简单规则。
那我们的耗电监控系统应该监控哪些内容,怎么样才能比 Android Vitals 做得更好呢?
缺点:
通常大家可能会使用 Battery Historian 来分析后台耗电,但是不够灵活。比如需要人工查看各资源使用情况及是否达标。所以用 python 实现了一个简单的分析 bugreport 文件的小工具;
核心代码是刚做测开半年左右写的,比较乱且水平有限,大家轻拍,也欢迎大家参与优化。
Hook 方案的好处在于使用者接入非常简单,不需要去修改自己的代码。下面我以几个比较常用的规则为例,看看如果使用 Java Hook 达到监控的目的。
// 代理 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")) {
// 释放的逻辑
}
}
// 代理 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")) {
// 清除的逻辑
}
}
通过 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,它监控的数据非常全,包括 Alarm、WakeLock、Camera、CPU、Network 等,而且也有收集电量充电状态、电量水平等信息。
Battery-Metrics 只是提供了一系列的基础类,在实际使用中,接入者可能需要修改大量的源码。但对于一些第三方 SDK 或者后续增加的代码,我们可能就不太能保证可以监控到了。这些场景也就无法监控了,所以 Facebook 内部是使用插桩来动态替换。
遗憾的是,Facebook 并没有开源它们内部的插桩具体实现方案。大家可以自行搜索不同插桩方案的实现。
插桩方案使用起来兼容性非常好,并且使用者也没有太大的接入成本。但是它并不是完美无缺的,对于系统的代码插桩方案是无法替换的,例如 JobService 申请 PARTIAL_WAKE_LOCK 的场景。