【经验】iOS HealthKit读取运动步数的问题记录

1.前言

最近公司的项目需要做一个读取用户健康数据中运动步数的功能,过程中遇到一个获取到的步数始终不准确的问题,经过一番折腾问题总算是解决了,将问题总结记录一下 备忘,也为后来遇到此坑的小伙伴提供借鉴。

2.问题描述

使用HealthKit读取到的用户健康数据上的步数与系统自带的健康App以及微信运动中的步数始终不一致,但该问题只有部分用户存在,其他大部分用户的步数是没问题的,问题用户数据差了几千步,一般差个10来步可以理解,差几千步肯定就不正常了。

页面展示效果图
3.问题分析

1、我项目中的处理方案是先访问健康数据,如果用户未授权健康数据再读取iPhone协处理器的步数。

2、在健康数据与协处理器数据都授权的情况下项目中并没有将两者的数据相加。

3、iPhone协处理器的步数与健康数据中的步数是有差异的,一般后者的数据比前者多。

4、微信等其他有步数显示的App获取到步数与健康数据是一致的,那说明并不是同步健康数据的服务器有问题。

5、部分有问题的设备健康数据中的步数除了本身iPhone设备的运动数据外还有iWatch的运动步数,其他没问题的设备中没有iWatch的运动步数。

那么问题的症结算是找到了,问题设备中我们项目显示的数据刚好是iPhone的步数加上iWatch的步数。

4.代码分析

获取健康数据的问题代码如下:

//获取步数
- (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显示的那个总步数呢? 即健康数据步数归总后的那组数据。

5.解决方案

经过分析HealthKit 处理查询健康数据的类发现,这个想法是可以得到实现的(否则微信等App怎么做到和健康数据保持一致的)。

HealthKit提供的几种健康数据查询方法类如下:

健康数据查询类型

1、HKHealthStore
HealthKit框架的核心类,主要对数据进行操作。

2、HKSampleQuery
样本查询的类:查询某个样本(运动,能量...)的数据。

3、HKObserverQuery
观察者查询的类:数据改变时发送通知(可以后台)。

4、HKAnchoredObjectQuery
锚定对象查询的类:数据插入后返回新插入的数据。

5、HKStatisticsQuery
统计查询的类:返回样本的总和/最大值/最小值...

6、HKStatisticsCollectionQuery
统计集合查询的类:返回时间段内样本的数据。

7、HKCorrelation
相关性查询的类:查询样本相关(使用少)。

8、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 ()<UIAlertViewDelegate>

/// 健康数据查询类
@property (nonatomic, strong) HKHealthStore *healthStore;
/// 协处理器类
@property (nonatomic, strong) CMPedometer *pedometer;

@end

@implementation HealthKitManager

#pragma mark - 初始化单例对象
static HealthKitManager *_healthManager;
+ (instancetype)shareInstance {
   static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!_healthManager) {
            _healthManager = [[PAHealthManager alloc]init];
        }
    });
    return _healthManager;
} 

#pragma mark - 应用授权检查
- (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:@"xxxx.com.cn" code:0 userInfo:userInfo];
        resultBlock(NO,aError);
    }
    
}

#pragma mark - 获取当天健康数据(步数)
- (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];
} 

#pragma mark - 构造当天时间段查询参数 
- (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];
        }
        NSCalendar *calendar = [NSCalendar currentCalendar];
        NSDate *now = [NSDate date];
        NSDateComponents *components = [calendar components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay fromDate:now];
        // 开始时间
        NSDate *startDate = [calendar dateFromComponents:components];
        // 结束时间
        NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
        [_pedometer queryPedometerDataFromDate:startDate toDate:endDate 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];
        }];
    }
}

#pragma mark - 跳转App运动与健康设置页面 
- (void)goAppRunSettingPage { 
    NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
    NSString *msgStr = [NSString stringWithFormat:@"请在【设置->%@->%@】下允许访问权限",appName,@"运动与健身"];
    dispatch_async(dispatch_get_main_queue(), ^{
    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"使用提示" message:msgStr delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"设置", nil];
    [alert show]; 
    });
}  

#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
     if(buttonIndex == 1) {
       if (IOS8) {
          [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
        }
     }
}

#pragma mark - getter
- (HKHealthStore *)healthStore {
    if (!_healthStore) {
        _healthStore = [[HKHealthStore alloc] init];
    }
    return _healthStore;
} 

@end

6.问题总结

1、遇到问题首先要理清思路,一步步分析查找问题的根源。

2、对于网上给出的解决方案代码不能只顾一时爽全盘抄写,要做具体分析(目前网上大部分获取健康步数的代码,在存在多设备上传健康数据情况下,统计是不准确的)。

3、要多去学习和了解HealthKit等我们用到的系统框架源码,熟悉底层逻辑底层处理方法。

7.iPhone协处理器说明

文中有提到iPhone协处理器,可能大部分人不了解这个东西是干嘛的,这里做个简单介绍。

目前iPhone设备中一般用的M8协处理器,它的作用是持续测量来自加速感应器、指南针、陀螺仪和全新气压计的数据,为A8芯片分担更多的工作量,从而提升了工作效能。不仅如此,这些传感器现在还具备更多功能,比如可以测量行走的步数、距离和海拔变化等。

参考资料来源:
1、iOS 8 HealthKit 介绍
2、手机上的协处理器有什么作用_苹果协处理器是干什么的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342