Android实现每日定时任务触发(附带源码)

一、项目介绍

1. 背景与动机

在许多应用场景中,我们需要在每天固定的时间点触发某项任务,例如:

  • 每日提醒:健康打卡、饮水提醒、每日一句、早安提醒

  • 定时同步:夜间自动同步数据到后台服务器

  • 日志上传:每天凌晨上传日志或诊断信息

  • 定时清理:缓存清理、数据库压缩、文件归档

这些需求都要求应用在即使被系统杀掉或设备重启后,仍能在指定时间点被唤醒并执行对应逻辑。Android 系统提供了多种机制来调度定时任务,各自有优缺点:

方案精度重启后存活Doze/节电适配适用 Android 版本依赖库
AlarmManager高(可精确)需额外重注册部分支持API 1+
JobScheduler中(可延后)原生支持良好API 21+
WorkManager中(延后)原生支持良好AndroidXandroidx.work
前台 Service + Handler高(在前台)不推荐不支持API 1+

2. 项目目标

  • 掌握多种每日定时调度方案:从系统原生 AlarmManager 到 Jetpack WorkManager,全面对比并实现。

  • 确保重启与节电模式可用:在设备重启后自动恢复调度,在 Doze 模式下可最小化延迟。

  • 整合示例代码:所有文件写在一个区块,使用注释区分,便于一键复制。

  • 性能与优化:深入分析不同方案在精度、电量、复杂度上的权衡,给出最佳实践。

  • 扩展思路:包括多任务管理、动态修改定时时间、跨应用分享任务等。


二、相关知识

  1. 系统时间与定时器

    • System.currentTimeMillis():绝对 UTC 时间

    • elapsedRealtime():开机后流逝时间(含睡眠)

    • 不同闹钟类型:RTC_WAKEUP(唤醒设备、按系统时间)、ELAPSED_REALTIME_WAKEUP(按流逝时间)

  2. Doze 与 App Standby

    • Android 6.0 引入 Doze 模式,限制 Alarm 精度和网络访问

    • setExactAndAllowWhileIdle() 可在 Doze 模式下少量触发(不保证秒级准确)

  3. 系统调度 API

    • AlarmManager:最灵活,可精确设置时间,但重启需重注册

    • JobScheduler:系统调度,有节电优化,周期最短 15 分钟

    • WorkManager:基于 JobScheduler/Firebase JobDispatcher,生命周期感知

  4. BroadcastReceiver 与 Service

    • 定时到点后通常发送广播,再在 onReceive() 内启动 Service 或执行逻辑

    • 对于耗时任务,需在 Service 中执行或使用 goAsync()

  5. 前台 Service

    • 持久后台任务,可在用户可见通知中运行,但耗电且 Android O+ 受限


三、实现思路

  1. 统一入口

    • 定义一个接口 DailyTaskScheduler,封装注册和取消调度的方法,便于切换实现。

  2. AlarmManager 实现

    • 在应用启动或重启(BOOT_COMPLETED)时注册 AlarmManager.setExactAndAllowWhileIdle(),并监听闹钟广播。

  3. JobScheduler 实现

    • 使用 JobInfo.Builder.setPeriodic() 注册周期性任务,每天运行一次。

  4. WorkManager 实现

    • 使用 PeriodicWorkRequestBuilder 设定周期,同时计算初始延迟到当天指定时间。

  5. 前台 Service 实现

    • 在 Service 中使用 Handler.postDelayed() 循环执行,保证秒级准确,但需前台通知。

  6. 重启恢复

    • 通过 RECEIVE_BOOT_COMPLETED 广播在开机后重新注册 Alarm、Job 或 Work。

  7. 节电模式适配

    • 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+ 受限。


七、性能与优化

  1. 避免重复注册

    • MyApplication 中只调用一次 scheduleDailyTask()

    • BOOT_COMPLETED 重注册时,判断已有任务并取消旧的。

  2. Doze 兼容

    • AlarmManager 使用 setExactAndAllowWhileIdle()

    • WorkManager/JobScheduler 内置对 Doze 的兼容。

  3. 抖动与延迟

    • Android 系统会对频繁闹钟进行节流,推荐 15 分钟以上周期或使用 JobScheduler/WorkManager。

  4. 节电模式

    • 合并多项任务到同一时刻执行,提高唤醒效率;

  5. 取消策略

    • 提供 UI 按钮或逻辑分支调用 cancelDailyTask(),清理 PendingIntent/Job/Work。


八、项目总结与拓展

1. 总结

  • 本文详细对比并实现了AlarmManagerJobSchedulerWorkManager 三种每日定时调度方案,并提供了统一接口和工厂模式,方便切换与扩展。

  • 分析了各方案在精度重启后存活Doze 适配等方面的差异,帮助开发者根据需求做出最佳选择。

2. 拓展方向

  1. 多任务管理:支持为不同任务指定不同时间点和逻辑,维护任务列表并提供增删改查接口。

  2. 动态重配置:在应用设置界面让用户调整定时时间,并在运行时调用 scheduleDailyTask() 动态更新。

  3. 备份与恢复:将调度时间保存在持久层(Room/SharedPreferences),在 SchedulerProvider init 时从中恢复。

  4. 通知与界面:集成系统通知,当任务触发时显示定制化通知或启动 Activity。

  5. 跨用户多进程:在多用户或多进程场景下保证任务注册的正确性和隔离。


九、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 广播并重注册。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值