当一个 Android 设备闲置时,它的屏幕会先变暗,然后关闭屏幕,最后关闭 CPU。 这样可以防止设备的电量被快速耗尽。 但是,有时候也会存在一些例外:
- 例如游戏或电影之类的应用可能需要保持屏幕常亮。
- 其他应用可能不需要屏幕保持常亮,但是可能需要 CPU 保持运行,直到关键操作完成。
本文将描述如何在必要的时候保持设备唤醒,同时又不会过多消耗它的电量。
1. 保持设备唤醒
了解如何根据需要保持屏幕或 CPU 的唤醒,同时最大限度地减少对电池寿命的影响。
为了避免电量耗尽,闲置的 Android 设备会迅速进入睡眠状态。但是,有时应用程序需要唤醒屏幕或 CPU 并保持唤醒状态才能完成某些工作。
采取什么样的方法取决于你应用程序的需求。然而,一般的经验是,你应该为应用使用最轻量级的方法,以尽量减少其对系统资源的影响。接下来的内容会介绍如何处理设备的默认睡眠行为与应用程序的需求不兼容的情况。
1.1 保持屏幕常亮
某些应用需要保持屏幕常亮,比如游戏与视频应用。做到这一点的最佳方式是在 Activity 中使用 FLAG_KEEP_SCREEN_ON 属性(只能在 Activity 中,而不能在 Service 或其他应用组件)。例如:
1 | public class MainActivity extends Activity { |
这种方法的优点是,不像唤醒锁(在保持CPU唤醒状态中讨论),它不需要特殊的权限,系统会正确地
管理应用之间的切换,且不必关心资源释放的问题。
另一种方法是在应用的 XML 布局文件里,使用 android:keepScreenOn 属性:
1 | <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
使用 android:keepScreenOn=”true” 与使用 FLAG_KEEP_SCRRE_ON 等效。你可以选择最适合你应用的方法。在 Activity 中通过代码设置常亮标识的优点在于:你可以通过代码动态清除这个标识,从而使屏幕可以关闭。
**注意:**除非你不再希望正在运行的应用长时间点亮屏幕(例如:在一段时间无操作发生后,你想要将屏幕关闭),否则你是不需要清除 FLAG_KEEP_SCRRE_ON 标识的。WindowManager 会在应用进入后台或者返回前台时,正确管理屏幕的关闭或点亮。但是如果你想要显式地清除这一标识,从而使得屏幕能够关闭,可以使用 clearFlags(): getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 方法。
1.2 保持 CPU 运行
如果需要保持 CPU 运行以便在设备进入睡眠状态之前完成某些工作,你可以使用 PowerManager 系统服务中的唤醒锁功能。唤醒锁允许你的应用控制设备的电源状态。
创建并保持唤醒锁可能会对设备的电池寿命产生巨大影响。 因此你应该仅在你确实需要时使用唤醒锁,且使用的时间应该越短越好。例如,你不应该在 Activity 中使用唤醒锁。如上所述,如果您想要屏幕在 Activity 中保持常亮,请使用 FLAG_KEEP_SCREEN_ON。
不必使用唤醒锁的情况:
- 如果你的应用正在执行一个 HTTP 长连接的下载任务,可以考虑使用 DownloadManager。
- 如果你的应用正在从一个外部服务器同步数据,可以考虑创建一个 SyncAdapter。
- 如果你的应用需要依赖于某些后台服务,可以考虑使用 RepeatingAlarm 或者 Google Cloud Messaging 以特定间隔触发这些服务。
使用唤醒锁的一种合理情况可能是:一个后台服务需要在屏幕关闭时利用唤醒锁保持 CPU 运行。再次强调,应该尽可能规避使用该方法,因为它会影响到电池寿命。
如果要使用唤醒锁,首先需要在应用的 Manifest 清单文件中增加 WAKE_LOCK 权限:
1 | <uses-permission android:name="android.permission.WAKE_LOCK" /> |
如果你的应用包含一个 BroadcastReceiver 并使用 Service 来完成一些工作,你可以通过 WakefulBroadcastReceiver 管理你的唤醒锁。在使用 WakefulBroadcastReceiver 章节中将会提到,这是首选的方法。如果你的应用不遵循这种模式,可以使用下面的方法直接设置唤醒锁:
1 | PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); |
可以调用 wakelock.release() 来释放唤醒锁。当应用使用完毕时,应该释放该唤醒锁,以避免电量过度消耗。
1.3 使用 WakefulBroadcastReceiver
将 BroadcastReceiver 与 Service 结合使用,可以管理后台任务的生命周期。
WakefulBroadcastReceiver 是一种特殊的 BroadcastReceiver,它专注于创建和管理应用的 PARTIAL_WAKE_LOCK。WakefulBroadcastReceiver 会将任务交付给 Service(通常是一个 IntentService),同时确保设备在此过程中不会进入睡眠状态。如果在该过程当中没有保持住唤醒锁,那么还没等任务完成,设备就有可能进入睡眠状态了。其结果就是:应用可能会在未来的某一个时间节点才把任务完成,这显然不是你所期望的。
使用 WakefulBroadcastReceiver 的第一步是将其添加到清单中,就像其他广播接收器一样:
1 | <receiver android:name=".MyWakefulReceiver"></receiver> |
下面的代码通过 startWakefulService() 启动 MyIntentService。该方法和 startService() 类似,除了 WakeflBroadcastReceiver 会在 Service 启动后将唤醒锁保持住。传递给 startWakefulService() 的 Intent 会携带有一个 Extra 数据,用来标识唤醒锁。
1 | public class MyWakefulReceiver extends WakefulBroadcastReceiver { |
当 Service 结束之后,它会调用 MyWakefulReceiver.completeWakefulIntent() 来释放唤醒锁。completeWakefulIntent() 方法中的 Intent 参数和 WakefulBroadcastReceiver 传递进来的 Intent 参数是一致的:
1 | public class MyIntentService extends IntentService { |
2. 调度重复闹钟
了解如何使用重复闹钟去调度那些发生在应用生命周期之外的操作,即使应用程序未运行或者设备处于睡眠状态。
闹钟(基于 AlarmManager 类)为你提供了一种在应用程序生命周期外执行基于时间的操作的方法。 例如,你可以使用闹钟初始化一个长时间的操作,例如每天启动一次服务以下载天气预报。
闹钟具有以下特点:
- 允许你通过预设时间或者设定某个时间间隔,来触发 Intent。
- 你可以将它与 BroadcastReceiver 结合使用,来启动服务并执行其他操作。
- 它们在应用程序之外运行,所以即使应用程序没有运行,甚至设备处于睡眠状态,也可以使用它们触发事件或操作。
- 它们帮助你最大限度地减少应用程序的资源需求。你可以使用闹钟进行任务调度,来替代计时器或长时间连续运行的后台服务。
**注意:**对于确保在应用程序生命周期中发生的定时操作,请考虑将 Handler 类与 Timer 和 Thread 结合使用。这样使 Android 更好地控制系统资源。
2.1 权衡利弊
重复闹钟是一种相对简单的机制,灵活性有限。它可能不是你的应用程序的最佳选择,特别是当你想要触发网络操作的时候。设计不当的闹钟可能会导致电池耗尽并给服务器带来很大负担。
一个常见的应用场景是,在应用的生命周期之外触发一个操作从服务器同步数据。此时你可能希望使用重复闹钟。但是如果存储数据的服务端是由你控制的,使用 Google Cloud Messaging(GCM)结合 sync adapter 是一种更好解决方案。SyncAdapter 提供的任务调度选项和 AlarmManager 基本相同,但是它能提供更大的灵活性。例如:同步的触发可能基于来自服务器/设备的“新数据”提示消息,用户的活动与否,每天的某一时刻等。
2.1.1 最佳实践方法
在设计重复闹钟时,你所做的每一个选择都可能会影响你的应用程序使用(或滥用)系统资源的方式。例如,假设一个流行的应用与服务器同步数据。如果同步操作基于时钟时间,并且每个应用程序的实例在晚上 11:00 进行同步,则服务器上的负载可能导致高延迟甚至“拒绝服务”。因此在我们使用闹钟时,请牢记下面的最佳实践建议:
- 对任何由重复闹钟触发的网络请求添加一定的随机性(抖动):
- 在闹钟触发时做一些本地任务。“本地任务”指的是任何不需要访问服务器或者从服务器获取数据的任务。
- 同时,对于那些包含有网络请求的闹钟,在调度时机上增加一些随机性。
- 尽量保持闹钟的频率最低。
- 如果不是必要的情况,不要唤醒设备(这一点与闹钟的类型有关,后续内容会提到)。
- 触发闹钟的时间不必过于精确。
尽量使用 setInexactRepeating() 方法替代 setRepeating() 方法。当你使用 setInexactRepeating() 方法时,Android 系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这可以减少系统唤醒设备的总次数,以此减少电量消耗。从Android 4.4(API Level19)开始,所有的重复闹钟都将是非精确型的。注意虽然 setInexactRepeating() 是 setRepeating() 的改进版本,它依然可能会导致每一个应用的实例在某一时间段内同时访问服务器,造成服务器负荷过重。因此,如上所述,对于网络请求,我们需要为闹钟的触发时机增加随机性。 - 尽量避免让闹钟基于时钟时间。
重复基于精确触发时间的闹钟不能很好地扩展。我们应该尽可能使用 ELAPSED_REALTIME。不同的闹钟类型会在本节后半部分展开。
2.2 设置重复闹钟
如上所述,对于定期执行的任务或者数据查询而言,使用重复闹钟是一个不错的选择。它具有下列属性:
- 报警类型。
- 触发时间。如果触发时间是过去的某个时间点,闹钟会立即被触发。
- 闹钟的间隔。例如,每天一次,每一小时一次,每五分钟一次,等等。
- 在闹钟被触发时才被发出的 Pending Intent。如果你为同一个 Pending Intent 设置了另一个闹钟,那么它会将第一个闹钟覆盖。
2.2.1 选择闹钟类型
使用重复闹钟要考虑的第一件事情是闹钟的类型。
闹钟类型有两大类:ELAPSED_REALTIME
和 REAL_TIME_CLOCK
(RTC)。ELAPSED_REALTIME
从系统启动之后开始计算,REAL_TIME_CLOCK
使用的是世界统一时间(UTC)。也就是说由于 ELAPSED_REALTIME
不受地区和时区的影响,所以它适合于基于时间差的闹钟(例如一个每过 30 秒触发一次的闹钟)。REAL_TIME_CLOCK
适合于那些依赖于地区位置的闹钟。
两种类型的闹钟都还有一个唤醒(WAKEUP)版本,也就是可以在设备屏幕关闭的时候唤醒 CPU。这可以确保闹钟会在既定的时间被激活,这对于那些实时性要求比较高的应用(比如含有一些对执行时间有要求的操作)来说非常有效。如果你没有使用唤醒版本的闹钟,那么所有的重复闹钟会在下一次设备被唤醒时被激活。
如果你只是简单的希望闹钟在一个特定的时间间隔被激活(例如每半小时一次),那么你可以使用任意一种 ELAPSED_REALTIME
类型的闹钟,通常这会是一个更好的选择。
如果你需要闹钟在每一天的特定时间被激活,那么你可以选择 REAL_TIME_CLOCK
类型的闹钟。不过需要注意的是,这个方法会有一些缺陷——如果地区发生了变化,应用可能无法做出正确的改变;另外,如果用户改变了设备的时间设置,这可能会造成应用产生预期之外的行为。使用 REAL_TIME_CLOCK
类型的闹钟还会有精度的问题,因此我们建议你尽可能使用 ELAPSED_REALTIME
类型。
以下是闹钟的具体类型:
- ELAPSED_REALTIME:从设备启动之后开始算起,度过了某一段特定时间后,激活 Pending Intent,但不会唤醒设备。其中设备睡眠的时间也会包含在内。
- ELAPSED_REALTIME_WAKEUP:从设备启动之后开始算起,度过了某一段特定时间后唤醒设备并激活 Pending Intent。
- RTC:在某个特定时刻激活 Pending Intent,但不会唤醒设备。
- RTC_WAKEUP:在某个特定时刻唤醒设备并激活 Pending Intent。
2.2.2 ELAPSED_REALTIME_WAKEUP 示例
下面是一些使用 ELAPSED_REALTIME_WAKEUP 的例子。
在 30 分钟内将设备唤醒以激活闹钟,然后每 30 分钟一次:
1 | // Hopefully your alarm will have a lower frequency than this! |
在一分钟后唤醒设备并激活一个一次性(无重复)闹钟:
1 | private AlarmManager alarmMgr; |
2.2.3 RTC 示例
下面是一些使用 RTC_WAKEUP 的例子。
在下午 2 点左右唤醒设备并激活闹钟,并且每天重复一次:
1 | // Set the alarm to start at approximately 2:00 p.m. |
让设备精确地在上午 8:30 被唤醒并激活闹钟,自此之后每 20 分钟唤醒一次:
1 | private AlarmManager alarmMgr; |
2.2.4 确定闹钟的精确度
如上所述,创建闹钟的第一步是要选择闹钟的类型,然后你需要决定闹钟的精确度。对于大多数应用而言,setInexactRepeating() 会是一个正确的选择。当你使用该方法时,Android系统会集中多个应用的重复闹钟同步请求,并一起触发它们。这样可以减少电量的损耗。
对于另一些实时性要求较高的应用——例如,闹钟需要精确地在上午 8:30 被激活,并且自此之后每隔 1 小时激活一次——那么可以使用 setRepeating()。不过你应该尽量避免使用精确的闹钟。
使用 setRepeating() 时,你可以制定一个自定义的时间间隔,但在使用 setInexactRepeating() 时不支持这么做。此时你只能选择一些时间间隔常量,例如:INTERVAL_FIFTEEN_MINUTES ,INTERVAL_DAY 等。完整的常量列表,可以查看 AlarmManager。
2.2 取消闹钟
你可能希望在应用中添加取消闹钟的功能。要取消闹钟,可以调用 AlarmManager 的 cancel() 方法,并把你不想激活的 PendingIntent 传递进去,例如:
1 | // If the alarm has been set, cancel it. |
2.3 设备启动后启用闹钟
默认情况下,所有的闹钟会在设备关闭时被取消。为防止这种情况发生,你可以让你的应用在用户重启设备后自动重启一个重复闹钟。这样可以让 AlarmManager 继续执行它的工作,且不需要用户手动重启闹钟。
具体步骤如下:
1. 在应用的 Manifest 文件中设置 RECEIVE_BOOT_CMPLETED 权限,这将允许你的应用接收系统启动完成后发出的 ACTION_BOOT_COMPLETED 广播(只有在用户至少将你的应用启动了一次后,这样做才有效):
1 | <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> |
2. 实现 BoradcastReceiver 用于接收广播:
1 | public class SampleBootReceiver extends BroadcastReceiver { |
3. 将接收器添加到应用程序的 Manifest 文件中,并使用意向过滤器对 ACTION_BOOT_COMPLETED 操作进行过滤:
1 | <receiver android:name=".SampleBootReceiver" |
注意 Manifest 文件中,对接收器设置了 android:enabled="false"
属性。这意味着除非应用显式地启用它,不然该接收器将不被调用。这可以防止接收器被不必要地调用。你可以像下面这样启动接收器(比如用户设置了一个闹钟):
1 | ComponentName receiver = new ComponentName(context, SampleBootReceiver.class); |
一旦以这种方式启用接收器,它将一直保持启动状态,即使用户重启了设备也不例外。换句话说,通过代码设置的启用配置将会覆盖掉 Manifest 文件中的现有配置,即使重启也不例外。接收器将保持启动状态,直到你的应用将其禁用。你可以像下面这样禁用接收器(比如用户取消了一个闹钟):
1 | ComponentName receiver = new ComponentName(context, SampleBootReceiver.class); |