使用 HealthKit 同步健康数据
Synchronize health data with HealthKit
2020年6月25日
一句话判断
如果你的健康应用在多设备同步上遇到过数据丢失或重复问题,这场 Session 讲解的 HealthKit 同步机制和冲突解决策略正是你需要的。
这场 Session 讲了什么
HealthKit 数据的跨设备同步是一个比看起来复杂得多的问题。用户可能在 Apple Watch 上记录了运动数据,同时在 iPhone 上手动输入了体重,这两份数据需要在 iCloud 上正确合并。Session 深入讲解了 HealthKit 的同步架构、数据冲突解决机制以及开发者需要遵循的最佳实践。
HealthKit 使用基于 UUID 的数据标识来追踪每条健康记录。每条 HKObject 都有唯一的 UUID,HealthKit 在同步时会通过这个 UUID 来识别和去重。如果同一条数据在多个设备上被创建(比如用户在 iPhone 和 Apple Watch 上同时记录了同一次跑步),HealthKit 会使用”最后写入者胜出”(last writer wins)策略来解决冲突。
Session 还介绍了 HKDevice 的概念——每条健康记录都关联了创建它的设备信息。这让你的应用可以区分”来自 Apple Watch 的心率数据”和”用户在 iPhone 上手动输入的数据”。在展示数据时,你应该清楚标注数据来源,帮助用户理解数据的可靠性。
值得深挖的点
数据同步的时机与策略
HealthKit 的同步不是实时的,而是依赖 iCloud 的同步周期。在正常情况下,数据会在几分钟内同步到所有设备。但在网络条件差或设备处于低电量模式时,同步可能延迟数小时。Session 建议不要依赖”即时同步”的假设来设计你的应用逻辑——本地数据始终是 source of truth,远端数据是延迟更新的补充。
批量数据导入的事务保证
如果你需要从其他健康平台迁移大量历史数据到 HealthKit,需要使用 HKCorrelation 和 HKWorkout 来保持数据的原子性。一个完整的运动记录包含心率序列、路线轨迹和汇总数据,这些必须作为整体写入,否则会变成碎片化的无效数据。
代码片段
import HealthKit
// 正确地写入健康数据并处理同步
class HealthDataWriter {
let healthStore = HKHealthStore()
// 保存运动记录(包含关联的样本数据)
func saveWorkout(startTime: Date, endTime: Date,
heartRates: [Double], distance: Double) {
// 创建心率样本序列
let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
let heartRateUnit = HKUnit.count().unitDivided(by: .minute())
let heartRateSamples = heartRates.enumerated().compactMap { index, bpm -> HKQuantitySample? in
let timeOffset = Double(index) * (endTime.timeIntervalSince(startTime) / Double(heartRates.count))
let date = startTime.addingTimeInterval(timeOffset)
return HKQuantitySample(
type: heartRateType,
quantity: HKQuantity(unit: heartRateUnit, doubleValue: bpm),
start: date,
end: date
)
}
// 创建距离样本
let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
let distanceSample = HKQuantitySample(
type: distanceType,
quantity: HKQuantity(unit: .meter(), doubleValue: distance),
start: startTime, end: endTime
)
// 创建完整的运动记录
let workout = HKWorkout(
activityType: .running,
start: startTime,
end: endTime,
duration: endTime.timeIntervalSince(startTime),
totalEnergyBurned: HKQuantity(unit: .kilocalorie(), doubleValue: distance * 0.06),
totalDistance: HKQuantity(unit: .meter(), doubleValue: distance),
metadata: [
HKMetadataKeyExternalUUID: UUID().uuidString, // 去重标识
HKMetadataKeyDeviceManufacturerName: "MyApp",
]
)
// 保存运动记录和关联样本
healthStore.save(workout) { success, error in
guard success else {
print("保存运动记录失败: \(error!)")
return
}
// 将心率样本关联到运动记录
self.healthStore.add(heartRateSamples, to: workout) { success, error in
if success {
print("运动数据保存成功,包含 \(heartRateSamples.count) 条心率记录")
}
}
}
}
}
// 查询数据时处理设备和来源信息
class HealthDataQuerier {
let healthStore = HKHealthStore()
func fetchStepsWithSourceInfo() {
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let query = HKSampleQuery(
sampleType: stepType,
predicate: nil,
limit: 50,
sortDescriptors: [NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)]
) { _, samples, error in
guard let samples = samples as? [HKQuantitySample] else { return }
for sample in samples {
let stepCount = sample.quantity.doubleValue(for: .count())
// 获取数据来源信息
let source = sample.sourceRevision.source.name
let device = sample.device?.name ?? "未知设备"
print("""
步数: \(Int(stepCount)) 步
来源: \(source)
设备: \(device)
时间: \(sample.startDate)
---
""")
}
}
healthStore.execute(query)
}
}
// 使用 metadata 的外部 UUID 防止重复写入
class DeduplicationManager {
let healthStore = HKHealthStore()
func saveStepCountIfNeeded(count: Double, date: Date, externalId: String) {
// 先检查是否已经保存过这条数据
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForObjects(
withMetadataKey: HKMetadataKeyExternalUUID,
operatorType: .equalTo,
value: externalId
)
let query = HKSampleQuery(
sampleType: stepType,
predicate: predicate,
limit: 1,
sortDescriptors: nil
) { [weak self] _, samples, _ in
// 如果已存在,跳过
if let _ = samples?.first {
print("数据已存在,跳过: \(externalId)")
return
}
// 不存在则保存
let sample = HKQuantitySample(
type: stepType,
quantity: HKQuantity(unit: .count(), doubleValue: count),
start: date,
end: date,
metadata: [
HKMetadataKeyExternalUUID: externalId,
]
)
self?.healthStore.save(sample) { success, error in
print(success ? "数据保存成功" : "保存失败: \(error!)")
}
}
healthStore.execute(query)
}
}
最佳实践
- 每条写入 HealthKit 的数据都附带一个外部 UUID(
HKMetadataKeyExternalUUID),用于去重检测 - 在 UI 中展示数据来源信息(Apple Watch、iPhone 手动输入、第三方设备),帮助用户理解数据可信度
- 批量导入历史数据时使用 background queue,不要阻塞主线程
- 保存运动记录时使用
HKWorkout类型而非独立的样本,确保数据的完整性和关联性 - 测试同步场景时使用两台真实的 Apple 设备,模拟网络不佳和离线情况
还有什么值得关注
- HealthKit 数据在 iCloud 中是端到端加密的,Apple 无法读取用户的健康数据
HKSourceRevision包含了数据来源的详细版本信息,可以用来过滤特定版本的应用产生的数据- Session 提到了 HealthKit 的新统计查询 API,可以更高效地计算日/周/月的汇总数据