一个让我纠结很久的问题
跑步 App 告诉我今天跑了 5 公里,运动手表告诉我消耗了 300 大卡。但我更想知道的是:我在这座城市住了三年,到底走过了百分之几的地方?
轨迹线能告诉你去过哪条路,但它没法回答"覆盖了多少"这个问题。我想要一个面积数字。
所以我做了一个叫「雁过留痕」的 iOS App,核心就干一件事:把你走过的 GPS 轨迹转换成探索面积(km²),告诉你这片土地你到底踩过多少。
怎么把一条线变成一个面
GPS 给的是一串经纬度坐标点,连起来是一条线。但我需要面积。
方案是把地图切成 25m × 25m 的网格。GPS 点落入哪个格子,那个格子就被点亮。探索面积 = 点亮的格子总数 × 单格面积。
25m 大概是一个篮球场的长边。这个精度是试出来的——50m 太粗,城市里走完一整条街可能只点亮两三个格子,体验很糊;10m 太细,GPS 在高楼之间的漂移会导致你坐在办公室面积都在涨,数据失去意义。25m 刚好,步行能明显看到新区域被点亮,静止时又不会乱飘。
网格索引的实现就是经纬度到整数坐标的映射:
// 将经纬度映射到 25m 网格索引
let metersPerDegLat = 111_320.0
let metersPerDegLon = 111_320.0 * cos(latitude * .pi / 180)
let gridX = Int(floor(longitude * metersPerDegLon / 25.0))
let gridY = Int(floor(latitude * metersPerDegLat / 25.0))
每个 (gridX, gridY) 对就是格子的唯一标识,扔进 Set 去重,最后 count * 0.000625(25×25=625m²)得到探索面积。简单粗暴,但好用。
行政区匹配:比想象中麻烦得多
"你解锁了哪些省份"——听起来就是判断点在哪个多边形里,对吧?实际做起来有三个坑。
坐标系偏移。 中国地图用 GCJ-02(火星坐标),CoreLocation 返回的是 WGS-84,两者差几十到几百米。沿海城市和省界附近这个偏移很致命,深圳的点可能被判到香港去。App 里内置了坐标转换,确保和行政区边界数据对齐。
边界数据体积。 中国省市两级行政区的 GeoJSON,精度稍高就几十 MB。我做了一轮简化,省级保留足够辨识度,市级适当降低,区县级直接放弃了(数据量扛不住)。最终压到大概 3MB。
判定性能。 每来一个 GPS 点就遍历所有省份做 ray-casting,太慢。我的做法是先用 bounding box 粗筛掉明显不相关的省份,再对候选区域精确判定。加上缓存最近命中的省份——下一个点大概率还在同一个省——命中缓存直接跳过。优化后后台运行基本感受不到开销。
成就系统怎么定义
面积算出来之后得让它有意义。我做了一套徽章系统,分了几条线:exploration(探索类)、consistency(坚持类)、china(行政区类)、world(全球类)。
BadgeMetrics 是成就判定的核心输入,把用户的统计数据打包在一起:
struct BadgeMetrics {
let totalDistanceKilometers: Double
let recordedDays: Int
let currentStreakDays: Int
let chinaProvinceCount: Int
let chinaAreaKm2: Double
let cityUnlockCount: Int
let globalAreaKm2: Double
// ... 十几个字段
}
每个徽章拿 metrics 里的字段做判定,比如「解锁 5 个省份」就是 chinaProvinceCount >= 5。同一维度还有多级递进,探索面积从 1km² 到 10km² 到 100km²。
定阈值花了很多时间。我自己用了两个月,记录日常通勤和周末出行的数据,才摸清普通用户的面积增长曲线。太容易没意思,太难劝退人。
轨迹分段也有讲究。两个 GPS 点间隔超过 10 分钟就切成两段,单段少于 8 个点直接过滤——8 个点、每个间隔约 15 米,意味着至少走了 100 多米。低于这个的多半是手机放桌上 GPS 飘出来的垃圾数据。
后台定位的电量取舍
做持续定位的 App,电量永远绕不开。
CLLocationManager 的 allowsBackgroundLocationUpdates 开了之后耗电很明显。我试了几种策略:
- 高精度持续定位:轨迹漂亮,但一天吃 15%-20% 电量,不可接受。
- significantLocationChange:省电,但城市里走几条街可能只给一两个点,面积算不出来。
- 折中:
desiredAccuracy = kCLLocationAccuracyNearestTenMeters+distanceFilter = 15。静止时不上报,走动时 15 米一个点,25m 网格够用。
我在 iPhone 15、iOS 17 上测的数据:日常通勤后台挂着大概 8-10 小时,耗电 5%-8%(蓝牙关闭、Wi-Fi 开启)。能接受,但也不算少。
回头看的一些想法
App Store 评分目前 5 分,评论数很少,下载量也很低。
说实话,我花了三周反复调网格精度,花了两周处理行政区边界数据,但花在想分发渠道上的时间基本为零。现在回头看,这个比例完全反了。
有几个决策我觉得做对了:25m 网格在 citywalk 场景下体验确实不错;徽章分多条线,出差多的人省份解锁快,跑步党连续打卡数高,不同用户能找到不同的成就感。
有一个地方做得不够好:面积数字缺乏参照。用户看到 2.3 km²,不知道算多还是少。应该换算成"xx 个足球场"之类的,或者给个城市排名。但排名涉及用户数据比较,隐私和服务端成本都是问题,暂时搁置了。
想讨论一个具体问题
行政区边界匹配我现在是 bounding box 粗筛 + ray-casting 精判,能用但总觉得不够好。有没有人试过用 S2 Geometry 或者 Uber 的 H3 来做空间索引?理论上六边形网格做区域归属判定应该更高效,但我没在 iOS 端实际跑过,不确定引入这些库的包体积和性能表现怎么样。如果有踩过坑的,评论区聊聊。