移动性能测试 合理使用应用唤醒 Alarm,让你的应用更省电

安卓绿色联盟 · 2019年03月07日 · 1160 次阅读

不少细心的开发者已发现,在华为终端开放实验室发布的最新绿色应用达标率调查报告中,国内千款主流应用有 63 款应用在灭屏时 Alarm 占用大于 20 次/小时,这些应用最终也因为此检测项未通过导致无法获得绿色应用标记。

那么在绿色应用检测过程中为什么会把灭屏时 Alarm 占用作为一个重要衡量标准进行检测呢,开发者又该如何针对此项标准对应用进行开发呢?本篇文章将给出你答案。

安卓设备为了节省电量,在设备无操作时会将屏幕变暗,然后灭屏,最终休眠 CPU。但有时应用可能需要不同的行为,比如一些游戏或视频类应用可能需要持续亮屏,还有一些应用可能不需要持续亮屏,但需要 CPU 保持运行,直到完成相关关键操作。为此,安卓系统提供了在必要时保持设备唤醒且省电的方案,分别是 Wakeup 和 AlarmManager,这也就是 Alarm(闹钟) 在安卓系统存在的原因。

一、Alarm 是什么

熟悉安卓系统的开发者都知道,Alarm 可以完成闹钟式定时任务,安卓系统主要通过 AlarmManager 类对其进行管理,我们可以通过 AlarmManager 在一些 Alarm 设定的时间点启动服务进行事件处理,同时还可以用 Alarm 来初始化一些长时间运行的操作,手机每天启动一个服务来下载天气预报的数据其实就是 Alarm 最熟悉的实用场景。

我们了解了 Alarm 是用来做什么的,那么 Alarm 都有哪些特性呢?

二、Alarm 特性

1、Alarm 允许在设定的时间或时间间隔发送 Intent(意图);

2、将 Alarm 与 Broadcast Receiver(广播接收器)进行结合启动 Service(服务),并执行其它操作;

3、Alarm 可在应用之外运行,所以即使应用没有启动、设备处于休眠状态,也可以通过 Alarm 来触发应用事件及操作;

4、Alarm 可以使应用对于系统资源进行最小化占用,可以在不依赖 timer(计时器)或者后台持续运行的 Service 来执行一些操作;

三、Repeating Alarm(重复闹钟)

Repeating Alarm(重复闹钟)是一种相对灵活的简单机制,但并非在所有场景下都适合,在应用需要触发网络操作时,如果使用 Repeating Alarm,可能会因为 Alarm 设计不当导致电量过度消耗,增加服务器负载。但通常情况下,在应用运行之外触发操作需要从服务器同步数据,这就需要借助 Repeating Alarm 来实现。当要同步数据的服务器是自有的,Google Cloud Messaging(GCM) 配合 sync adapter 同步框架是比 AlarmManager 更好的解决方案。sync adapter 同步框架可以提供 AlarmManager 所有的功能,而且更加灵活。

当设备在 Doze 模式下处于空闲状态时,Alarm 不会被触发。任何预先设置的 Alarm 将被推迟,直到设备退出 Doze 模式。如果需要设备在空闲状态也能完成相关操作,可以使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle() 来保证 Alarm 被执行;也可以使用新的 WorkManager API 来完成后台单次或定期的执行工作。使用 setInexactRepeating() 时,不能像使用 setRepeating() 时自定义间隔,如果要自定义间隔,必须使用间隔常量:

INTERVAL_FIFTEEN_MINUTES、INTERVAL_DAY 等

四、Repeating Alarm 最佳实践

使用 Repeating Alarm 进行的每一个设置都会影响应用对系统资源的占用,那么该如何以最小的系统资源占用使用 Repeating Alarm 呢?

在 Repeating Alarm 触发的网络请求里添加随机性(抖动)操作:

①当 Alarm 触发时,先执行无需访问服务器或从服务器获取数据的操作。

②与此同时,对于包含网络请求的 Alarm,在设定时间上添加一些随机变量。

降低 Alarm 触发频率。

除非必要,否则不使用唤醒设备的 Alarm。

除非必要,否则不要使用高精度的 RTC 时钟来触发 Alarm。

使用 setInexactRepeating() 来替换 setRepeating()。当使用 setInexactRepeating() 时,安卓系统会同步多个应用的 Repeating Alarm,并同时触发它们。这样可以减少系统唤醒设备的总次数,从而减少手机电量消耗。从安卓系统 4.4(API 级别 19)开始,所有 Repeating Alarm 都是不精确的。尽管 setInexactRepeating() 相对于 setRepeating() 做了改进,但如果手机上的应用都在接近的时间内集中访问服务器,仍会给服务器造成压力。因此,很有必要对于网络请求的 Alarm 添加一些随机性。

尽可能避免使用基于 RTC 的 Alarm。

基于 RTC 的 Alarm 扩展性不好,使用 ELAPSED_REALTIME 对 Alarm 进行设置更好一些。

五、Repeating Alarm 的设置

Repeating Alarm 适合用来执行常规事件及查找数据,接下来我们来认识一下 Repeating Alarm 具有哪些特性?

Alarm 类型的选择;

触发时间,如果设定的触发时间已经过去了,则 Alarm 会立即触发;

Alarm 的间隔:每天、每小时、每 5 分钟等;

触发 Alarm 时需执行的 Pending Intent,如果第二个 Alarm 设置使用之前存在的 Pending Intent,系统会默认替换之前的 Alarm。

六、选择一个 Alarm 类型

设置 Repeating Alarm 时首要考虑因素就是它的类型。

Alarm 有两种通用时钟类型:“elapsedreal time(相对时间)” 和 “real time clock(RTC 实时时间)”。相对时间使用 “自系统启动以来的时间” 作为参考,实时时间使用 UTC 时间。这意味着相对时间适合根据时间的推移设置 Alarm,因为它不受时区/区域设置的影响,实时时间更依赖于当前区时设置 Alarm。

两种时钟类型都有一个设备 “唤醒” 机制,可以在屏幕熄灭后唤醒 CPU,这就确保了 Alarm 在设定时间被触发。如果你的应用具有时间依赖性(例如要求在有限窗口执行特定操作),可以使用该机制。如果不使用设备 “唤醒” 机制,所有 Repeating Alarm 都将在设备下次唤醒时全部触发。

如果以特定时间间隔触发 Alarm,最好使用相对时间类型。反之,如果需要在一天中的特定时间触发 Alarm,则需要选择一种基于 UTC 时间的实时时间类型。

以下是所有时钟类型:

ELAPSED_REALTIME——根据设备启动后的时间来处理 Pending Intent,但不会唤醒设备,设备休眠时间也会被统计进来。

ELAPSED_REALTIME_WAKEUP——在设备启动后经过设定的时间长度,唤醒设备并处理 Pending Intent。

RTC——在设定时间处理 Pending Intent,但不会唤醒设备。

RTC_WAKEUP——唤醒设备并在设定时间处理 Pending Intent。

七、Alarm 设置示例

1、相对时间 Alarm 设置示例

以下是使用 ELAPSED_REALTIME_WAKEUP 进行 Alarm 设置示例。

30 分钟内唤醒设备并触发 Alarm,之后每 30 分钟触发一次 Alarm:

// Hopefully your alarm will have a lower frequency than this!
alarmMgr.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
AlarmManager.INTERVAL_HALF_HOUR,
AlarmManager.INTERVAL_HALF_HOUR, alarmIntent);
一分钟内唤醒设备并触发一个一次性 Alarm:
private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() +
60 * 1000, alarmIntent);
2、实时时间 Alarm 设置示例

以下是一些使用 RTC_WAKEUP 进行 Alarm 设置的示例。

在下午 2 点左右唤醒设备并触发 Alarm,之后每天在同样的时间重复触发一次这个 Alarm:

// Set the alarm to start at approximately 2:00 p.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 14);

// With setInexactRepeating(), you have to use one of the AlarmManager interval
// constants--in this case, AlarmManager.INTERVAL_DAY.
alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
AlarmManager.INTERVAL_DAY, alarmIntent);
在上午 8 点 30 分准时唤醒设备,之后每隔 20 分钟触发一次 Alarm:

private AlarmManager alarmMgr;
private PendingIntent alarmIntent;
...
alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(context, AlarmReceiver.class);
alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

// Set the alarm to start at 8:30 a.m.
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(System.currentTimeMillis());
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 30);

// setRepeating() lets you specify a precise custom interval--in this case,
// 20 minutes.
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(),
1000 * 60 * 20, alarmIntent);

八、Alarm 设置的取消

应用取消 Alarm 设置,需要调用 AlarmManager 里 cancel() 的方法,把不想被触发的 PendingIntent 实例传入 cancel() 里。例如:

// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
alarmMgr.cancel(alarmIntent);
}

九、设备重启时如何触发 Alarm

默认情况下,设备关闭时会同时关闭所有 Alarm。为防止这种情况发生,可以在用户重新启动设备时应用自动重新启动 Repeating Alarm。可以确保 AlarmManager 在用户无需手动重启 Alarm 的情况下继续执行其任务。

以下是步骤:

1、在应用 manifest 中设置 RECEIVE_BOOT_COMPLETED 权限。允许应用在系统完成启动后接收 ACTION_BOOT_COMPLETED 广播(仅在用户已经至少启动过应用一次时才有效):


2、实现一个 BroadcastReceiver 来接收广播:

public class SampleBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
// Set the alarm here.
}
}
}
3、使用可过滤的 ACTION_BOOT_COMPLETED 操作的 Intent 过滤器将 receiver 添加到应用程序的 manifest 文件中:

android:enabled="false">




4、在 manifest 文件中,将启动 receiver 设置为 android:enabled="false"。这意味着除非应用明确启用 receiver,否则不会被调用,这可以防止启动 receiver 被不必要地调用。可以按如下方式启用 receiver:

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
以这种方式启用 receiver 后,即使设备被重新启动,receiver 也会保持启用状态。换句话说,通过代码启用 receiver 会覆盖 manifest 文件的设置,即使重新启动也是如此,receiver 将继续启用,直到应用将它关闭。可以按如下方式禁用 receiver:

ComponentName receiver = new ComponentName(context, SampleBootReceiver.class);
PackageManager pm = context.getPackageManager();

pm.setComponentEnabledSetting(receiver,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);

十、Doze 和 App Standby 模式对 Alarm 的影响

Doze 和 App Standby 模式在安卓 6.0(API 级别 23)中被引入,旨在延长设备电池寿命。当设备处于 Doze 模式时,任何标准 Alarm 都将会延迟,直到设备退出 Doze 模式或维护窗口被打开。如果需要在 Doze 模式下触发 Alarm,可以通过使用 setAndAllowWhileIdle() 或 setExactAndAllowWhileIdle()。应用在一段时间内未被使用,同时应用在前台没有任何进程时,将进入 App Standby 模式。当应用处于 App Standby 模式时,Alarm 会像在 Doze 模式下一样延迟。当应用运行或设备接入电源时,此限制将被解除。

DevEco 检测方案
综上所述,频繁唤醒设备的 Alarm 对设备电池寿命影响较大,华为 DevEco 云测平台通过检测应用在后台灭屏 1 小时内触发唤醒设备 Alarm 的次数来衡量应用是否存在不合理使用 Alarm 的情况。具体测试方法如下:

将应用安装,启动,正常操作几分钟后,回到首页,放置后台,灭屏。执行以下指令:

清理上次的测试数据:adb shell dumpsys batterystats --reset

允许记录所有 wake 信息:adb shell dumpsys batterystats --enable full-wake-history

模拟拔除电缆:adb shell dumpsys battery unplug

一小时后,执行 adb bugreport > bugreport.txt 导出 bugreport 报告

通过分析 bugreport(参考 Battery Historian 的搭建),Wakeup alarm info 里面的 Alarm 累计唤醒次数进行判断。

本文参考《Schedule repeating alarms》进行翻译整理

文章原地址为:

https://developer.android.com/training/scheduling/alarms

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册