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、手機上的協處理器有什么作用_蘋果協處理器是干什么的