一、项目介绍
1. 背景与动机
在许多应用场景中,我们需要在每天固定的时间点触发某项任务,例如:
-
每日提醒:健康打卡、饮水提醒、每日一句、早安提醒
-
定时同步:夜间自动同步数据到后台服务器
-
日志上传:每天凌晨上传日志或诊断信息
-
定时清理:缓存清理、数据库压缩、文件归档
这些需求都要求应用在即使被系统杀掉或设备重启后,仍能在指定时间点被唤醒并执行对应逻辑。Android 系统提供了多种机制来调度定时任务,各自有优缺点:
方案 | 精度 | 重启后存活 | Doze/节电适配 | 适用 Android 版本 | 依赖库 |
---|---|---|---|---|---|
AlarmManager | 高(可精确) | 需额外重注册 | 部分支持 | API 1+ | 无 |
JobScheduler | 中(可延后) | 原生支持 | 良好 | API 21+ | 无 |
WorkManager | 中(延后) | 原生支持 | 良好 | AndroidX | androidx.work |
前台 Service + Handler | 高(在前台) | 不推荐 | 不支持 | API 1+ | 无 |
2. 项目目标
-
掌握多种每日定时调度方案:从系统原生
AlarmManager
到 JetpackWorkManager
,全面对比并实现。 -
确保重启与节电模式可用:在设备重启后自动恢复调度,在 Doze 模式下可最小化延迟。
-
整合示例代码:所有文件写在一个区块,使用注释区分,便于一键复制。
-
性能与优化:深入分析不同方案在精度、电量、复杂度上的权衡,给出最佳实践。
-
扩展思路:包括多任务管理、动态修改定时时间、跨应用分享任务等。
二、相关知识
-
系统时间与定时器
-
System.currentTimeMillis()
:绝对 UTC 时间 -
elapsedRealtime()
:开机后流逝时间(含睡眠) -
不同闹钟类型:
RTC_WAKEUP
(唤醒设备、按系统时间)、ELAPSED_REALTIME_WAKEUP
(按流逝时间)
-
-
Doze 与 App Standby
-
Android 6.0 引入 Doze 模式,限制 Alarm 精度和网络访问
-
setExactAndAllowWhileIdle()
可在 Doze 模式下少量触发(不保证秒级准确)
-
-
系统调度 API
-
AlarmManager:最灵活,可精确设置时间,但重启需重注册
-
JobScheduler:系统调度,有节电优化,周期最短 15 分钟
-
WorkManager:基于 JobScheduler/Firebase JobDispatcher,生命周期感知
-
-
BroadcastReceiver 与 Service
-
定时到点后通常发送广播,再在
onReceive()
内启动 Service 或执行逻辑 -
对于耗时任务,需在 Service 中执行或使用
goAsync()
-
-
前台 Service
-
持久后台任务,可在用户可见通知中运行,但耗电且 Android O+ 受限
-
三、实现思路
-
统一入口
-
定义一个接口
DailyTaskScheduler
,封装注册和取消调度的方法,便于切换实现。
-
-
AlarmManager 实现
-
在应用启动或重启(BOOT_COMPLETED)时注册
AlarmManager.setExactAndAllowWhileIdle()
,并监听闹钟广播。
-
-
JobScheduler 实现
-
使用
JobInfo.Builder.setPeriodic()
注册周期性任务,每天运行一次。
-
-
WorkManager 实现
-
使用
PeriodicWorkRequestBuilder
设定周期,同时计算初始延迟到当天指定时间。
-
-
前台 Service 实现
-
在 Service 中使用
Handler.postDelayed()
循环执行,保证秒级准确,但需前台通知。
-
-
重启恢复
-
通过
RECEIVE_BOOT_COMPLETED
广播在开机后重新注册 Alarm、Job 或 Work。
-
-
节电模式适配
-
AlarmManager 使用
setExactAndAllowWhileIdle()
;WorkManager 与 JobScheduler 内置 Doze 兼容。
-
四、环境与依赖
// 文件: app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.dailytasks"
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0"
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.0"
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.work:work-runtime-ktx:2.8.1'
}
五、整合代码
// =======================================================
// 文件: app/src/main/AndroidManifest.xml
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dailytasks">
<!-- 权限:开机重启后恢复 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="每日定时任务"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.DailyTasks">
<!-- 主界面 -->
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- 定时闹钟的广播接收 -->
<receiver android:name=".AlarmReceiver"
android:exported="false"/>
<!-- 监听设备开机完成 -->
<receiver android:name=".BootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/MyApplication.kt
// =======================================================
package com.example.dailytasks
import android.app.Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 应用启动时,初始化并注册每日定时任务
SchedulerProvider.init(this)
}
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/SchedulerProvider.kt
// =======================================================
package com.example.dailytasks
import android.content.Context
/**
* 简单工厂,提供当下使用的调度器实现
*/
object SchedulerProvider {
private lateinit var impl: DailyTaskScheduler
fun init(ctx: Context) {
// 此处可切换不同实现
impl = AlarmDailyScheduler(ctx)
impl.scheduleDailyTask(hour = 9, minute = 0, requestCode = 100)
}
fun rescheduleAfterBoot() {
impl.rescheduleDailyTask()
}
fun cancel() {
impl.cancelDailyTask()
}
}
/** 每日定时任务的接口 */
interface DailyTaskScheduler {
/**
* 安排每天指定时刻触发任务
* @param hour 24h 小时
* @param minute 分钟
* @param requestCode 闹钟 request code,用于 PendingIntent 唯一标识
*/
fun scheduleDailyTask(hour: Int, minute: Int, requestCode: Int)
/** 设备重启后需调用此方法重注册 */
fun rescheduleDailyTask()
/** 取消定时任务 */
fun cancelDailyTask()
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/AlarmDailyScheduler.kt
// 使用 AlarmManager 实现每日定时
// =======================================================
package com.example.dailytasks
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import java.util.*
class AlarmDailyScheduler(private val ctx: Context) : DailyTaskScheduler {
private val am = ctx.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private var hour = 0
private var minute = 0
private var reqCode = 0
override fun scheduleDailyTask(hour: Int, minute: Int, requestCode: Int) {
this.hour = hour; this.minute = minute; this.reqCode = requestCode
// 计算首次触发时间
val cal = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND,0)
if (timeInMillis <= System.currentTimeMillis()) {
add(Calendar.DATE,1)
}
}
val pi = PendingIntent.getBroadcast(
ctx, requestCode,
Intent(ctx, AlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or getImmutableFlag()
)
// 在 Doze 下使用 allowWhileIdle
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
am.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
cal.timeInMillis,
pi
)
} else {
am.setExact(
AlarmManager.RTC_WAKEUP,
cal.timeInMillis,
pi
)
}
}
override fun rescheduleDailyTask() {
scheduleDailyTask(hour, minute, reqCode)
}
override fun cancelDailyTask() {
val pi = PendingIntent.getBroadcast(
ctx, reqCode,
Intent(ctx, AlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or getImmutableFlag()
)
am.cancel(pi)
}
private fun getImmutableFlag(): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE else 0
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/AlarmReceiver.kt
// 接收闹钟广播并执行任务
// =======================================================
package com.example.dailytasks
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
// TODO: 在此执行每日任务
Toast.makeText(ctx, "每日任务触发(AlarmManager)", Toast.LENGTH_SHORT).show()
// 任务执行后,重新注册下一次
SchedulerProvider.rescheduleAfterBoot()
}
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/BootReceiver.kt
// 监听系统重启,重注册任务
// =======================================================
package com.example.dailytasks
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// 重启后重新安排任务
SchedulerProvider.rescheduleAfterBoot()
}
}
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/JobSchedulerDaily.kt
// 使用 JobScheduler 实现每日定时
// =======================================================
package com.example.dailytasks
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.Build
import java.util.concurrent.TimeUnit
class JobSchedulerDaily(private val ctx: Context) : DailyTaskScheduler {
private val js = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
private var jobId = 101
override fun scheduleDailyTask(hour: Int, minute: Int, requestCode: Int) {
this.jobId = requestCode
// 计算初始延迟
val now = System.currentTimeMillis()
val cal = java.util.Calendar.getInstance().apply {
set(java.util.Calendar.HOUR_OF_DAY, hour)
set(java.util.Calendar.MINUTE, minute)
set(java.util.Calendar.SECOND, 0)
if (timeInMillis <= now) add(java.util.Calendar.DATE, 1)
}
val initialDelay = cal.timeInMillis - now
// 构建 JobInfo
val component = ComponentName(ctx, DailyJobService::class.java)
val info = JobInfo.Builder(jobId, component)
.setMinimumLatency(initialDelay)
.setOverrideDeadline(initialDelay + TimeUnit.MINUTES.toMillis(5))
.setPersisted(true)
.setPeriodic(TimeUnit.DAYS.toMillis(1), TimeUnit.HOURS.toMillis(1))
.build()
js.schedule(info)
}
override fun rescheduleDailyTask() {
js.cancel(jobId)
scheduleDailyTask(0, 0, jobId) // 需重新传入 hour/min,与上面持久化保存
}
override fun cancelDailyTask() {
js.cancel(jobId)
}
}
class DailyJobService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
// 执行定时任务
Toast.makeText(this, "每日任务触发(JobScheduler)", Toast.LENGTH_SHORT).show()
jobFinished(params, false)
return true
}
override fun onStopJob(params: JobParameters?): Boolean = false
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/WorkManagerDaily.kt
// 使用 WorkManager 实现每日定时
// =======================================================
package com.example.dailytasks
import android.content.Context
import android.widget.Toast
import androidx.work.*
import java.util.Calendar
import java.util.concurrent.TimeUnit
class WorkManagerDaily(private val ctx: Context) : DailyTaskScheduler {
private val wm = WorkManager.getInstance(ctx)
private var workName = "daily_work"
override fun scheduleDailyTask(hour: Int, minute: Int, requestCode: Int) {
// 先取消已有
wm.cancelUniqueWork(workName)
// 计算初始延迟
val now = System.currentTimeMillis()
val cal = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
if (timeInMillis <= now) add(Calendar.DATE,1)
}
val initialDelay = cal.timeInMillis - now
val request = PeriodicWorkRequestBuilder<DailyWorker>(1, TimeUnit.DAYS)
.setInitialDelay(initialDelay, TimeUnit.MILLISECONDS)
.build()
wm.enqueueUniquePeriodicWork(workName, ExistingPeriodicWorkPolicy.REPLACE, request)
}
override fun rescheduleDailyTask() {
// 与 scheduleDailyTask 同理,可重复调用
}
override fun cancelDailyTask() {
wm.cancelUniqueWork(workName)
}
}
class DailyWorker(appCtx: Context, params: WorkerParameters)
: CoroutineWorker(appCtx, params) {
override suspend fun doWork(): Result {
Toast.makeText(applicationContext, "每日任务触发(WorkManager)", Toast.LENGTH_SHORT).show()
return Result.success()
}
}
// =======================================================
// 文件: app/src/main/java/com/example/dailytasks/MainActivity.kt
// =======================================================
package com.example.dailytasks
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.example.dailytasks.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.btnCancel.setOnClickListener {
SchedulerProvider.cancel()
}
}
}
// =======================================================
// 文件: app/src/main/res/layout/activity_main.xml
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="每日定时任务示例"
android:textSize="20sp"
android:layout_marginBottom="24dp"/>
<Button
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消任务"/>
</LinearLayout>
六、代码解读
1. 接口与工厂(SchedulerProvider
)
-
通过
DailyTaskScheduler
抽象接口屏蔽具体实现,方便切换或并存多种方案。 -
在
MyApplication
中调用SchedulerProvider.init()
,应用启动即注册每日任务。
2. AlarmManager 实现
-
精度最高:使用
setExactAndAllowWhileIdle()
,即使在 Doze 模式也可触发(但 Android 限制频率)。 -
重启恢复:通过
BOOT_COMPLETED
广播在开机后再次注册闹钟。 -
缺点:开发者需手动管理重复注册和取消。
3. JobScheduler 实现
-
系统调度:
setPeriodic()
设置每天 24 小时周期,但会根据系统策略合并执行。 -
持久化:
setPersisted(true)
使任务重启后保留。 -
缺点:最小周期 15 分钟,不保证精确到点触发;JobInfo 无法指定时间点,只能通过初始延迟 + 周期。
4. WorkManager 实现
-
跨进程可靠:内部自动选择最佳调度后端(JobScheduler/AlarmManager/Firebase)。
-
易用:通过
PeriodicWorkRequest
自动重试和持久化。 -
缺点:周期任务最小间隔 15 分钟,精准度有限;启动时需计算初始延迟。
5. 前台 Service(未示例)
-
高精度:可在 Service 里使用
Handler.postDelayed()
循环。 -
缺点:必须显示前台通知,耗电且在 Android O+ 受限。
七、性能与优化
-
避免重复注册
-
在
MyApplication
中只调用一次scheduleDailyTask()
; -
在
BOOT_COMPLETED
重注册时,判断已有任务并取消旧的。
-
-
Doze 兼容
-
AlarmManager 使用
setExactAndAllowWhileIdle()
; -
WorkManager/JobScheduler 内置对 Doze 的兼容。
-
-
抖动与延迟
-
Android 系统会对频繁闹钟进行节流,推荐 15 分钟以上周期或使用 JobScheduler/WorkManager。
-
-
节电模式
-
合并多项任务到同一时刻执行,提高唤醒效率;
-
-
取消策略
-
提供 UI 按钮或逻辑分支调用
cancelDailyTask()
,清理 PendingIntent/Job/Work。
-
八、项目总结与拓展
1. 总结
-
本文详细对比并实现了AlarmManager、JobScheduler、WorkManager 三种每日定时调度方案,并提供了统一接口和工厂模式,方便切换与扩展。
-
分析了各方案在精度、重启后存活、Doze 适配等方面的差异,帮助开发者根据需求做出最佳选择。
2. 拓展方向
-
多任务管理:支持为不同任务指定不同时间点和逻辑,维护任务列表并提供增删改查接口。
-
动态重配置:在应用设置界面让用户调整定时时间,并在运行时调用
scheduleDailyTask()
动态更新。 -
备份与恢复:将调度时间保存在持久层(Room/SharedPreferences),在
SchedulerProvider
init 时从中恢复。 -
通知与界面:集成系统通知,当任务触发时显示定制化通知或启动 Activity。
-
跨用户多进程:在多用户或多进程场景下保证任务注册的正确性和隔离。
九、FAQ
Q1:AlarmManager 与 WorkManager 哪个更好?
A1:若需秒级精准触发,且任务轻量,使用 AlarmManager;若需系统调度兼容性和持久性,使用 WorkManager。
Q2:JobScheduler 可以在 API<21 上使用吗?
A2:原生不行,但 WorkManager 会自动降级为兼容后端(AlarmManager 或 Firebase JobDispatcher)。
Q3:如何在应用被杀死后仍能触发?
A3:AlarmManager 的闹钟和 WorkManager 的周期任务均可唤醒应用,而前台 Service 在应用被强制杀死时也会停止。
Q4:为什么 setRepeating 不推荐?
A4:从 API19 开始,系统会批量合并 setRepeating()
的闹钟,精度难以保证,推荐使用 setExactAndAllowWhileIdle()
并手动重注册。
Q5:如何处理夏令时、时区变更等问题?
A5:建议使用 ELAPSED_REALTIME
类型的闹钟,避免受系统时钟调整影响;若需按本地时间触发,可监听 TIMEZONE_CHANGED
广播并重注册。