Skip to content

地点提醒常驻守护在进程被杀后无法恢复投递(上游 expo-location/expo-task-manager 已知缺陷,非应用层 bug) #413

Description

@LUPENGHAN

地点提醒常驻守护在进程被杀后无法恢复投递(上游 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-managerdefineTask 回调(本项目里打了诊断日志 [guard] dispatching sample to the live listener完全没有任何输出。前台常驻通知(startLocationUpdatesAsyncforegroundService 通知)全程正常显示,服务本身没有被系统拆掉。

结论:断点精确在 expo-location 把原生位置事件路由回 expo-task-manager 注册的 JS defineTask 回调这一段——原生侧一切正常,JS 侧收不到。

复现步骤

  1. 创建一条地点类型日程,走到围栏外触发 armed
  2. 强杀 App 进程(adb shell am force-stop <package>,或让系统自然回收)
  3. 重新打开 App
  4. 走回围栏内

预期:触发提醒。实际:不触发,且此后任何出圈/回圈都不再触发,直到卸载重装。

冷启动时会有一次性的例外:ExpoLocationMonitor.rebuild()frontend/src/infrastructure/location/ExpoLocationMonitor.ts)在 watch()/rebuild() 时会主动取一次当前定位喂给状态机;如果出圈时 geofence_armed 已经持久化为 true 且此刻恰好在圈内,这一次性采样会补一条提醒。这与后台持续监控是否恢复无关,容易造成"重启后会提醒一次"的误判。

已排查、已确认无效的应用层修法

  1. stopLocationUpdatesAsync + startLocationUpdatesAsync 重建注册(ReminderGuardCoordinator.ensureLocationUpdates
  2. 额外补一次 TaskManager.unregisterTaskAsync() 强制清空持久化任务记录后再重新注册
  3. 换任务名方案评估:理论上单次冷启动有效,但因为 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 自动生效,无需手动操作
  • ReminderGuardCoordinatorfrontend/src/features/reminder/application/ReminderGuardCoordinator.ts)新增了"陈旧检测":不再拿注册 options 当"还在投递"的证据,改用"本进程是否自己成功建过注册 + 最近是否收到过心跳"判断,检测到继承自上一个(已死)进程的注册时会主动重建(含真正的 TaskManager.unregisterTaskAsync()

补丁和检测逻辑均未能确认彻底解决问题(受限于测试条件未完成完整的真机出圈/回圈验证),但作为已知修复方向保留,不产生副作用。

结论

这是 Android 平台 expo-location/expo-task-manager 处理进程被杀后台任务恢复的上游限制,不是 Timeflow 自身业务代码的 bug。当前没有已验证的、可靠的应用层完整解法。已采取的缓解(升级到含 #47958 修复的 patch)方向正确但未获完整验证。

已知限制:地点类提醒在 App 进程被系统杀掉后可能失效,需要重新打开 App 才能一次性补触发(若出圈时已 armed),此后持续监控能否恢复不保证。时间类提醒不受影响(走独立的原生闹钟 TimeflowAlarm 机制)。

演示/验收时应避免中途强杀或长时间后台放置 App。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions