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ù)查詢方法類如下:
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é)處理器是干什么的