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-manager 的 defineTask 回调(本项目里打了诊断日志 [guard] dispatching sample to the live listener)完全没有任何输出。前台常驻通知(startLocationUpdatesAsync 的 foregroundService 通知)全程正常显示,服务本身没有被系统拆掉。

结论:断点精确在 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 自动生效,无需手动操作
  • 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

  1. LUPENGHAN commented on Aug 28, 2026

    @LUPENGHAN
    ContributorAuthor

    更新:已定位真正根因并验证修复

    最初以为是纯粹的上游库 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"],逼这两个模块走本地源码编译
    • 编译进去之后,上游 #47958 的 backport 才第一次真正生效
    • ReminderGuardCoordinator 额外加了一层陈旧检测,兜底"注册 options 看着正常但其实是继承自已死进程"的情况

    已通过真机端到端验证

    强杀重开 App 后,走出围栏再走回来,正常触发:

    [reminder] TRIGGERED, delivering ... 拿快递
    

    原生响铃/通知正常展示,多轮心跳持续稳定,不再出现"冷启动余波后彻底沉默"的旧现象。

    详见 #414。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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