地点提醒常驻守护在进程被杀后无法恢复投递(上游 expo-location/expo-task-manager 已知缺陷,非应用层 bug)
现象
设备(小米 MIUI)出圈后走回围栏内,地点提醒不触发。且必须在同一个 App 进程存活期间内出圈+回圈才会响;只要期间进程被系统杀掉(force-stop 或系统后台回收)后重开,之后的出圈/回圈就再也不会触发提醒——直到卸载重装。
关键证据:系统在正常投递,JS 收不到
adb shell dumpsys location 证实:进程被杀重开之后,系统 LocationManager 里对应的 GPS 订阅一直是活的、按注册间隔(15s)持续投递样本:
service: ProviderRequest[@+15s0ms, HIGH_ACCURACY, WorkSource{... com.anonymous.timeflow}]
listeners:
.../fused_location_provider/4E23CD76 Request[@+15s0ms HIGH_ACCURACY ...]
...
22:32:01.869: gps provider delivered location[1] to .../4E23CD76
22:32:16.869: gps provider delivered location[1] to .../4E23CD76
22:32:31.866: gps provider delivered location[1] to .../4E23CD76
22:32:45.862: gps provider delivered location[1] to .../4E23CD76
同一时间段,App 内 expo-task-manager 的 defineTask 回调(本项目里打了诊断日志 [guard] dispatching sample to the live listener)完全没有任何输出。前台常驻通知(startLocationUpdatesAsync 的 foregroundService 通知)全程正常显示,服务本身没有被系统拆掉。
结论:断点精确在 expo-location 把原生位置事件路由回 expo-task-manager 注册的 JS defineTask 回调这一段——原生侧一切正常,JS 侧收不到。
复现步骤
- 创建一条地点类型日程,走到围栏外触发
armed
- 强杀 App 进程(
adb shell am force-stop <package>,或让系统自然回收)
- 重新打开 App
- 走回围栏内
预期:触发提醒。实际:不触发,且此后任何出圈/回圈都不再触发,直到卸载重装。
冷启动时会有一次性的例外:ExpoLocationMonitor.rebuild()(frontend/src/infrastructure/location/ExpoLocationMonitor.ts)在 watch()/rebuild() 时会主动取一次当前定位喂给状态机;如果出圈时 geofence_armed 已经持久化为 true 且此刻恰好在圈内,这一次性采样会补一条提醒。这与后台持续监控是否恢复无关,容易造成"重启后会提醒一次"的误判。
已排查、已确认无效的应用层修法
stopLocationUpdatesAsync + startLocationUpdatesAsync 重建注册(ReminderGuardCoordinator.ensureLocationUpdates)
- 额外补一次
TaskManager.unregisterTaskAsync() 强制清空持久化任务记录后再重新注册
- 换任务名方案评估:理论上单次冷启动有效,但因为
defineTask() 必须在模块顶层同步调用(否则 headless 唤醒时找不到对应 handler),无法安全地用运行时动态生成的任务名实现,评估后放弃
根因与上游状态
这是 expo-location/expo-task-manager 在 Android 上处理"进程被杀后台任务恢复"的已知问题类别,多个 SDK 大版本反复出现:
#47958 的根因(TaskService.java):sHeadlessTaskManagers 会在 invalidateApp() 后被提前清空,此时 Android context 实际还活着(不满足重建条件),导致 getTaskManager() 此后一直返回 null——事件不是没收到,是收到了却找不到管理器处理,静默丢进队列。
该修复尚未发布到任何稳定 SDK 补丁版本(已核实 57.0.9/57.0.14/56.0.27 均不含此修复,仅存在于未转正的 58.0.0-canary 分支)。
已应用的缓解
- 本仓库已通过
patch-package 把 #47958 的 diff 打进 node_modules/expo-task-manager(见 frontend/patches/expo-task-manager+57.0.9.patch),postinstall 自动生效,无需手动操作
ReminderGuardCoordinator(frontend/src/features/reminder/application/ReminderGuardCoordinator.ts)新增了"陈旧检测":不再拿注册 options 当"还在投递"的证据,改用"本进程是否自己成功建过注册 + 最近是否收到过心跳"判断,检测到继承自上一个(已死)进程的注册时会主动重建(含真正的 TaskManager.unregisterTaskAsync())
补丁和检测逻辑均未能确认彻底解决问题(受限于测试条件未完成完整的真机出圈/回圈验证),但作为已知修复方向保留,不产生副作用。
结论
这是 Android 平台 expo-location/expo-task-manager 处理进程被杀后台任务恢复的上游限制,不是 Timeflow 自身业务代码的 bug。当前没有已验证的、可靠的应用层完整解法。已采取的缓解(升级到含 #47958 修复的 patch)方向正确但未获完整验证。
已知限制:地点类提醒在 App 进程被系统杀掉后可能失效,需要重新打开 App 才能一次性补触发(若出圈时已 armed),此后持续监控能否恢复不保证。时间类提醒不受影响(走独立的原生闹钟 TimeflowAlarm 机制)。
演示/验收时应避免中途强杀或长时间后台放置 App。
地点提醒常驻守护在进程被杀后无法恢复投递(上游 expo-location/expo-task-manager 已知缺陷,非应用层 bug)
现象
设备(小米 MIUI)出圈后走回围栏内,地点提醒不触发。且必须在同一个 App 进程存活期间内出圈+回圈才会响;只要期间进程被系统杀掉(force-stop 或系统后台回收)后重开,之后的出圈/回圈就再也不会触发提醒——直到卸载重装。
关键证据:系统在正常投递,JS 收不到
adb shell dumpsys location证实:进程被杀重开之后,系统 LocationManager 里对应的 GPS 订阅一直是活的、按注册间隔(15s)持续投递样本:同一时间段,App 内
expo-task-manager的defineTask回调(本项目里打了诊断日志[guard] dispatching sample to the live listener)完全没有任何输出。前台常驻通知(startLocationUpdatesAsync的foregroundService通知)全程正常显示,服务本身没有被系统拆掉。结论:断点精确在
expo-location把原生位置事件路由回expo-task-manager注册的 JSdefineTask回调这一段——原生侧一切正常,JS 侧收不到。复现步骤
armedadb shell am force-stop <package>,或让系统自然回收)预期:触发提醒。实际:不触发,且此后任何出圈/回圈都不再触发,直到卸载重装。
冷启动时会有一次性的例外:
ExpoLocationMonitor.rebuild()(frontend/src/infrastructure/location/ExpoLocationMonitor.ts)在watch()/rebuild()时会主动取一次当前定位喂给状态机;如果出圈时geofence_armed已经持久化为 true 且此刻恰好在圈内,这一次性采样会补一条提醒。这与后台持续监控是否恢复无关,容易造成"重启后会提醒一次"的误判。已排查、已确认无效的应用层修法
stopLocationUpdatesAsync+startLocationUpdatesAsync重建注册(ReminderGuardCoordinator.ensureLocationUpdates)TaskManager.unregisterTaskAsync()强制清空持久化任务记录后再重新注册defineTask()必须在模块顶层同步调用(否则 headless 唤醒时找不到对应 handler),无法安全地用运行时动态生成的任务名实现,评估后放弃根因与上游状态
这是
expo-location/expo-task-manager在 Android 上处理"进程被杀后台任务恢复"的已知问题类别,多个 SDK 大版本反复出现:startLocationUpdatesAsync回调收不到任何位置更新hasStartedGeofencingAsync()/getRegisteredTasksAsync()有时直接返回空#47958的根因(TaskService.java):sHeadlessTaskManagers会在invalidateApp()后被提前清空,此时 Android context 实际还活着(不满足重建条件),导致getTaskManager()此后一直返回null——事件不是没收到,是收到了却找不到管理器处理,静默丢进队列。该修复尚未发布到任何稳定 SDK 补丁版本(已核实
57.0.9/57.0.14/56.0.27均不含此修复,仅存在于未转正的58.0.0-canary分支)。已应用的缓解
patch-package把#47958的 diff 打进node_modules/expo-task-manager(见frontend/patches/expo-task-manager+57.0.9.patch),postinstall自动生效,无需手动操作ReminderGuardCoordinator(frontend/src/features/reminder/application/ReminderGuardCoordinator.ts)新增了"陈旧检测":不再拿注册 options 当"还在投递"的证据,改用"本进程是否自己成功建过注册 + 最近是否收到过心跳"判断,检测到继承自上一个(已死)进程的注册时会主动重建(含真正的TaskManager.unregisterTaskAsync())补丁和检测逻辑均未能确认彻底解决问题(受限于测试条件未完成完整的真机出圈/回圈验证),但作为已知修复方向保留,不产生副作用。
结论
这是 Android 平台
expo-location/expo-task-manager处理进程被杀后台任务恢复的上游限制,不是 Timeflow 自身业务代码的 bug。当前没有已验证的、可靠的应用层完整解法。已采取的缓解(升级到含#47958修复的 patch)方向正确但未获完整验证。已知限制:地点类提醒在 App 进程被系统杀掉后可能失效,需要重新打开 App 才能一次性补触发(若出圈时已 armed),此后持续监控能否恢复不保证。时间类提醒不受影响(走独立的原生闹钟
TimeflowAlarm机制)。演示/验收时应避免中途强杀或长时间后台放置 App。