【經(jīng)驗】iOS HealthKit讀取運動步數(shù)的問題記錄

1.前言

最近公司的項目需要做一個讀取用戶健康數(shù)據(jù)中運動步數(shù)的功能,過程中遇到一個獲取到的步數(shù)始終不準確的問題洼怔,經(jīng)過一番折騰問題總算是解決了署惯,將問題總結記錄一下 備忘,也為后來遇到此坑的小伙伴提供借鑒镣隶。

2.問題描述

使用HealthKit讀取到的用戶健康數(shù)據(jù)上的步數(shù)與系統(tǒng)自帶的健康App以及微信運動中的步數(shù)始終不一致极谊,但該問題只有部分用戶存在,其他大部分用戶的步數(shù)是沒問題的安岂,問題用戶數(shù)據(jù)差了幾千步轻猖,一般差個10來步可以理解,差幾千步肯定就不正常了嗜闻。

頁面展示效果圖
3.問題分析

1蜕依、我項目中的處理方案是先訪問健康數(shù)據(jù)桅锄,如果用戶未授權健康數(shù)據(jù)再讀取iPhone協(xié)處理器的步數(shù)琉雳。

2样眠、在健康數(shù)據(jù)與協(xié)處理器數(shù)據(jù)都授權的情況下項目中并沒有將兩者的數(shù)據(jù)相加。

3翠肘、iPhone協(xié)處理器的步數(shù)與健康數(shù)據(jù)中的步數(shù)是有差異的檐束,一般后者的數(shù)據(jù)比前者多。

4束倍、微信等其他有步數(shù)顯示的App獲取到步數(shù)與健康數(shù)據(jù)是一致的被丧,那說明并不是同步健康數(shù)據(jù)的服務器有問題。

5绪妹、部分有問題的設備健康數(shù)據(jù)中的步數(shù)除了本身iPhone設備的運動數(shù)據(jù)外還有iWatch的運動步數(shù)甥桂,其他沒問題的設備中沒有iWatch的運動步數(shù)。

那么問題的癥結算是找到了邮旷,問題設備中我們項目顯示的數(shù)據(jù)剛好是iPhone的步數(shù)加上iWatch的步數(shù)。

4.代碼分析

獲取健康數(shù)據(jù)的問題代碼如下:

//獲取步數(shù)
- (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(@"當天行走步數(shù) = %lf",totleSteps);
                completion(totleSteps,error);
            }
    }];
    
    [self.healthStore executeQuery:query];
} 

通過分析代碼發(fā)現(xiàn)在獲取健康數(shù)據(jù)的for 循環(huán)中有“+=”的操作,那問題肯定是出在這里了夹孔,此處將問題設備中的所有步數(shù)都加起來了说搅,顯然這種處理方式是有問題的。

既然這樣不行那我就過濾掉iWatch的數(shù)據(jù)只讀取iPhone設備的數(shù)據(jù)總可以吧律歼,修改后的代碼如下:

double totleSteps = 0;
for(HKQuantitySample *quantitySample in results)
  {
      // 過濾掉其它應用寫入的健康數(shù)據(jù)
       if ([source.name isEqualToString:[UIDevice currentDevice].name]) {
         HKQuantity *quantity = quantitySample.quantity;
         HKUnit *heightUnit = [HKUnit countUnit];
         double usersHeight = [quantity doubleValueForUnit:heightUnit];
         totleSteps += usersHeight;  //問題在此
       }
      NSLog(@"當天行走步數(shù) = %lf",totleSteps);
      completion(totleSteps,error);
 } 

這樣處理后獲取步數(shù)依舊不準確民镜,仔細分析下這樣的做法顯然也是不符合邏輯的,健康App中顯示的步數(shù)肯定是取的iPhone與iWatch步數(shù)的總和的险毁,如果同一時間段iPhone與iWatch都有走步的話制圈,那么取的步數(shù)較高的那一組設備數(shù)據(jù)。

那有沒有辦法直接取到健康App顯示的那個總步數(shù)呢畔况? 即健康數(shù)據(jù)步數(shù)歸總后的那組數(shù)據(jù)离唐。

5.解決方案

經(jīng)過分析HealthKit 處理查詢健康數(shù)據(jù)的類發(fā)現(xiàn),這個想法是可以得到實現(xiàn)的(否則微信等App怎么做到和健康數(shù)據(jù)保持一致的)问窃。

HealthKit提供的幾種健康數(shù)據(jù)查詢方法類如下:

健康數(shù)據(jù)查詢類型

1亥鬓、HKHealthStore
HealthKit框架的核心類,主要對數(shù)據(jù)進行操作。

2域庇、HKSampleQuery
樣本查詢的類:查詢某個樣本(運動嵌戈,能量...)的數(shù)據(jù)。

3听皿、HKObserverQuery
觀察者查詢的類:數(shù)據(jù)改變時發(fā)送通知(可以后臺)熟呛。

4、HKAnchoredObjectQuery
錨定對象查詢的類:數(shù)據(jù)插入后返回新插入的數(shù)據(jù)尉姨。

?5庵朝、HKStatisticsQuery
統(tǒng)計查詢的類:返回樣本的總和/最大值/最小值...

6、HKStatisticsCollectionQuery
統(tǒng)計集合查詢的類:返回時間段內(nèi)樣本的數(shù)據(jù)。

?7九府、HKCorrelation
相關性查詢的類:查詢樣本相關(使用少)椎瘟。

8、HKSourceQuery
來源查詢的類:查詢數(shù)據(jù)來源侄旬。

顯然我們只要使用?HKStatisticsQuery即可實現(xiàn)獲取總的步數(shù)的想法肺蔚,經(jīng)過一番修改后問題終于完美解決,獲取到的步數(shù)與健康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>

/// 健康數(shù)據(jù)查詢類
@property (nonatomic, strong) HKHealthStore *healthStore;
/// 協(xié)處理器類
@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 系統(tǒng)低于8.0不能獲取健康數(shù)據(jù)宣羊,請升級系統(tǒng)"                                                                      forKey:NSLocalizedDescriptionKey];
        NSError *aError = [NSError errorWithDomain:@"xxxx.com.cn" code:0 userInfo:userInfo];
        resultBlock(NO,aError);
    }
    
}

#pragma mark - 獲取當天健康數(shù)據(jù)(步數(shù))
- (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(@"當天行走步數(shù) = %lf",stepCount);
            if(stepCount > 0){
                if (queryResultBlock) {
                    queryResultBlock(stepCount,nil);
                }
            } else {
                [self getCMPStepCount: queryResultBlock];
            }
        }
        
    }];
    [self.healthStore executeQuery:query];
} 

#pragma mark - 構造當天時間段查詢參數(shù) 
- (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 - 獲取協(xié)處理器步數(shù)
- (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、對于網(wǎng)上給出的解決方案代碼不能只顧一時爽全盤抄寫族操,要做具體分析(目前網(wǎng)上大部分獲取健康步數(shù)的代碼赞枕,在存在多設備上傳健康數(shù)據(jù)情況下,統(tǒng)計是不準確的)坪创。

3炕婶、要多去學習和了解HealthKit等我們用到的系統(tǒng)框架源碼,熟悉底層邏輯底層處理方法莱预。

7.iPhone協(xié)處理器說明

文中有提到iPhone協(xié)處理器柠掂,可能大部分人不了解這個東西是干嘛的,這里做個簡單介紹依沮。

目前iPhone設備中一般用的M8協(xié)處理器涯贞,它的作用是持續(xù)測量來自加速感應器、指南針危喉、陀螺儀和全新氣壓計的數(shù)據(jù)宋渔,為A8芯片分擔更多的工作量,從而提升了工作效能辜限。不僅如此皇拣,這些傳感器現(xiàn)在還具備更多功能,比如可以測量行走的步數(shù)薄嫡、距離和海拔變化等氧急。

參考資料來源:
1、iOS 8 HealthKit 介紹
2毫深、手機上的協(xié)處理器有什么作用_蘋果協(xié)處理器是干什么的

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吩坝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子哑蔫,更是在濱河造成了極大的恐慌钉寝,老刑警劉巖弧呐,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異嵌纲,居然都是意外死亡俘枫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門疹瘦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崩哩,“玉大人巡球,你說我怎么就攤上這事言沐。” “怎么了酣栈?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵险胰,是天一觀的道長。 經(jīng)常有香客問我矿筝,道長起便,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任窖维,我火速辦了婚禮榆综,結果婚禮上,老公的妹妹穿的比我還像新娘铸史。我一直安慰自己鼻疮,他們只是感情好,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布琳轿。 她就那樣靜靜地躺著判沟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崭篡。 梳的紋絲不亂的頭發(fā)上挪哄,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音琉闪,去河邊找鬼迹炼。 笑死,一個胖子當著我的面吹牛颠毙,可吹牛的內(nèi)容都是我干的疗涉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼吟秩,長吁一口氣:“原來是場噩夢啊……” “哼咱扣!你這毒婦竟也來了?” 一聲冷哼從身側響起涵防,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤闹伪,失蹤者是張志新(化名)和其女友劉穎沪铭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體偏瓤,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡杀怠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了厅克。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赔退。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖证舟,靈堂內(nèi)的尸體忽然破棺而出硕旗,到底是詐尸還是另有隱情,我是刑警寧澤女责,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布漆枚,位于F島的核電站,受9級特大地震影響抵知,放射性物質發(fā)生泄漏墙基。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一刷喜、第九天 我趴在偏房一處隱蔽的房頂上張望残制。 院中可真熱鬧,春花似錦掖疮、人聲如沸初茶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纺蛆。三九已至,卻和暖如春规揪,著一層夾襖步出監(jiān)牢的瞬間桥氏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工猛铅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留字支,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓奸忽,卻偏偏與公主長得像堕伪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子栗菜,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353