在 Android 中实现“**每天 8 点以后执行一次任务**”,是一个非常常见的需求,比如:
- 每日签到提醒
- 今日计划推送
- 数据自动同步
由于现代 Android 系统的省电机制(Doze、App Standby、厂商限制),不能使用简单的 `Handler` 或 `Timer`,而应采用系统级调度方案。
---
## ✅ 最佳实践:使用 `AlarmManager` + `BroadcastReceiver` 实现精准触发
### 🎯 目标:
> **每天 8:00 整或稍后时间执行一次任务,即使应用被杀死也能运行**
---
### ✅ 方案一:使用 `AlarmManager`(推荐用于精确时间)
#### 1. 创建 BroadcastReceiver 接收闹钟事件
```java
// DailyEightAMReceiver.java
public class DailyEightAMReceiver extends BroadcastReceiver {
private static final String TAG = "DailyEightAM";
@Override
public void onReceive(Context context, Intent intent) {
if ("com.example.EIGHT_AM_EVENT".equals(intent.getAction())) {
Log.d(TAG, "⏰ 检测到 8:00,开始执行每日任务: " + new Date());
// 👇 执行你的业务逻辑
performDailyTask(context);
}
}
private void performDailyTask(Context context) {
// 示例:发送通知
sendNotification(context);
// 其他操作:启动 WorkManager、更新数据库、签到等
}
private void sendNotification(Context context) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
"eight_am_channel",
"每日 8 点提醒",
NotificationManager.IMPORTANCE_DEFAULT
);
manager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "eight_am_channel")
.setSmallIcon(R.drawable.ic_alarm)
.setContentTitle("早安!")
.setContentText("今天的目标完成了吗?")
.setAutoCancel(true)
.setWhen(System.currentTimeMillis());
manager.notify(800, builder.build());
}
}
```
#### 2. 创建调度器设置每天 8:00 触发
```java
// DailyTaskScheduler.java
public class DailyTaskScheduler {
private static final String ALARM_ACTION = "com.example.EIGHT_AM_EVENT";
private static final int REQUEST_CODE = 800;
public static void schedule(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(ALARM_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
// 设置每天 8:00 触发
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 如果当前时间已过 8:00,则设为明天 8:00
if (calendar.getTimeInMillis() <= System.currentTimeMillis()) {
calendar.add(Calendar.DAY_OF_YEAR, 1);
}
long triggerAtMillis = calendar.getTimeInMillis();
if (alarmManager != null) {
// 使用 setInexactRepeating 更省电(推荐)
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
);
}
}
// 取消任务
public static void cancel(Context context) {
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(ALARM_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE
);
if (pendingIntent != null && alarmManager != null) {
alarmManager.cancel(pendingIntent);
pendingIntent.cancel();
}
}
}
```
#### 3. 在 AndroidManifest.xml 中注册 Receiver
```xml
<receiver android:name=".DailyEightAMReceiver" />
```
> ⚠️ 注意:从 Android 8.0(API 26)开始,不能静态注册隐式广播(除了少数例外)。但我们可以使用 **显式 Intent + FLAG_IMMUTABLE** 来绕过限制。
✅ 所以确保你使用的是 `PendingIntent.FLAG_IMMUTABLE`,这是 Android 12+ 的要求。
---
#### 4. 启动任务(建议在 Application 或首次启动时调用)
```java
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
DailyTaskScheduler.schedule(this); // 设置每天 8:00 执行
}
}
```
并在 `AndroidManifest.xml` 声明:
```xml
<application
android:name=".MyApplication"
... >
</application>
```
---
### ✅ 方案二:使用 `WorkManager`(更现代,但不保证准时)
如果你不需要“正好 8:00”执行,而是“8:00 之后某时执行”,可以用 `WorkManager`。
```java
// 使用 PeriodicWorkRequest 每 24 小时运行一次
PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(EightAMWorker.class, 24, TimeUnit.HOURS)
.setInitialDelay(calculateInitialDelayToEightAM(), TimeUnit.MILLISECONDS)
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"daily_8am_task",
ExistingPeriodicWorkPolicy.REPLACE,
workRequest
);
```
其中 `calculateInitialDelayToEightAM()` 返回从现在到下一个 8:00 的毫秒数。
📌 优点:兼容 Doze 模式、自动恢复
📌 缺点:可能延迟到 9:00 甚至更晚才执行
---
### ✅ 方案三:设备重启后恢复任务
如果用户重启手机,`AlarmManager` 的任务会丢失(除非你监听 `BOOT_COMPLETED`)。
#### 添加权限:
```xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
```
#### 创建 BootReceiver:
```java
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
// 重新设置闹钟
DailyTaskScheduler.schedule(context);
}
}
}
```
#### 注册:
```xml
<receiver android:name=".BootReceiver" android:enabled="true" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
```
---
## ✅ 补充:如何计算“到今天或明天 8:00”的延迟?
```java
public static long calculateInitialDelayToEightAM() {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, 8);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
long eightAMToday = calendar.getTimeInMillis();
long now = System.currentTimeMillis();
if (now > eightAMToday) {
calendar.add(Calendar.DAY_OF_YEAR, 1); // 明天 8:00
}
return calendar.getTimeInMillis() - now;
}
```
可用于 `WorkManager` 的 `.setInitialDelay(...)`
---
## ✅ 总结对比
| 方案 | 是否准时 | 是否常驻 | 是否需 BOOT 广播 | 推荐场景 |
|------|----------|-----------|------------------|-----------|
| `AlarmManager` | ✅ 较准时(±几分钟) | ❌ 不常驻 | ✅ 需要 | 精确提醒、通知 |
| `WorkManager` | ⚠️ 延迟容忍(可能晚几小时) | ✅ 自动恢复 | ❌ 不需要 | 后台同步、非实时任务 |
| `Handler/Timer` | ❌ 应用退出即失效 | ✅ 常驻 | ❌ | ❌ 不推荐 |
---
## ✅ 推荐组合策略(生产环境)
```text
✅ 使用 AlarmManager 实现“每天 8:00 提醒”
✅ 结合 Notification 发送通知
✅ 使用 BOOT_COMPLETED 广播恢复任务
✅ 加上 WorkManager 处理后台逻辑(如数据上传)
```
这样既能保证及时性,又能应对各种系统限制。
---