☰
cw-omnibus AlarmManager/Scheduled 源码解读:用重复闹钟(Repeating Alarm)驱动定时轮询服务的模式
2026/10/15 21:14:53 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

Source code to omnibus edition of _The Busy Coder's Guide to Android Development_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载

导读

本文以 cw-omnibus 仓库中的 AlarmManager/Scheduled 示例为骨架,深入讲解 Android 中最经典的后台轮询方案——"定时调度服务模式(Scheduled Service Pattern)":即通过AlarmManager注册重复闹钟(repeating alarm),周期性地唤醒一个BroadcastReceiver,再由它把真正的业务工作派发给后台Service执行。读完本文,你将掌握setRepeating()的完整参数语义、ELAPSED_REALTIME_WAKEUP与SystemClock.elapsedRealtime()的配合方式、开机后自动恢复闹钟(BOOT_COMPLETED)的注册方法,以及如何用JobIntentService承接闹钟触发的后台任务。

一、示例定位:AlarmManager 章节的"定时调度服务模式"

Scheduled 的 README 只有短短几句话,但其技术指向非常明确:本示例用于演示"using repeating alarms withAlarmManager",它对应The Busy Coder's Guide to Android Development中关于AlarmManager与定时调度服务模式(the scheduled service pattern)的章节。

在 Android 的"非前台、非交互"场景中,AlarmManager是系统提供的"闹钟"服务:它允许应用在未来某个时刻被唤醒,即使应用此刻没有处于前台。配合PendingIntent,它可以触发三类组件:

PendingIntent 类型触发目标适用场景
PendingIntent.getBroadcast()BroadcastReceiver定时轮询、定时任务分发(本示例所用)
PendingIntent.getActivity()Activity到点弹出界面
PendingIntent.getService()Service到点启动后台服务

Scheduled 示例采用的就是"广播 → 服务"这条链路,是最具代表性的定时轮询骨架。

二、整体架构:三个类如何协作

该示例只包含三个 Java 类(位于 wakesvc 包),分工一目了然:

类角色职责
ScheduledServiceDemoActivity入口应用启动时立即注册闹钟,随后finish()退出
PollReceiver调度中枢既是闹钟的接收者,也是开机广播的接收者;负责注册闹钟与分发任务
ScheduledService执行者一个JobIntentService,真正执行周期性的后台工作

完整的数据流如下:

ScheduledServiceDemoActivity.onCreate() │ PollReceiver.scheduleAlarms(this) ▼ AlarmManager.setRepeating(ELAPSED_REALTIME_WAKEUP, ...) │ 5 秒后首次触发,此后每 15 分钟一次 ▼ PollReceiver.onReceive() (Intent 无 action) │ ScheduledService.enqueueWork(ctxt) ▼ ScheduledService.onHandleWork() → Log.d("ScheduledService", "I ran!")

三、核心代码逐段解析

3.1 入口 Activity:一行代码完成闹钟注册

ScheduledServiceDemoActivity.java 的逻辑极其精简:

public class ScheduledServiceDemoActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PollReceiver.scheduleAlarms(this); Toast.makeText(this, R.string.alarms_scheduled, Toast.LENGTH_LONG) .show(); finish(); } }

它不做任何界面展示(Manifest 中为其指定了Theme.Translucent.NoTitleBar透明主题),只在onCreate中调用PollReceiver.scheduleAlarms(this)完成闹钟注册,弹出 "Alarms scheduled!" 提示后立即finish()。这种"启动即调度、调度即退出"的做法,适合把闹钟注册逻辑收敛到一个入口组件(如 Home Screen Widget 的配置界面或首次启动的 Activity)。

3.2 PollReceiver:闹钟注册与任务分发

PollReceiver.java 是本示例的灵魂。先看两个常量:

private static final int PERIOD=900000; // 15 minutes private static final int INITIAL_DELAY=5000; // 5 seconds
  • PERIOD = 900000毫秒 =15 分钟,即轮询周期;
  • INITIAL_DELAY = 5000毫秒 =5 秒,即首次触发前的延迟,给应用留出启动与初始化时间。

注册闹钟的核心方法:

static void scheduleAlarms(Context ctxt) { AlarmManager mgr= (AlarmManager)ctxt.getSystemService(Context.ALARM_SERVICE); Intent i=new Intent(ctxt, PollReceiver.class); PendingIntent pi=PendingIntent.getBroadcast(ctxt, 0, i, 0); mgr.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + INITIAL_DELAY, PERIOD, pi); }

这里有四个值得深挖的要点:

(1)通过getSystemService(Context.ALARM_SERVICE)获取AlarmManager这是AlarmManager的标准获取方式,与getSystemService(AlarmManager.class)等价(后者要求 API 23+)。

(2)PendingIntent.getBroadcast(ctxt, 0, i, 0)构造"广播型"待定意图

  • 第一个参数0是requestCode,用于区分同组件、不同闹钟的多个PendingIntent(本示例只有一个闹钟,取 0 即可);
  • Intent没有设置任何 action,这一点至关重要——它让闹钟触发时onReceive()收到的intent.getAction()为null,从而走"分发任务"分支(见下文 3.3);
  • 最后一个参数0是 flags,不设置FLAG_UPDATE_CURRENT等标志。

(3)AlarmManager.ELAPSED_REALTIME_WAKEUP时间基准AlarmManager提供四种"时间基准 × 唤醒行为"的组合:

类型时间基准休眠时行为说明
RTC墙钟时间(UTC 毫秒)不唤醒适合"某绝对时刻"(如每日 8 点)
RTC_WAKEUP墙钟时间(UTC 毫秒)唤醒设备适合闹钟应用
ELAPSED_REALTIME系统开机以来的毫秒数不唤醒适合相对周期任务
ELAPSED_REALTIME_WAKEUP系统开机以来的毫秒数唤醒设备本示例所用,适合周期轮询

轮询任务与墙钟时间无关,因此选ELAPSED_REALTIME_WAKEUP最合理——即使设备处于休眠,闹钟到达后也会唤醒 CPU 来派发任务(这正是WAKE_LOCK权限在 Manifest 中出现的背景,早期版本依赖它维持唤醒状态)。

(4)SystemClock.elapsedRealtime()作为触发时间基准SystemClock.elapsedRealtime()返回设备自开机以来经过的毫秒数(含休眠时间、不含用户调整系统时间的影响)。setRepeating()的触发时间参数为:

SystemClock.elapsedRealtime() + INITIAL_DELAY // 5 秒后首次触发

由于时间基准是ELAPSED_REALTIME系列,这里必须用SystemClock.elapsedRealtime()计算绝对触发时刻,而不能用System.currentTimeMillis()(那是 RTC 基准)——这是初学者最常见的错误。

3.3 onReceive 的双分支设计:闹钟触发与开机恢复共用同一入口

@Override public void onReceive(Context ctxt, Intent i) { if (i.getAction() == null) { ScheduledService.enqueueWork(ctxt); } else { scheduleAlarms(ctxt); } }

这是本示例最巧妙的设计——同一个BroadcastReceiver同时处理两条截然不同的触发路径:

  • 闹钟触发:scheduleAlarms()构造的PendingIntent的 Intent 没有 action,因此i.getAction() == null,此时只派发工作(ScheduledService.enqueueWork(ctxt)),不再重复注册闹钟——因为setRepeating()本身就会自动按周期继续触发;
  • 开机广播:系统发出android.intent.action.BOOT_COMPLETED,getAction()非空,此时只重新注册闹钟,不执行工作——因为设备刚重启,不应立刻跑一轮任务。

这样"触发即分发 / 开机即重注册"的分工,保证了闹钟在设备重启后依然存在(下文 Manifest 部分会看到RECEIVE_BOOT_COMPLETED权限与intent-filter的配合)。

3.4 ScheduledService:用 JobIntentService 承接后台工作

ScheduledService.java 继承自android.support.v4.app.JobIntentService:

public class ScheduledService extends JobIntentService { private static final int UNIQUE_JOB_ID=1337; static void enqueueWork(Context ctxt) { enqueueWork(ctxt, ScheduledService.class, UNIQUE_JOB_ID, new Intent(ctxt, ScheduledService.class)); } @Override public void onHandleWork(Intent i) { Log.d(getClass().getSimpleName(), "I ran!"); } }

要点如下:

  • JobIntentService是"Service + JobScheduler"的兼容层:在 Android 8.0(API 26)及以上,enqueueWork()实际由JobScheduler调度执行;在低版本上则退化为传统Service行为。它比裸Service更省电、更符合新版系统的后台限制,因此这个 2012 年起步的示例也与时俱进地迁移到了该实现;
  • UNIQUE_JOB_ID=1337是任务的唯一标识,用于在系统内区分不同JobIntentService的任务,多个任务排队串行执行;
  • onHandleWork()在后台线程执行,可以安全地做网络请求、数据库写入等耗时操作(示例中仅以Log.d("ScheduledService", "I ran!")演示"任务真的执行了",tag 即类名ScheduledService,便于在 logcat 中过滤)。

四、AndroidManifest:三处关键配置

AndroidManifest.xml 的配置决定了上述逻辑能否成立:

<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <activity android:name="ScheduledServiceDemoActivity" android:label="@string/app_name" android:theme="@android:style/Theme.Translucent.NoTitleBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <receiver android:name="PollReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver> <service android:name="ScheduledService" android:permission="android.permission.BIND_JOB_SERVICE" />

逐条解读:

  • RECEIVE_BOOT_COMPLETED权限:声明后才能接收开机广播。注意:Android 3.1+ 之后,应用安装后处于 "stopped" 状态时无法接收该广播,用户必须手动启动过一次应用(本示例即由ScheduledServiceDemoActivity完成首次调度)后,开机广播才会被投递;
  • WAKE_LOCK权限:ELAPSED_REALTIME_WAKEUP类闹钟需要唤醒设备,该权限为其提供支撑(这也是 Android 5.0 时代WakefulBroadcastReceiver盛行期间的标准搭配,本示例因使用JobIntentService而无需再手工管理 wakelock);
  • <receiver>的BOOT_COMPLETEDintent-filter:让PollReceiver在开机后收到广播,走onReceive()的"非空 action"分支重新注册闹钟;
  • <service>的android:permission="android.permission.BIND_JOB_SERVICE":JobIntentService在 API 26+ 上由JobScheduler绑定启动,必须声明该权限才能被系统正确调用,否则任务无法执行;
  • android:label="@string/app_name"指向 strings.xml 中的WakefulServiceDemo(应用名与类名不完全一致,属于历史遗留命名)。

五、AlarmManager 重复闹钟的 API 全貌与选型

理解 Scheduled 示例之后,可以把AlarmManager的调度 API 放在一起对比,便于在实际项目中选型(相关兄弟示例见 AlarmManager 目录):

API特点适用场景仓库示例
setRepeating()周期重复,从 Android 4.4(API 19)起为不精确触发,且不保证严格对齐周期后台轮询、数据同步Scheduled(15 分钟周期)
setInexactRepeating()显式不精确、最省电低频率任务—
set()/setWindow()一次性;setWindow允许指定触发窗口一次性延迟任务Listener(setWindow+OnAlarmListener)
setExact()/setAlarmClock()精确触发(受 Doze 与电池优化限制)闹钟、倒计时AlarmClock
setAndAllowWhileIdle()/setExactAndAllowWhileIdle()在 Doze 模式下仍可少量触发Doze 环境下的关键任务AntiDoze

关于setRepeating()的重要平台事实:本项目project.properties中target=android-19,而 Android 4.4(API 19)正是重复闹钟"精确性"的分水岭——从该版本开始,setRepeating()与setInexactRepeating()均不再保证精确触发,系统会为了省电合并、延迟触发。因此示例中 15 分钟的轮询周期是合理用法;如果需要高精度的周期任务(如倒计时),应改用"每次触发后重新set()"的自重复模式。

关于 Doze 的限制:从 Android 6.0(API 23)引入 Doze 后,设备静止充电时会限制闹钟触发频率。Scheduled 这种周期性轮询在 Doze 下会被推迟到维护窗口批量执行,无法保证每 15 分钟准点触发——这正是仓库中 AntiDoze 示例 试图解决的问题方向。

关于OnAlarmListener:Android 7.0(API 24)为AlarmManager引入了OnAlarmListener回调变体(set()/setWindow()/setExact()的监听器版本),无需再借助PendingIntent触发组件。Listener 示例 展示了它的用法:Activity实现AlarmManager.OnAlarmListener,在onAlarm()中Toast+ 重新schedule(),并在onDestroy()中mgr.cancel(this)取消回调。相比 Scheduled 的"广播分发",OnAlarmListener更轻量,但它要求持有存活的对象引用,不适合"应用已退出仍要触发"的轮询场景。

六、运行与验证

6.1 构建运行

  • 该项目可通过 settings.gradle(include ':app')导入 Android Studio 直接运行;仓库根目录的 README.markdown 提醒:导入前务必核对 gradle-wrapper.properties(本仓库使用的是 Gradle 4.4),且可能需要按本地已安装 SDK 调整compileSdkVersion;
  • 从命令行构建时,仓库出于安全考虑未附带gradlew,需要本机安装的 Gradle。

6.2 观察调度行为

  1. 安装并启动应用 → 弹出 "Alarms scheduled!" 提示,Activity 随即退出;
  2. 5 秒后首次触发,之后每 15 分钟触发一次;
  3. 用 logcat 过滤验证任务执行:
adb logcat -s ScheduledService

应能看到周期性的D/ScheduledService: I ran!输出。

  1. 查看当前已注册的系统闹钟:
adb shell dumpsys alarm

输出中Alarm com.commonsware.android.wakesvc相关条目即为本示例注册的重复闹钟(含ELAPSED_REALTIME_WAKEUP类型、15 分钟周期与触发时间)。可以借此直观看到:重启设备后该闹钟会由BOOT_COMPLETED广播自动恢复。

七、局限与演进方向

从"定时调度服务模式"的历史演进看,Scheduled 示例的写法属于该模式的成熟形态,但仍有三点值得注意:

  1. setRepeating()不精确且受 Doze 限制:如果轮询对时间精度敏感,需改为"一次性set()+ 触发后自我重排"的链式调度;
  2. JobIntentService是过渡方案:Android 8.0 之后的官方推荐是直接使用JobScheduler(API 21+)或 JetpackWorkManager,它们能更好地与系统维护窗口、网络状态、电量约束协同;Scheduled 选择JobIntentService,是在"低版本兼容"与"新平台后台限制"之间取得平衡的务实写法;
  3. 权限与启动限制:应用若从未被用户手动启动过,开机广播不会送达;从 Android 8.0 起,后台启动Service的限制也进一步收紧,轮询类需求应优先考虑JobScheduler路线。

八、相关文件速查

  • 文档:AlarmManager/Scheduled/README.markdown
  • 入口 Activity:ScheduledServiceDemoActivity.java
  • 闹钟注册与广播分发:PollReceiver.java
  • 后台执行者:ScheduledService.java
  • 权限与组件声明:AndroidManifest.xml
  • 字符串资源:strings.xml
  • 对照示例:AlarmManager/Simple、AlarmManager/Listener、AlarmManager/AntiDoze、AlarmManager/AlarmClock
  • 示例工程
  • 教程

【免费下载链接】cw-omnibus

Source code to omnibus edition of _The Busy Coder's Guide to Android Development_

项目地址:https://gitcode.com/gh_mirrors/cw/cw-omnibus
点击查看免费下载
上一篇:wagmi 中 Tempo Wallet 连接器 tempoWallet 完整指南:安装、配置与源码原理
下一篇:Home Assistant Yamaha select_scene 动作详解:在自动化与脚本中切换功放场景

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询