认识 Core Location Monitor
Meet Core Location Monitor
2023年6月5日
一句话判断
Core Location 团队终于把位置监听从 delegate 回调地狱中解救出来了——CLMonitor 是一个 Swift Actor,用三行代码就能实现地理围栏和 Beacon 监听,再也不用手动管理 CLLocationManager 的各种回调状态。
这场 Session 讲了什么
Core Location Frameworks 团队的 Nivash 介绍了全新的 CLMonitor API。这是一个基于 Swift Actor 的位置监听框架,核心用法极其简洁:创建 Monitor、添加条件、await 事件。
CLMonitor 是顶层 Swift Actor,每个实例是一个监听网关。因为它是 Actor,所以天然具备线程安全特性,不需要开发者手动处理任务同步问题。创建 Monitor 时传入一个字母数字字符串作为名称——如果同名 Monitor 已存在就打开它,否则创建新的。同一名称同一时刻只能有一个实例。
支持两种条件类型:CircularGeographicCondition(圆形地理区域,类似原来的 CLCircularRegion)和 BeaconIdentityCondition(Beacon 身份条件,类似原来的 CLBeaconIdentityConstraint)。每种条件添加后会被 Core Location 持续监控,状态变化通过事件记录通知。
Session 特别详细地讲解了 Beacon 的三级监控策略:只用 UUID 监控所有站点、UUID + Major 监控特定站点、UUID + Major + Minor 监控特定区域的特定位置。
值得深挖的点
Beacon 条件的通配符设计:BeaconIdentityCondition 支持 UUID、UUID+Major、UUID+Major+Minor 三种粒度。以 Apple 食堂为例——所有食堂的 Beacon 共享同一 UUID,每个站点用不同的 Major 值,站点内不同餐区用不同的 Minor 值。这种层级设计让一套 Beacon 基础设施同时服务于粗粒度和细粒度的位置感知。
记录系统(Records):每个被添加的条件都有一个对应的 Record 对象,包含条件本身、当前状态(unknown/satisfied/unsatisfied)、上次满足的时间戳和上次事件的时间戳。可以通过 identifier 查询单条记录,也可以获取所有记录。应用生命周期内随时可以检查这些记录。
初始状态覆盖:添加条件时可以通过 assuming: 参数指定初始状态。比如你的应用已经通过其他方式知道用户不在 Apple Park,就可以设置初始状态为 unsatisfied。不过不用担心——如果假设错了,Core Location 会在确定真实状态后纠正它。
代码片段
// 创建 Monitor 实例
let monitor = await CLMonitor("MyLocationMonitor")
// 添加圆形地理区域条件 - 监控用户是否到达公司
let workCenter = CLLocationCoordinate2D(latitude: 37.3349, longitude: -122.0090)
let workCondition = CircularGeographicCondition(center: workCenter, radius: 100)
await monitor.add(workCondition, identifier: "Work", assuming: .unsatisfied)
// 添加 Beacon 条件 - 监控所有 Apple 食堂
let beaconUUID = UUID(uuidString: "12345678-1234-1234-1234-123456789ABC")!
let allCafeterias = BeaconIdentityCondition(uuid: beaconUUID)
await monitor.add(allCafeterias, identifier: "AllCafeterias")
// 监控特定站点
let parkSite = BeaconIdentityCondition(uuid: beaconUUID, major: 1)
await monitor.add(parkSite, identifier: "ApplePark")
// 监听事件变化 - 在后台持续监控
func startMonitoring() async {
let monitor = await CLMonitor("MyLocationMonitor")
let events = await monitor.events
for await event in events {
// event 包含 identifier 和 state
switch event.identifier {
case "Work":
handleWorkStateChange(event.state)
case "AllCafeterias":
handleCafeteriaNearby(event.state)
default:
break
}
}
}
// 查询当前所有记录的状态
func checkAllRecords() async {
let monitor = await CLMonitor("MyLocationMonitor")
let records = await monitor.records
for (identifier, record) in records {
print("\(identifier): \(record.state)")
}
}
最佳实践
- 用有意义的 identifier:identifier 不仅是关联条件的键,也是后续查询记录和移除条件的依据。“Work”、“Home” 这样的命名比 “condition1” 好得多。
- 合理使用 assuming 参数:如果你的应用已经有位置上下文信息,用它来设置初始状态可以减少 Core Location 的判断时间。但不要滥用——错误的初始假设不会导致问题,但会增加状态切换的延迟。
- 移除不再需要的条件:用
remove(identifier:)清理不再需要的监听条件,减少不必要的系统资源消耗。 - 注意 Monitor 的单例限制:同一名称同一时刻只能有一个打开的 Monitor 实例。不要在多个地方创建同名 Monitor。
- 权限仍然是前提:CLMonitor 不改变位置权限的请求方式,仍然需要在使用前获取适当的位置权限。
还有什么值得关注
- CLMonitor 的 events 是一个 AsyncSequence,可以 for-await 循环监听——这是 Apple 在系统框架中全面推进 Swift Concurrency 的又一个例证。
- Beacon 的三级监控策略(UUID only / UUID+Major / UUID+Major+Minor)是一个很好的架构范式,值得在设计多层级位置感知系统时参考。
- Record 中包含 lastSatisfiedAt 和 lastEventAt 时间戳,这对分析用户的行为模式(比如用户通常什么时候到公司)很有用。
- Session 强调 CLMonitor 是 Actor 而非 Class——这意味着所有属性的访问都需要 await,但同时也意味着你不需要担心并发安全问题。
- 旧的 CLCircularRegion 和 CLBeaconRegion API 仍然可用,但新项目建议直接使用 CLMonitor。