【经验】iOS HealthKit读取运动步数的问题记录
一、前言
最近公司的项目需要做一个读取用户健康数据中运动步数的功能,过程中遇到一个获取到的步数始终不准确的问题,经过一番折腾问题总算是解决了,将问题总结记录一下 备忘,也为后来遇到此坑的小伙伴提供借鉴。
二、问题描述
使用HealthKit读取到的用户健康数据上的步数与系统自带的健康App以及微信运动中的步数始终不一致,但该问题只有部分用户存在,其他大部分用户的步数是没问题的,问题用户数据差了几千步,一般差个10来步可以理解,差几千步肯定就不正常了。
页面展示效果图三、问题分析
1、我项目中的处理方案是先访问健康数据,如果用户未授权健康数据再读取iPhone协处理的步数。
2、在健康数据与协处理数据都授权的情况下项目中并没有将两者的数据相加。
3、iPhone协处理器的步数与健康数据中的步数是有差异的,一般后者的数据比前者多。
4、微信等其他有步数显示的App获取到步数与健康数据是一致的,那说明并不是同步健康数据的服务器有问题。
5、部分有问题的设备健康数据中的步数除了本身iPhone设备的运动数据外还有iWatch的运动步数,其他没问题的设备中没有iWatch的运动步数。
那么问题的症结算是找到了,问题设备中我们项目显示的数据刚好是iPhone的步数加上iWatch的步数。
四、代码分析
获取健康数据的问题代码如下:
//获取步数
- (void)getStepCount:(void(^)(double stepCount, NSError *error))completion
{
HKQuantityType *stepType = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
NSSortDescriptor *timeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:HKSampleSortIdentifierEndDate ascending:NO];
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:stepType predicate:[HealthKitManage predicateForSamplesToday] limit:HKObjectQueryNoLimit sortDescriptors:@[timeSortDescriptor] resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if(error)
{
completion(0,error);
}
else
{
double totleSteps = 0;
for(HKQuantitySample *quantitySample in results)
{
HKQuantity *quantity = quantitySample.quantity;
HKUnit *heightUnit = [HKUnit countUnit];
double usersHeight = [quantity doubleValueForUnit:heightUnit];
totleSteps += usersHeight; //问题在此
}
NSLog(@"当天行走步数 = %lf",totleSteps);
completion(totleSteps,error);
}
}];
[self.healthStore executeQuery:query];
}
通过分析代码发现在获取健康数据的for 循环中有“+=”的操作,那问题肯定是出在这里了,此处将问题设备中的所有步数都加起来了,显然这种处理方式是有问题的。
既然这样不行那我就过滤掉iWatch的数据只读取iPhone设备的数据总可以吧,修改后的代码如下:
double totleSteps = 0;
for(HKQuantitySample *quantitySample in results)
{
// 过滤掉其它应用写入的健康数据
if ([source.name isEqualToString:[UIDevice currentDevice].name]) {
HKQuantity *quantity = quantitySample.quantity;
HKUnit *heightUnit = [HKUnit countUnit];
double usersHeight = [quantity doubleValueForUnit:heightUnit];
totleSteps += usersHeight; //问题在此
}
NSLog(@"当天行走步数 = %lf",totleSteps);
completion(totleSteps,error);
}
这样处理后获取步数依旧准确,仔细分析下这样的做法显然也是不符合逻辑的,健康App中显示的步数肯定是取的iPhone与iWatch步数的总和的,如果同一时间段iPhone与iWatch都有走步的话,那么取的步数较高的那一组设备数据。
那有没办法直接取到健康App显示的那个总步数呢? 即健康数据步数归总后的那组数据。
五、解决方案
经过分析HealthKit 处理查询健康数据的类发现,这个想法是可以得到实现的(否则微信等App怎么做到和健康数据保持一致的)。
HealthKit提供的几种健康数据查询方法类如下:
健康数据查询类型HKHealthStore —— 直接查询的类
HKSampleQuery —— 样本查询的类:查询某个样本(运动,能量...)的数据
HKObserverQuery —— 观察者查询的类:数据改变时发送通知(可以后台)
HKAnchoredObjectQuery —— 锚定对象查询的类:数据插入后返回新插入的数据
HKStatisticsQuery —— 统计查询的类:返回样本的总和/最大值/最小值...
HKStatisticsCollectionQuery —— 统计集合查询的类:返回时间段内样本的数据
HKCorrelation —— 相关性查询的类:查询样本相关(使用少)
HKSourceQuery —— 来源查询的类:查询数据来源
显然我们只要使用HKStatisticsQuery即可实现获取总的步数的想法,经过一番修改后问题终于完美解决,获取到步数与健康App以及微信运动保持了一致。
修正后的完整代码如下:
#import "HealthKitManager.h"
#import <UIKit/UIDevice.h>
#import <HealthKit/HealthKit.h>
#import <CoreMotion/CoreMotion.h>
#define IOS8 ([UIDevice currentDevice].systemVersion.floatValue >= 8.0f)
@interface HealthKitManager ()
/// 健康数据查询类
@property (nonatomic, strong) HKHealthStore *healthStore;
/// 协处理器类
@property (nonatomic, strong) CMPedometer *pedometer;
@end
@implementation HealthKitManager
///初始化单例对象
static HealthKitManager *_healthManager;
+ (instancetype)shareInstance {
if (!_healthManager) {
_healthManager = [[HealthKitManager alloc]init];
}
return _healthManager;
}
/// 应用授权检测
- (void)authorizateHealthKit:(void (^)(BOOL success, NSError *error))resultBlock {
if(IOS8)
{
if ([HKHealthStore isHealthDataAvailable]) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSSet *readObjectTypes = [NSSet setWithObjects:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount], nil];
[self.healthStore requestAuthorizationToShareTypes:nil readTypes:readObjectTypes completion:^(BOOL success, NSError * _Nullable error) {
if (resultBlock) {
resultBlock(success,error);
}
}];
});
}
} else {
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"iOS 系统低于8.0不能获取健康数据,请升级系统" forKey:NSLocalizedDescriptionKey];
NSError *aError = [NSError errorWithDomain:@"pingan.com.cn" code:0 userInfo:userInfo];
resultBlock(NO,aError);
}
}
/// 获取当天健康数据(步数)
- (void)getStepCount:(void (^)(double stepCount, NSError *error))queryResultBlock {
HKQuantityType *quantityType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
HKStatisticsQuery *query = [[HKStatisticsQuery alloc]initWithQuantityType:quantityType quantitySamplePredicate:[self predicateForSamplesToday] options:HKStatisticsOptionCumulativeSum completionHandler:^(HKStatisticsQuery * _Nonnull query, HKStatistics * _Nullable result, NSError * _Nullable error) {
if (error) {
[self getCMPStepCount: queryResultBlock];
} else {
double stepCount = [result.sumQuantity doubleValueForUnit:[HKUnit countUnit]];
NSLog(@"当天行走步数 = %lf",stepCount);
if(stepCount > 0){
if (queryResultBlock) {
queryResultBlock(stepCount,nil);
}
} else {
[self getCMPStepCount: queryResultBlock];
}
}
}];
[self.healthStore executeQuery:query];
}
/**
当天时间段
@return 时间段
*/
- (NSPredicate *)predicateForSamplesToday {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *now = [NSDate date];
NSDateComponents *components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:now];
[components setHour:0];
[components setMinute:0];
[components setSecond: 0];
NSDate *startDate = [calendar dateFromComponents:components];
NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
NSPredicate *predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionNone];
return predicate;
}
#pragma mark - 获取协处理器步数
- (void)getCMPStepCount:(void(^)(double stepCount, NSError *error))completion
{
if ([CMPedometer isStepCountingAvailable] && [CMPedometer isDistanceAvailable]) {
if (!_pedometer) {
_pedometer = [[CMPedometer alloc]init];
}
NSDate *thatDay = [NSDate date];
NSDate *from = [DateHelper startDayTime:thatDay];
[_pedometer startPedometerUpdatesFromDate:from withHandler:^(CMPedometerData * _Nullable pedometerData, NSError * _Nullable error) {
if (error) {
if(completion) completion(0 ,error);
[self goAppRunSettingPage];
} else {
double stepCount = [pedometerData.numberOfSteps doubleValue];
if(completion)
completion(stepCount ,error);
}
[_pedometer stopPedometerUpdates];
}];
}
}
/**
跳转App运动与健康设置页面
*/
- (void)goAppRunSettingPage {
if(![self valiantPromp]) return;
NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
NSString *msgStr = [NSString stringWithFormat:@"请在【设置->%@->%@】下允许访问权限",appName,@"运动与健身"];
dispatch_async(dispatch_get_main_queue(), ^{
[[PAAlertViewManager shareAlertView]showAlertViewWithTitle:@"使用提示" andMsg:msgStr cancelButonTitle:@"取消" otherButtonTitle:@"设置" clickAction:^(NSInteger index) {
if (index == 1) {
if (IOS8) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}
}
}];
});
}
#pragma mark - getter
- (HKHealthStore *)healthStore {
if (!_healthStore) {
_healthStore = [[HKHealthStore alloc] init];
}
return _healthStore;
}
@end
六、问题总结
1、遇到问题首先要理清思路,一步步分析查找问题的根源。
2、对于网上给出的解决方案代码不能只顾一时爽全盘抄写,要做具体分析(之前出问题的代码即是抄写网上的,目前网上大部分获取健康步数的代码写法都是我问题代码那样的)。
3、要多去学习和了解HealthKit等我们用到的系统框架源码,熟悉底层逻辑底层处理方法。
七、iPhone协处理器说明
文中有提到iPhone协处理器,可能大部分人不了解这个东西是干嘛的,这里做个简单介绍。
目前iPhone设备中一般用的M8协处理器,它的作用是持续测量来自加速感应器、指南针、陀螺仪和全新气压计的数据,为A8芯片分担更多的工作量,从而提升了工作效能。不仅如此,这些传感器现在还具备更多功能,比如可以测量行走的步数、距离和海拔变化等。
参考资料来源:
1、iOS 8 HealthKit 介绍
2、手机上的协处理器有什么作用_苹果协处理器是干什么的