返回首页

iOS 中的 GPS 跟踪器:Kalman 滤波器和 geohash 实现

关于在 SwiftUI 上使用 Kalman 滤波器和 geohash 实现后台 GPS 跟踪器的文章。数据处理、路线可视化和通过 App Store Review 的技术解决方案详细分解。

iOS 中的后台 GPS 跟踪器:完整技术指南
Advertisement 728x90

iOS 后台 GPS 追踪器创建指南:技术解决方案与常见陷阱

2024 年 Google Timeline 关闭后,开发者们失去了一个可靠的路线分析工具。与其依赖云端方案,我们现在转向构建本地应用,来精确追踪油耗。本文将逐步介绍在 SwiftUI 中实现 GPS 追踪器,包括处理噪声数据、后台优化,以及通过 App Store 审核的经验。

CoreLocation 中后台追踪基础

关键挑战是在应用进入后台时获取坐标,同时不耗尽电池。所需配置如下:

  • 在 CLLocationManager 中启用 allowsBackgroundLocationUpdates
  • 添加包含位置类型的能力(Background Modes entitlement)
  • 在 Info.plist 中通过 NSLocationAlwaysAndWhenInUseUsageDescription 提供使用理由

初始实现记录每个点,导致数据量过大(每次行程数千点)。通过组合以下参数进行优化:

Google AdInline article slot
  • desiredAccuracy = kCLLocationAccuracyBest — 优先使用 GPS 而非 Wi-Fi/蜂窝塔
  • distanceFilter = 10 — 忽略小于 10 米的移动
  • 过滤 horizontalAccuracy > 50 — 剔除噪声数据

这些设置将每次行程数据量减少到 200–500 点。60+ 条路线后,数据库占用约 15 MB,对于 CoreData 来说完全可接受。

无 IMU 的卡尔曼滤波:高速公路追踪的数学方法

在 GPS 干扰路段(如 Krasnodar–Gelendzhik),经典的传感器方案不可用——后台下 CMMotionManager 被阻塞。我们不得不调整卡尔曼滤波,仅使用坐标工作:

func processLocation(_ location: CLLocation) -> CLLocation {
    guard let previous = lastFilteredLocation else {
        lastFilteredLocation = location
        return location
    }

    let dt = location.timestamp.timeIntervalSince(previous.timestamp)
    let processNoise = dt * speedVariance
    predictedVariance += processNoise

    let measurementVariance = location.horizontalAccuracy * location.horizontalAccuracy
    let kalmanGain = predictedVariance / (predictedVariance + measurementVariance)

    let lat = previous.coordinate.latitude
        + kalmanGain * (location.coordinate.latitude - previous.coordinate.latitude)
    let lon = previous.coordinate.longitude
        + kalmanGain * (location.coordinate.longitude - previous.coordinate.longitude)

    predictedVariance = (1 - kalmanGain) * predictedVariance

    return CLLocation(
        coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon),
        altitude: location.altitude,
        horizontalAccuracy: sqrt(predictedVariance),
        verticalAccuracy: location.verticalAccuracy,
        timestamp: location.timestamp
    )
}

该算法动态调整对测量的信任度:当 horizontalAccuracy 高(GPS 信号差)时,kalmanGain 降低,系统更多依赖先前数据。对于超过 10 秒的信号丢失,我们使用三次样条进行后处理。

Google AdInline article slot

SwiftUI 中按速度可视化路线

iOS 17+ 提供 MapPolyline,但每个对象仅支持一种颜色。为了实现动态速度着色,我们需要:

  • 将轨迹拆分为速度范围均匀的片段
  • 将每个片段渲染为独立的 MapPolyline
  • 相邻片段重叠一个点
struct SpeedSegment {
    let coordinates: [CLLocationCoordinate2D]
    let color: Color
}

func buildSegments(from points: [LocationPoint]) -> [SpeedSegment] {
    var segments: [SpeedSegment] = []
    var currentCoords: [CLLocationCoordinate2D] = []
    var currentColor: Color = .green

    for point in points {
        let color = speedColor(for: point.speed)
        if color != currentColor && !currentCoords.isEmpty {
            segments.append(SpeedSegment(
                coordinates: currentCoords,
                color: currentColor
            ))
            currentCoords = [currentCoords.last!]
        }
        currentCoords.append(point.coordinate)
        currentColor = color
    }

    if !currentCoords.isEmpty {
        segments.append(SpeedSegment(
            coordinates: currentCoords,
            color: currentColor
        ))
    }
    return segments
}

系统使用 4 种颜色范围:

  • 绿色: < 50 km/h(城市)
  • 黄色:50–90 km/h(高速公路)
  • 橙色:90–110 km/h(快速路)
  • 红色:> 110 km/h

典型路线生成 20–50 条折线。500+ 点时,老设备可能有轻微延迟,但大多数场景性能良好。

Google AdInline article slot

Geohash 战争迷雾:地理空间优化

为了实现“战争迷雾”效果(隐藏未走过的路段),我们选择了 geohash 而非 quadtree 或直接点存储。优势:

  • 通过前缀匹配快速查找
  • 简单序列化到 CoreData
  • 通过精度参数灵活调整

| 精度 | 单元格大小 | 应用场景 |

|------|------------------|--------------|

| 5 | ~4.9 × 4.9 km | 国家概览 |

| 6 | ~1.2 × 0.6 km | 城市概览 |

| 7 | ~150 × 150 m | 详细视图 |

实现包括:

  • 根据 MKCoordinateSpan 动态切换精度
  • 使用 Set<String> 检查可见单元格
  • 0.7s easeOut 透明度动画消散

当前限制:矩形单元格边界。下步计划:边缘模糊的渐变渲染,更自然的外观。

关键要点

  • 输入过滤至关重要:无 distanceFilter 和 horizontalAccuracy 检查,数据库体积膨胀 5–10 倍
  • Live Activity 限制严格:iOS 最多允许 1 Hz 更新
  • Geohash 优于 quadtree:移动场景下序列化更简单
  • 无 IMU 的卡尔曼滤波 可处理长达 10 秒的信号丢失
  • MapPolyline 需要分段 以实现动态着色——一条路线 = 20–50 个对象

Live Activity:对抗人为错误

问题:用户忘记停止记录,导致电池耗尽。解决方案:通过 ActivityKit 在锁屏显示指示器:

// 启动活动
let activity = try Activity.request(
    attributes: TripActivityAttributes(carName: "Polo Sedan"),
    content: .init(state: initialState)
)

// 每秒更新
await activity.update(.init(state: updatedState))

关键:更新频率上限约 1 Hz。更频繁发送会被系统忽略。这对速度表足够,但高频指标需数据聚合。

CoreData 中的数据存储

数据库模式针对路线和点快速访问进行了优化:

TripEntity (1) ──── (*) LocationPointEntity
    ├── startDate
    ├── distance
    └── fuelCost

LocationPointEntity
    ├── latitude
    ├── longitude
    └── speed

ExploredCellEntity
    └── geohash (unique)

实现细节:

  • geohash 单元格使用 NSMergeByPropertyObjectTrumpMergePolicy
  • 通过时间戳自动删除旧记录
  • tripID 和时间戳索引以加速查询

每条路线 500+ 点时,最小化 fetch 请求至关重要。所有 LocationPointEntity 操作在单一托管对象上下文中运行,并使用批量更新。

— Editorial Team

Advertisement 728x90

继续阅读