冰川孤辰 2019-07-01
不少细心的开发者已发现,在华为终端开放实验室发布的最新绿色应用达标率调查报告中,国内千款主流应用有63款应用在灭屏时Alarm占用大于20次/小时,这些应用最终也因为此检测项未通过导致无法获得绿色应用标记。
那么在绿色应用检测过程中为什么会把灭屏时Alarm占用作为一个重要衡量标准进行检测呢,开发者又该如何针对此项标准对应用进行开发呢?本篇文章将给出你答案。
安卓设备为了节省电量,在设备无操作时会将屏幕变暗,然后灭屏,最终休眠CPU。但有时应用可能需要不同的行为,比如一些游戏或视频类应用可能需要持续亮屏,还有一些应用可能不需要持续亮屏,但需要CPU保持运行,直到完成相关关键操作。为此,安卓系统提供了在必要时保持设备唤醒且省电的方案,分别是Wakeup和AlarmManager,这也就是Alarm(闹钟)在安卓系统存在的原因。
熟悉安卓系统的开发者都知道,Alarm可以完成闹钟式定时任务,安卓系统主要通过AlarmManager类对其进行管理,我们可以通过AlarmManager在一些Alarm设定的时间点启动服务进行事件处理,同时还可以用Alarm来初始化一些长时间运行的操作,手机每天启动一个服务来下载天气预报的数据其实就是Alarm最熟悉的实用场景。
我们了解了Alarm是用来做什么的,那么Alarm都有哪些特性呢?
1、Alarm允许在设定的时间或时间间隔发送Intent(意图);
2、将Alarm与Broadcast Receiver(广播接收器)进行结合启动Service(服务),并执行其它操作;
3、Alarm可在应用之外运行,所以即使应用没有启动、设备处于休眠状态,也可以通过Alarm来触发应用事件及操作;
4、Alarm可以使应用对于系统资源进行最小化占用,可以在不依赖timer(计时器)或者后台持续运行的Service来执行一些操作;
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触发的网络请求里添加随机性(抖动)操作:
①当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具有哪些特性?
Alarm类型的选择;
触发时间,如果设定的触发时间已经过去了,则Alarm会立即触发;
Alarm的间隔:每天、每小时、每5分钟等;
触发Alarm时需执行的Pending Intent,如果第二个Alarm设置使用之前存在的Pending Intent,系统会默认替换之前的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。
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设置,需要调用AlarmManager里cancel()的方法,把不想被触发的PendingIntent实例传入cancel()里。例如:
// If the alarm has been set, cancel it.
if (alarmMgr!= null) {
alarmMgr.cancel(alarmIntent);
}
默认情况下,设备关闭时会同时关闭所有Alarm。为防止这种情况发生,可以在用户重新启动设备时应用自动重新启动Repeating Alarm。可以确保AlarmManager在用户无需手动重启Alarm的情况下继续执行其任务。
以下是步骤:
1、在应用manifest中设置RECEIVE_BOOT_COMPLETED权限。允许应用在系统完成启动后接收ACTION_BOOT_COMPLETED广播(仅在用户已经至少启动过应用一次时才有效):
<uses-permission android:name="android.permission.RECEIVE_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文件中:
<receiver android:name=".SampleBootReceiver"
android:enabled="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"></action> </intent-filter>
</receiver>
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模式在安卓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》进行翻译整理
文章原地址为: