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

1.前言

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

2.問題描述

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

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

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

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

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

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

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

那么問題的癥結(jié)算是找到了,問題設(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(@"當(dāng)天行走步數(shù) = %lf",totleSteps);
                completion(totleSteps,error);
            }
    }];
    
    [self.healthStore executeQuery:query];
} 

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

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

double totleSteps = 0;
for(HKQuantitySample *quantitySample in results)
  {
      // 過濾掉其它應(yīng)用寫入的健康數(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(@"當(dāng)天行走步數(shù) = %lf",totleSteps);
      completion(totleSteps,error);
 } 

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

那有沒有辦法直接取到健康A(chǔ)pp顯示的那個總步數(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ù)進(jìn)行操作。

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
相關(guān)性查詢的類:查詢樣本相關(guān)(使用少)。

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

顯然我們只要使用?HKStatisticsQuery即可實現(xiàn)獲取總的步數(shù)的想法,經(jīng)過一番修改后問題終于完美解決,獲取到的步數(shù)與健康A(chǔ)pp以及微信運動保持了一致。

修正后的完整代碼如下:

#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 - 應(yīng)用授權(quán)檢查
- (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 - 獲取當(dāng)天健康數(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(@"當(dāng)天行走步數(shù) = %lf",stepCount);
            if(stepCount > 0){
                if (queryResultBlock) {
                    queryResultBlock(stepCount,nil);
                }
            } else {
                [self getCMPStepCount: queryResultBlock];
            }
        }
        
    }];
    [self.healthStore executeQuery:query];
} 

#pragma mark - 構(gòu)造當(dāng)天時間段查詢參數(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];
        // 結(jié)束時間
        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 - 跳轉(zhuǎn)App運動與健康設(shè)置頁面 
- (void)goAppRunSettingPage { 
    NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
    NSString *msgStr = [NSString stringWithFormat:@"請在【設(shè)置->%@->%@】下允許訪問權(quán)限",appName,@"運動與健身"];
    dispatch_async(dispatch_get_main_queue(), ^{
    UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"使用提示" message:msgStr delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"設(shè)置", 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.問題總結(jié)

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

2、對于網(wǎng)上給出的解決方案代碼不能只顧一時爽全盤抄寫,要做具體分析(目前網(wǎng)上大部分獲取健康步數(shù)的代碼,在存在多設(shè)備上傳健康數(shù)據(jù)情況下,統(tǒng)計是不準(zhǔn)確的)。

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,635評論 2 380