在 iOS 后台记录 GPS,而且不把这趟弄丢
iOS 随时可以挂起或者终止你的记录器,而且不欠你一句提醒。对一个本职工作就是把你去过哪里写下来的 App 来说,这意味着什么,以及到底什么才真正保护得住一趟记录。
你开始记录,把手机放进口袋,骑了三个小时,拿出来一看,App 显示二十分钟。这是关于手机记录器最常见的一类严重投诉,而原因几乎从来不是 GPS。
iOS 到底对你的 App 做了什么
一个在后台的 iOS App 并不只是跑得慢一点。它会在几种状态之间移动:还在运行、被挂起(冻结在内存里,什么都不执行)、被终止(没了)。系统根据内存压力、发热情况、电量以及它自己的判断,把你在这些状态之间搬来搬去,而且它没有义务提前通知你。
声明了定位后台模式的 App,在定位更新持续到达的时候可以一直跑下去,整件事情能成立就是靠这个。和一个普通 App 相比,这已经是一个很强的保障了。但它不是一个绝对的保障:在真实的内存压力下——一个大型游戏、一个相机 App、一台内存本来就小的手机——你的记录器照样可能在半路上被杀掉。
真正要命的失败就是从这里来的。如果这个 App 一直把这趟路攥在内存里,等着你按下停止之后才往下写,那么从上一次保存到现在的所有东西就都没了。不是损坏,是没了,而且没有任何报错,因为那个知道这些数据的进程已经不存在了。
唯一真正的保护:来一个写一个
这件事没有什么取巧的办法。保护的方式,就是让「还没保存的数据」这个东西从一开始就不存在。
在 Roam 里,一次定位回调就是一个批次,一个批次就是一次数据库事务,轨迹点和这次记录的累计数字一起提交。没有任何缓冲区在等着这趟路结束,写入这条路上也没有哪个分支会把某个点悄悄丢掉——提交不下去的批次会直接报错并被挂进告警,而不是被默默吞掉。如果 App 在第四十公里被杀掉,第四十公里早就在手机上了。
把累计数字和轨迹点放在同一次事务里提交,其分量比看上去要重。它意味着汇总那一行永远不可能描述一组并不在磁盘上的点,所以被杀掉之后,不存在两者对不上的那个窗口——而且恢复的时候也不需要把两万八千个点重放一遍才能算出你走到了哪里,它读那一行就够了。
同一个想法的另一半:什么都不删。滤波、平滑和自动暂停从不从数据库里移走任何一行。它们只是作为标记写在那个点上,原始数据流按它到达时的样子原样留着,这样一来,当某个启发式算法在一趟真实的路上判断错了,原始采样点还在,可以拿它们重新推导。一个把自己不认同的采样点悄悄丢掉的 App,等于亲手销毁了能证明它判断错了的那份证据。
被终止之后,怎么重新站起来
如果你给了「始终允许」的定位权限,iOS 就可以在发生显著位置变化的时候,把一个已经被终止的 App 在后台重新拉起来。这是一条真实存在的救命绳,同时也带着一个陷阱,很多记录器都在这上面栽过。
在被重新拉起的那一刻,App 只有一个非常短、而且长度不可预测的窗口,之后系统随时可能再次把它挂起——关键在于,它可以在任何一个挂起点上被挂起,而在现代 Swift 里,这意味着任何一个 `await` 处。一个重新拉起之后先异步加载一堆状态、然后才去重建定位管理器的 App,完全可能在中途被冻住,而一个在重新武装之前就被挂起的进程,从此再也不会听到 GPS 的任何消息。它看上去在运行。它什么都没收到。
Roam 的重新武装被刻意写成同步的,原因正是这个:重建定位管理器和重新启动位置更新,都发生在第一个 await 之前,这让那个错误从「不推荐犯」变成了「根本没法犯」。它同时被写成可以安全地调用两次,因为一次后台拉起和一次前台启动撞在一起的时候,干的就是这件事。
有一点值得说清楚:「始终允许」是一道保险,不是一个必要条件。用「使用期间」也完全能记录,而且 App 会在权限弹窗里把这句话说出来,而不是暗示你非得交出那个更强的权限不可。
而且它是保险,不是保证,这个区别值得较真。Roam 自己的配置里就把这个假设写成了一句话——它可能永远不会被重新拉起来,而耐久性是它唯一给出的承诺。iOS 里没有任何东西规定系统必须把一个被终止的 App 拉回来。在被杀掉和被重新拉起之间发生的事情,没有任何人记录过,之后也没有任何 API 能把它交给你,所以你拿回来的是被杀掉之前的那段路,加上重新拉起之后的那段路,中间老老实实留着一个洞。任何把这件事描述成「无缝续接」的 App,都在描述一件平台并不提供的事情。
骑车的人能做点什么
- 只要要记录的路程比较长,就把定位权限给成「始终允许」。这是「被终止一次什么都不损失」和「被终止一次损失掉后面一整趟」之间的区别。
- 能不开低电量模式就别开。它会让后台工作更容易被削减。
- 不要在记录途中从多任务界面把 App 划掉。强制退出等于告诉 iOS 你是认真的,之后不会有任何后台拉起。
- 不要同时跑两个记录器。两个 App 一起把 GPS 拽着不放,耗电和内存压力都会翻倍,而内存压力正是其中一个被杀掉的原因。
- 每次 iOS 升级之后检查一遍权限。升级偶尔会把「始终允许」不声不响地降回「使用期间」。
怎么判断你的记录器安不安全
这件事你十分钟就能自己测,不需要任何特殊工具。开始记录,走上五分钟,然后从多任务界面把 App 强制退出,再打开它。如果那五分钟还在,说明这个 App 是边走边写的。如果这趟是空的,或者被截断了,说明它把你的数据攥在内存里,而你现在也清楚地知道,一趟长途你可能会损失掉多少。
一台记录器还应该尽可能少地声明能力。每多一项后台模式,就多一个它可能因为滥用而被终止的理由,也多一个 App Store 审核员会追问的点;Roam 只声明了一项——定位——而且仓库里有一道构建闸门,这份清单只要多出一条,构建就失败。
常见问题
为什么我的 iPhone 在后台停止记录了?
几乎总是因为 iOS 在内存压力下把 App 挂起或者终止了,或者因为它被从多任务界面强制退出了。定位后台模式能让一台记录器比普通 App 多活很久,但它不是绝对的保证,这也正是为什么「App 拿到每一个定位点之后做了什么」比「它有没有活着」更要紧。
记录一趟路,一定要给「始终允许」的定位权限吗?
不用。「使用期间」就足以在熄屏状态下记录,因为定位后台模式会在更新持续到达的时候让 App 保持运行。「始终允许」多出来的,是让 iOS 在 App 被终止之后有可能把它在后台重新拉起来的能力,所以对长途来说它是一道保险,而不是一个必要条件。
后台 GPS 费电吗?
它确实要用掉实打实的电——接收机在持续工作,App 每收到一个定位点就要醒一次。一台现代手机跑几个小时毫无压力;真正要考虑这件事的是一整天的活动,而那也正是「边走边写」的 App 决定你到底是丢掉最后几分钟、还是丢掉一整天的地方。
我怎么测出自己的记录器会不会丢数据?
开始记录,走五分钟,从多任务界面把 App 强制退出,再打开它。如果那五分钟还在,说明它边走边写。如果不在,说明它一直把你这趟路攥在内存里。
一个不会把这趟弄丢的记录 App
Roam 每收到一个定位点就立刻写进手机,和当前的累计数字在同一个事务里落盘;信号断掉的那一段标成断的,不拿一条直线糊过去;爬升读的是气压计,不是 GPS 高度。没有账号,不用登录,不同步。完整记录一趟、看它、回放它,都是免费的。