Repository navigation
地点提醒常驻守护在进程被杀后无法恢复投递(上游 expo-location/expo-task-manager 已知缺陷,非应用层 bug) #413
Description
地点提醒常驻守护在进程被杀后无法恢复投递(上游 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 大版本反复出现:
- expo/expo#28959 — SDK 51,
startLocationUpdatesAsync回调收不到任何位置更新 - expo/expo#23559 — Android 上 App 从不以 headless=true 执行,OS 唤醒时若没有 React 树挂载,任务收不到东西
- expo/expo#3535 — 进程被杀后
hasStartedGeofencingAsync()/getRegisteredTasksAsync()有时直接返回空 - expo/expo#47673 — 症状与本项目高度一致("进程被系统杀掉后地点更新停止投递,只有完整重装能恢复"),已关闭,修复见 expo/expo#47958
#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。
Activity
LUPENGHAN commentedon Aug 28, 2026
更新:已定位真正根因并验证修复
最初以为是纯粹的上游库 bug(expo/expo#47673),打了对应修复 expo/expo#47958 的 backport 之后,真机测试仍然复现——排查发现还有第二层原因,两者叠加才是完整根因:
expo-task-manager(及其依赖unimodules-app-loader)默认从 Maven 拉官方预编译二进制,不从node_modules本地源码编译。 这个项目此前从没在package.json里配置过要求这两个模块从源码编译,所以不管怎么改node_modules里的代码、打多少次 patch,Gradle 从头到尾都没读过这些文件——装到手机上的一直是官方原版未修改的库。用unzip+grep直接检查安装的 APK 的 dex 内容确认了这一点(自己加的诊断字符串完全不存在于编译产物里)。修复
package.json加expo.autolinking.buildFromSource: ["expo-task-manager", "unimodules-app-loader"],逼这两个模块走本地源码编译ReminderGuardCoordinator额外加了一层陈旧检测,兜底"注册 options 看着正常但其实是继承自已死进程"的情况已通过真机端到端验证
强杀重开 App 后,走出围栏再走回来,正常触发:
原生响铃/通知正常展示,多轮心跳持续稳定,不再出现"冷启动余波后彻底沉默"的旧现象。
详见 #414。
fix(reminder): 地点提醒常驻守护在进程被杀后无法恢复投递 (#414)