做了什么
我做了一个 iOS App 叫「雁过留痕」,核心玩法是把你走过的所有路转化成探索面积(km²),地图上像消除战争迷雾一样逐格点亮。底层逻辑是 25m × 25m 网格统计,GPS 轨迹经过的格子标记为已探索,累积计算总面积。
技术上最难啃的是后台 GPS 持续记录的功耗控制。这篇主要聊这个问题的演进过程,顺带说说行政区边界判定和成就系统架构踩过的坑。
后台定位:三版方案的演进
第一版:significantLocationChanges
想法很朴素——系统级显著位置变化回调,功耗几乎为零。问题是精度完全不够用,触发阈值大约 500m,在城市里走小巷子根本不会触发。记录出来的轨迹从一个路口直接跳到另一个路口,中间全丢了。25m 网格根本没法算,pass。
第二版:kCLAccuracyBest + 持续后台
切到高精度持续采样,每秒一个点,精度确实到了 5-10m 级别。代价是有用户反馈半天掉了 30% 电量。我打开 Instruments 的 Energy Log,Location 那栏全程红色,后台 CPU 唤醒频率太高。
第三版(当前):动态精度 + 运动状态判定
核心思路:移动时采样,静止时休眠。
// 静止判定:连续 8 个点位移总和 < 15m 则进入休眠
func evaluateMotionState(recentLocations: [CLLocation]) -> MotionState {
guard recentLocations.count >= 8 else { return .moving }
let totalDisplacement = zip(recentLocations, recentLocations.dropFirst())
.reduce(0.0) { $0 + $1.0.distance(from: $1.1) }
if totalDisplacement < 15.0 {
return .stationary // 降到 kCLAccuracyHundredMeters
}
return .moving // 维持高精度
}
休眠期间降到 kCLAccuracyHundredMeters,检测到移动再切回高精度。步行状态下采样间隔拉到 5-8 秒,骑行状态缩到 3 秒。
实测数据:全天开启(早 8 点到晚 10 点),后台 GPS 模块总 CPU 时间约 4 分钟,电量消耗 8-12%(iPhone 14 Pro)。相比第二版降了 60% 以上。
行政区边界判定的三个坑
省市解锁功能要做 point-in-polygon 判断,这里踩了几个实实在在的坑:
1. 坐标系偏移
iOS 的 CLLocation 给的是 WGS-84,中国地图服务用 GCJ-02。省界附近这个偏移(100-600m)会直接判错省份。解法是内置 WGS84→GCJ02 转换,判定前统一坐标系。
2. 边界数据体积
完整的中国省市区三级边界 GeoJSON 有 40 多 MB,启动时全加载不现实。做法是按省拆分文件,根据用户粗略位置按需加载当前省份 + 相邻省份数据。首次判定延迟从 2 秒降到 200ms。
3. 飞地问题
部分地级市辖区不连续,用外接矩形做预筛选会漏判。给每个行政区存 MultiPolygon,预筛选用所有子区域外接矩形的并集。
成就系统:重构三次后的架构
做了四条徽章路线(exploration、consistency、china、world),每条下面多个系列分等级。架构演进了三版,最终方案是单次计算 + 纯函数判定:
struct BadgeMetrics {
let totalDistanceKm: Double
let recordedDays: Int
let currentStreakDays: Int
let nightSegmentCount: Int
let chinaProvinceCount: Int
let chinaAreaKm2: Double
static func build(
stats: TraceStats,
segments: [TraceSegment],
geo: GeographicProfile
) -> BadgeMetrics { /* 所有指标集中计算 */ }
}
所有原始指标在 BadgeMetrics.build() 里一次性算完,每个徽章的 requirement 只是对 metrics 做纯函数比较。加新徽章只需在 catalog 里加一条定义,如果指标已存在连 metrics 都不用动。
之前硬编码版本加一个「夜间探索者」徽章要改四个文件联动,现在两步搞定。
存储方案:SwiftData 扛住日均 3000+ 轨迹点
轨迹点量级:步行日约 2000-4000 点/天,骑行日 8000+。累积半年单用户 50-80 万条记录。
SwiftData 的查询模式比较固定(按时间段、按 segment 分组),实测 50 万条按日期范围查询在 iPhone 12 上耗时约 80ms。
两个关键优化:
- 分段归档:超过 90 天的轨迹点压缩成 polyline 编码存一条归档记录,原始点删除。地图展示解码 polyline 就够,面积统计从网格缓存读。
- 网格缓存:探索面积的网格标记单独一张表,新增轨迹点时增量更新,不需要从全量点重算。
总结几个经验
- 后台定位不要一刀切精度等级,结合运动状态动态切换是性价比最高的方案
- GeoJSON 大文件一定要按需加载,别信「现在手机内存够大」这种话
- 成就/徽章系统如果有扩展预期,越早抽象 metrics 层越好,硬编码到第三个徽章就会爆炸
- 轨迹类 App 的数据增长比你想象的快,归档策略要在早期就设计好
如果你也在做地理围栏、区域判定或者后台定位相关的需求,坐标转换和边界判定的部分实现我后续打算整理成 gist 放出来,有兴趣的话可以关注后续更新。也欢迎在评论区聊聊你们在后台定位上踩过的坑。