本文深入解析 ImportantDays 项目中的前台提醒检查机制,包括为什么需要在 onForeground 中检查提醒、checkAndSendReminders 的完整流程、与 onCreate 中检查的区别、重复通知的处理策略,以及这种机制的优势与局限。

在这里插入图片描述

一、为什么需要前台检查?

鸿蒙的后台限制

HarmonyOS 对后台应用有严格限制:

  • 后台应用无法主动发送通知
  • 后台应用的网络和计算能力受限
  • 后台应用可能被系统杀死

因此,ImportantDays 采用"前台检查"策略:应用每次回到前台时检查是否有需要提醒的重要日。

典型场景

时间线:
08:00 用户打开应用,设置重要日(3天后提醒)
08:05 用户退出应用
...(应用在后台)
11:00 重要日进入提醒范围(假设提前3天)
...(应用仍在后台,无法发通知)
14:00 用户重新打开应用
      → onForeground 触发
      → checkAndSendReminders()
      → 发送通知!

如果没有前台检查,用户在 11:00 到 14:00 之间不会收到任何提醒。

二、检查的三个触发点

1. onCreate(冷启动)

async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  PreferenceUtil.init(this.context);
  ImportantDayManager.getInstance().init();  // init 内部调用 checkAndSendReminders
  
  const hasPermission = await NotificationUtil.checkPermission();
  if (hasPermission) {
    await ImportantDayManager.getInstance().checkAndSendReminders();  // 再次检查
  }
}

2. init()(Manager 初始化)

init(): void {
  if (this.initialized) return;
  this.allDays = PreferenceUtil.loadImportantDays();
  this.rebuildDayMap();
  this.initialized = true;
  this.checkAndSendReminders();  // 初始化时检查
}

3. onForeground(回到前台)

onForeground(): void {
  ImportantDayManager.getInstance().checkAndSendReminders();
}

三个触发点的关系

冷启动:
  onCreate → init() → checkAndSendReminders (第1次)
           → checkPermission → checkAndSendReminders (第2次)
           → onForeground → checkAndSendReminders (第3次)

热启动(从后台恢复):
  onForeground → checkAndSendReminders (唯一一次)

通知点击启动:
  onCreate → init() → checkAndSendReminders
           → onForeground → checkAndSendReminders

为什么冷启动会检查三次?

  1. init() 中的检查:确保数据加载后立即检查
  2. onCreate 中的检查:确保权限确认后再次检查
  3. onForeground 中的检查:系统标准回调

这三次检查是冗余的(checkAndSendReminders 内部有权限检查),但确保了可靠性。

三、checkAndSendReminders 完整流程

async checkAndSendReminders(): Promise<void> {
  // 第一步:获取需要提醒的重要日
  const needReminder = this.getDaysNeedReminder();
  
  // 第二步:检查通知权限
  const hasPermission = await NotificationUtil.checkPermission();
  if (!hasPermission) {
    return;  // 无权限则退出
  }
  
  // 第三步:逐个发送通知
  for (const day of needReminder) {
    const title = day.isToday() ? `今天:${day.title}` : `即将到来:${day.title}`;
    const content = day.getDisplayText();
    await NotificationUtil.publishBasic(title, content, this.getNotificationId(day.dayID));
  }
}

流程图

checkAndSendReminders()
    ↓
getDaysNeedReminder()
    ↓ 过滤:reminderEnabled && diff >= 0 && diff <= reminderDaysBefore
    ↓
needReminder = [...]
    ↓
needReminder 为空?
    ├── 是 → 结束
    └── 否 ↓
    
checkPermission()
    ↓
有权限?
    ├── 否 → 结束
    └── 是 ↓
    
遍历 needReminder
    ↓
对每个 day:
    ├── 计算 title(今天/即将到来)
    ├── 计算 content(getDisplayText)
    └── publishBasic(title, content, notificationId)
    ↓
完成

四、getDaysNeedReminder 的筛选逻辑

getDaysNeedReminder(): ImportantDay[] {
  return this.allDays.filter(d => d.shouldRemind());
}

shouldRemind 的条件

shouldRemind(): boolean {
  if (!this.reminderEnabled) return false;       // 条件1:启用提醒
  const diff = this.getDaysDiff();
  return diff >= 0 && diff <= this.reminderDaysBefore;  // 条件2:在范围内
}

筛选示例

重要日列表:
1. 妈妈生日(reminderEnabled=true, reminderDaysBefore=3, diff=2)→ 需要
2. 结婚纪念日(reminderEnabled=true, reminderDaysBefore=7, diff=10)→ 不需要(超出范围)
3. 旅行计划(reminderEnabled=false, diff=1)→ 不需要(未启用)
4. 考试日期(reminderEnabled=true, reminderDaysBefore=1, diff=-2)→ 不需要(已过期)
5. 今天会议(reminderEnabled=true, reminderDaysBefore=1, diff=0)→ 需要

needReminder = [妈妈生日, 今天会议]

五、通知标题策略

const title = day.isToday() ? `今天:${day.title}` : `即将到来:${day.title}`;
const content = day.getDisplayText();

标题分类

场景 标题格式 示例
今天 今天:${title} “今天:妈妈生日”
即将到来 即将到来:${title} “即将到来:妈妈生日”

内容

content = day.getDisplayText();
// 输出:"还有 2 天" / "就是今天" / "已过 3 天"

六、重复通知的处理

问题

每次 onForeground 都会发送通知,可能导致同一重要日被多次通知。

解决方案:稳定的通知 ID

private getNotificationId(dayID: string): number {
  let hash = 0;
  for (let i = 0; i < dayID.length; i++) {
    hash = ((hash << 5) - hash) + dayID.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash) % 100000 + ImportantDayManager.notificationIdBase;
}

ID 稳定性

同一个 dayID 总是生成相同的通知 ID。当使用相同 ID 发布通知时,新通知会覆盖旧通知。

覆盖行为

第一次 onForeground:
  → publishBasic("即将到来:妈妈生日", "还有 2 天", 10234)
  → 通知栏显示通知

第二次 onForeground(2小时后):
  → publishBasic("即将到来:妈妈生日", "还有 2 天", 10234)
  → 同 ID → 覆盖旧通知(不会出现两条)

跨天场景

Day 1 (diff=2):
  → 通知内容:"还有 2 天"

Day 2 (diff=1):
  → 通知内容:"还有 1 天"(覆盖前一天的)

由于 getDaysDiff() 基于当前时间计算,不同日期的通知内容会自动更新。

七、与 scheduleReminder 的对比

scheduleReminder(单个提醒)

private async scheduleReminder(day: ImportantDay): Promise<void> {
  const diff = day.getDaysDiff();
  if (diff < 0) return;
  
  if (diff <= day.reminderDaysBefore) {
    const title = day.isToday() ? `今天:${day.title}` : `即将到来:${day.title}`;
    const content = day.getDisplayText();
    await NotificationUtil.publishBasic(title, content, this.getNotificationId(day.dayID));
  }
}

调用时机对比

方法 调用时机 场景
scheduleReminder 添加/更新重要日后 用户刚设置了提醒
checkAndSendReminders 启动/回前台 定期检查所有重要日

逻辑对比

特性 scheduleReminder checkAndSendReminders
检查单个 否(检查所有)
检查权限
调用频率 低(用户操作时) 高(每次回前台)

scheduleReminder 不检查权限是因为它通常在用户明确设置提醒后调用,此时用户应该已授权。

八、权限检查的时序

应用启动
    ↓
onCreate
    ↓
init() → checkAndSendReminders()
              ↓
              checkPermission() → false(首次使用未授权)
              ↓
              return(不发通知)
    ↓
checkPermission() → false
    ↓
跳过 checkAndSendReminders
    ↓
用户进入"我的"页面
    ↓
打开通知开关 → requestPermission() → 授权成功
    ↓
onForeground(下次回前台时)
    ↓
checkAndSendReminders()
    ↓
checkPermission() → true
    ↓
发送通知!

九、机制的优势与局限

优势

  1. 简单可靠:不需要后台任务或定时器
  2. 零额外资源:不消耗后台 CPU 和电量
  3. 实时性好:用户打开应用立即检查
  4. 无权限要求:不需要特殊后台权限

局限

  1. 依赖用户打开应用:如果用户长时间不打开应用,无法发送提醒
  2. 非实时:提醒延迟到用户打开应用时
  3. 无精确定时:无法在特定时间点(如早上8点)发送通知

改进方向

如果需要精确定时提醒,可以使用鸿蒙的后台任务:

// 概念性改进(需要后台任务权限)
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';

// 注册延迟提醒任务
backgroundTaskManager.startBackgroundRunning(context, ...)

但这需要额外的权限申请和更复杂的实现。

十、总结

前台提醒检查机制的设计体现了以下原则:

  1. 务实选择:在后台限制下选择最简单可行的方案
  2. 三重保障:onCreate + init + onForeground 确保不遗漏
  3. 权限先行:每次检查前确认权限
  4. 防重复:稳定通知 ID 实现覆盖而非追加
  5. 内容自更新:基于当前时间计算内容,通知随时间推移自动更新

这套机制虽然简单,但在不需要精确定时的场景下完全够用,是鸿蒙应用通知提醒的务实之选。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐