iOS之HeathKit详解

6,403 阅读13分钟

简介

在HeathKit中,可以使用HKHealthStore类来访问健康App中的数据,如健身记录、营养摄入、睡眠状况等。也可以对这些数据进行读取和共享(即第三方App写入或删除数据到苹果健康App中)。笔者就以获取步数为例,还可以获取身高、体重、睡眠时间、心率等等。

使用前

在Xcode中, 打开HealthKit 功能 Snip1.png

在info.plist文件中,增加NSHealthShareUsageDescription用于读取数据的描述和NSHealthUpdateUsageDescription用于写入数据的描述。

Snip_3.png

HeathKit不支持在iPad中使用,而且它也不支持扩展。

HeathKit框架

主要是抽象类HKObject + HKObjectType

HKObject

在抽象类HKObject的子类中,每个对象都有下面的属性:

  • UUID:对象的标识符
  • source:数据的来源,来源可以是健康App,也可以第三方App。对象存储到HeathKit中时会设置其来源。只有从HeathKit中获取到的数据的来源才有效。
  • metadata:一个包含该对象额外信息的字典,元数据包含预定义的key和自定义的key,预定义的key用来帮助我们在应用间共享数据,而自定义的key用来扩展HeathKit,为对象添加针对应用的数据。

特征和样本

HeathKit的对象主要分为特征和样本(开始套娃):

  • 特征:用户的基本不变的数据,包括用户的生日、血型和性别等。只能用户在健康App中添加或修改。
  • 样本:某个时间段的数据,其对象都是HKSample的子类,有以下属性:
    • type:样本类型,例如:步数、距离、心率等,其类型又可以分为以下四种:
      • HKCategorySample:类别样本,iOS 8 中,只有睡眠分析这一个类别样本。代表有限种类的样本。
      • HKQuantitySample:代表存储数据的样本,比如步数、距离、用户的体温等。是最常见的数据类型。
      • HKCorrelation:代表复合数据,包括一个或者多个样本。在iOS 8 中,用correlation代表食物和血压。在创建食物或血压时,需要用correlation。
      • HKWorkout:代表某种活动,比如走、跑步等。包含有开始时间、结束时间、运动类型、消耗能量、运动距离等属性。还可以为workout关联许多详细的样本。不像correlation,这些样本不包含在workou中,但是可以通workout获取到。
    • startDate:样本采样开始时间。
    • endDate:样本采样结束时间。(存在startDate == endDate)

常用的数据类型:

  • HKQuantityTypeIdentifierBodyMassIndex: 体重指数
  • HKQuantityTypeIdentifierBodyFatPercentage: 体脂百分比
  • HKQuantityTypeIdentifierHeight: 身高
  • HKQuantityTypeIdentifierBodyMass: 体重
  • HKQuantityTypeIdentifierLeanBodyMass: 瘦体重
  • HKQuantityTypeIdentifierWaistCircumference: 腰围
  • HKQuantityTypeIdentifierStepCount: 步数
  • HKQuantityTypeIdentifierDistanceWalkingRunning: 步行+跑步距离
  • HKQuantityTypeIdentifierFlightsClimbed: 上楼梯数
  • HKQuantityTypeIdentifierDistanceSwimming: 游泳距离
  • HKQuantityTypeIdentifierDistanceDownhillSnowSports: 滑雪距离
  • HKQuantityTypeIdentifierHeartRate: 心率
  • HKQuantityTypeIdentifierBodyTemperature: 身体温度
  • HKQuantityTypeIdentifierBloodPressureSystolic: 血液收缩压
  • HKQuantityTypeIdentifierBloodPressureDiastolic: 血液舒张压
  • HKQuantityTypeIdentifierRespiratoryRate: 用户呼吸率
  • HKQuantityTypeIdentifierRestingHeartRate: 静息心率
  • HKQuantityTypeIdentifierWalkingHeartRateAverage: 步行时心率

一共有超过100项数据类型。目前常用或是能用也就这几项。

需要查询其他类型的数据只需替换类型即可。

HKUnit

读取的数据的单位的类,比如体重的单位kg,时间单位min(minutes)、hr(hours)、d(days)等。一般在获取数据或写入样本数据的时候需要添加对应的单位,如多少步、多少米等。

HKQuery

读取数据的方法,大致以下几类:

  • HKHealthStore:提供了一个用于访问和存储用户健康数据的接口。
  • HKSampleQuery:样本查询。这是使用最多的查询。使用样本查询可以查询在HeathKit中任意的数据。而且可以对结果进行排序等。
  • HKObserverQuery:观察者查询的类:这是一个长时间运行的查询,它会检测HealthKit存储,并在匹配到的样本发生变化时通知你(可以后台)。
  • HKAnchoredObjectQuery:锚定对象查询。用这种查询来搜索添加进存储的项。当锚定查询第一次执行时,会返回存储中所有匹配的样本。在接下来的执行中,只会返回上一次执行之后添加的项目。通常,锚定对象查询会和观察者查询一起使用。观察者查询告诉你某些项目发生了变化,而锚定对象查询来决定有哪些(如果有的话)项目被添加进了存储。
  • HKStatisticsQuery:统计查询。使用这种查询来在一系列匹配的样本中执行统计运算。即计算与给定数量类型和谓词匹配的数量样本的统计信息。你可以使用统计查询来计算样本的总和、最小值、最大值或平均值。
  • HKStatisticsCollectionQuery:统计集合查询。使用这种查询来在一系列长度固定的时间间隔中执行多次统计查询。通常使用这种查询来生成图表。查询提供了一些简单的方法来计算某些值,例如,每天消耗的总热量或者每5分钟行走的步数。统计集合查询是长时间运行的。查询可以返回当前的统计集合,也可以监测HealthKit存储,并对更新做出响应。
  • HKCorrelation:Correlation查询。使用这种查询来在correlation查找数据。这种查询可以为correlation中每个样本类型包含独立的谓词。如果你只是想匹配correlation类型,那么请使用样本查询。
  • HKSourceQuery:来源查询。使用这种查询来查找HealthKit存储中的匹配数据的来源(应用和设备)。来源查询会列出储存的特定样本类型的所有来源。

注意事项

因为是健康App的步数是可写入的,所以想要获取真实的步数,就需要将手动写入的数据过滤,即:真实步数 = 总步数 - 写入的步数。

获取步数完整代码

#import "ViewController.h"
#import <HealthKit/HealthKit.h>

@interface ViewController ()
// 创建healthStore实例对象
@property (nonatomic,strong) HKHealthStore *healthStore;
// 查询数据的类型,比如计步,行走+跑步距离等等
@property (nonatomic,strong) HKQuantityType *quantityType
// 谓词,用于限制查询返回结果
@property (nonatomic,strong) NSPredicate *predicate;
@end

@implementation ViewController

- (HKHealthStore *)healthStore{
    if(_healthStore == nil){
        _healthStore = [[HKHealthStore alloc]init];
    }
    return _healthStore;
}

- (HKQuantityType *)quantityType{
    if(_quantityType == nil){
        _quantityType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    }
    return _quantityType;
}

- (NSPredicate *)predicate{
    if(_predicate == nil){
        // 构造当天时间段查询参数
        NSCalendar *calendar = [NSCalendar currentCalendar];
        NSDate *now = [NSDate date];
        // 开始时间
        NSDate *startDate = [calendar startOfDayForDate:now];
        // 结束时间
        NSDate *endDate = [calendar dateByAddingUnit:NSCalendarUnitDay value:1 toDate:startDate options:0];
        _predicate = [HKQuery predicateForSamplesWithStartDate:startDate endDate:endDate options:HKQueryOptionStrictStartDate];
    }
    return _predicate;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self useHealthKit];
}
// 注意代码块中的循环引用,这里忽略
- (void)useHealthKit{

    //判断设备是否支持查看healthKit数据
    if([HKHealthStore isHealthDataAvailable] == NO){
        NSLog(@"设备不支持healthKit");
        return;
    }

    // 这里只获取运动步数的权限
    NSSet *readObjectTypes = [NSSet setWithObjects:[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount], nil];
    
    // 向用户请求授权共享或读取健康App数据
    [self.healthStore requestAuthorizationToShareTypes:nil readTypes:readObjectTypes completion:^(BOOL success, NSError * _Nullable error) {
        if(success){
            [self queryTotalStepCount:^(NSInteger stepCount) {
                NSLog(@"真实运动步数(总步数 - 编辑的步数) = %ld",(long)stepCount);
            }];
        }else{
            NSLog(@"获取步数权限失败");
        }
    }];
}
 
//这里的步数是总步数,和各个来源 包含手动编辑录入的
// HKStatisticsOptionCumulativeSum 总步数
// HKStatisticsOptionSeparateBySource 健康App所有步数数据的来源,包括iPhone、iWatch、健康App、第三方App等
- (void)queryTotalStepCount:(void(^)(NSInteger stepCount))completion{
    HKStatisticsQuery *query = [[HKStatisticsQuery alloc]initWithQuantityType:self.quantityType quantitySamplePredicate:self.predicate options:HKStatisticsOptionCumulativeSum|HKStatisticsOptionSeparateBySource completionHandler:^(HKStatisticsQuery * _Nonnull query, HKStatistics * _Nullable result, NSError * _Nullable error) {
        if (error) {
            NSLog(@"获取失败!");
            !completion?:completion(0);
            return;
        }
        // 总步数
        double totalStepCount = [result.sumQuantity doubleValueForUnit:[HKUnit countUnit]];
        // 健康App编辑的步数
        double userEnteredCount = 0;
        // 遍历数据来源,获得健康App编辑的数值
        for(HKSource *source in result.sources){
            if([source.name isEqualToString:@"健康"]){
                userEnteredCount = [[result sumQuantityForSource:source] doubleValueForUnit:[HKUnit countUnit]];
            }
        }
        !completion?:completion((NSInteger)(totalStepCount - userEnteredCount)); 
    }];
    [self.healthStore executeQuery:query];
}
 
@end

正常情况下,过滤用户在健康App手动输入的步数后就能较精确的数值,但是第三方App在获取授权后也可以向健康App写入样本数据,如添加在某个开始时间到结束时间段内,行走了多少步的数据。

死磕?思考?

接下来我们简单深入下,我们依次做下以下操作:

  • 我们用上面代码读取步数,比较健康App、微信、QQ、Keep、支付宝的数值
  • 在健康App手动编辑输入10000步,再重复步骤一
  • 使用代码写入10000步,再重复步骤一

在进行上述测试时,笔者的手机和手表放在桌面,保证不会有步数数据更新。

步骤一

我们使用HKSampleQuery查询下,获得每个样本更详细的数据:

// 结果排序,从开始到结束依次
NSSortDescriptor *startSortDec = [NSSortDescriptor sortDescriptorWithKey:HKPredicateKeyPathStartDate ascending:NO];
NSSortDescriptor *endSortDec = [NSSortDescriptor sortDescriptorWithKey:HKPredicateKeyPathEndDate ascending:NO];
    
HKSampleQuery *sampleQuery = [[HKSampleQuery alloc]initWithSampleType:self.quantityType predicate:self.predicate limit:HKObjectQueryNoLimit sortDescriptors:@[startSortDec,endSortDec] resultsHandler:^(HKSampleQuery * _Nonnull query, NSArray<__kindof HKSample *> * _Nullable results, NSError * _Nullable error) {
        if(error){
            !completion?:completion(0);
            return;
        }else{
            // 单位
            HKUnit *unit = [HKUnit countUnit];
            // 计算iPhone记录的步数
            NSInteger iPhoneCount = 0;
            // 计算iWatch记录的步数
            NSInteger iWatchCount = 0;
            // 计算健康App手动编辑的步数
            NSInteger userEnteredCount = 0;
            // 计算第三方App写入的步数
            NSInteger thirdAppCount = 0;
            // 遍历样本
            for (HKQuantitySample *sample in results){
                // 样本步数
                NSInteger count = (NSInteger)[sample.quantity doubleValueForUnit:unit];
                // 设备名称
                NSString *deviceName = sample.device.name;
                if (deviceName == nil) { // 包含手动编辑和第三方App写入
                    // 判断用户手动录入的数据。
                    NSInteger isUserEntered = [sample.metadata[HKMetadataKeyWasUserEntered] integerValue];;
                    if(isUserEntered == 1){
                        userEnteredCount += count;
                    }else{
                        thirdAppCount += count;
                    }
                }else if ([deviceName isEqualToString:@"iPhone"]){
                    iPhoneCount += count;
                }else if ([deviceName isEqualToString:@"Apple Watch"]){
                    iWatchCount += count;
                }
            }
            NSLog(@"iPhone记录的步数 = %ld",(long)iPhoneCount);
            NSLog(@"iWatch记录的步数 = %ld",(long)iWatchCount);
            NSLog(@"健康App手动编辑的步数 = %ld",(long)userEnteredCount);
            NSLog(@"第三方App写入的步数 = %ld",(long)thirdAppCount);       
            // 主线程更新UI
            dispatch_async(dispatch_get_main_queue(), ^{
                !completion?:completion(userEnteredCount);                
            });
        }
    }];

    [self.healthStore executeQuery:sampleQuery];

运行:

WeChat535062e0e12c8048a789ad7f03aa9400.png

健康App截图:

WeChat974fec423f3b023a0b65a6ba390451e5.png

微信截图:

WeChatd80366c4b54b2cf025d312ffd278f698.png

QQ截图:

QQ特意分两种情况,授权使用健康App前后两种情况。 未授权截图:

WeChat1309ab91ec32931f7ec9392822a62fd1.png

授权后截图:

WeChatd2959ece28e4ac6711af6a9202499b59.png

以上可知QQ获取运动步数的方式是,若用户授权使用健康App,则读取健康App的数据;若用户未授权则使用CMPedometer传感器获取。

Keep截图:

WeChatf130432dcb833900c14616e2d3a8abaf.png

支付宝截图:

WeChate8e240deb50d4807ea204df7bbe05a5f.png

WeChate69e041634f079f0c397d3ec49e08d4c.png 总结

多数App优先使用的还是健康App的数据,其次使用CMPedometer传感器获取运动步数。其中Keep作为一款主打运动的应用,可能有自己的优化或算法也算正常。

步骤二

在健康App手动编辑输入10000步:

WeChatdc0c5b963d9dbd2819afe55cdbec3c15.png

代码读取:

WeChate69e041634f079f0c397d3ec49e08d4c.png

健康App截图:

WeChat0a0fd7cfb648cb000b5e178cf3e2574c.png

微信运动截图:

WeChat6340d6925fca32a1efaead30d0278dae.png

QQ运动截图:

WeChat9fd308d4b7be216b4a4ecbe503e669c9.png

Keep截图:

WeChat863224fbd6576a385803a4dd8026e821.png

支付宝运动截图:

WeChated7dac4ba20befa8f3b91c7a90b61dd2.png

总结

在健康App手动输入10000步后,我们使用代码可以识别出手动编辑的,并计算真实的步数。除了Keep之外,其他的App操作均相同,过滤了手动编辑值的影响。

记住我们目前比较准确的步数是3288步。

步骤三

使用代码写入10000步,再重复步骤一

写入步数代码(也可以删除数据,这里就不演示了):

// 向健康App写入步数
- (void)userEnteredStepCount:(double)step{
    // 授权写入的数据类型为步数
    HKQuantityType *stepType = [HKQuantityType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
    NSSet *shareTypes = [NSSet setWithObjects:stepType, nil];
   [self.healthStore requestAuthorizationToShareTypes:shareTypes readTypes:nil completion:^(BOOL success, NSError * _Nullable error) {    
        NSCalendar *calendar = [NSCalendar currentCalendar];
        NSDate *now = [NSDate date];
        // 开始时间,这里为当天的零点时刻
        NSDate *startDate = [calendar startOfDayForDate:now];
        // 多少步
        HKQuantity *stepQuantity = [HKQuantity quantityWithUnit:[HKUnit countUnit] doubleValue:step];
        // 创建样本(某个时间段或时刻内,行走多少步)
        HKQuantitySample *stepSample = [HKQuantitySample quantitySampleWithType:stepType quantity:stepQuantity startDate:startDate endDate:[NSDate date]];
        // 写入样本
        [self.healthStore saveObject:stepSample withCompletion:^(BOOL success, NSError * _Nullable error) {
          if (error) {
            NSLog(@"error: %@", error.localizedDescription);
          }
           dispatch_async(dispatch_get_main_queue(), ^{
              NSLog(@"写入%@",success ? @"成功" : @"失败");
          });
        }];         
    }];
}

代码读取:

WeChata307fb2795bbced552bd9dbd06f955f4.png

健康App:

WeChat8e3d38f7613d19530591c6053953edb9.png

微信运动:

WeChatb741e91c7347486f1561b7dc7f604f6c.png

QQ运动:

WeChatf0ff0c9ddacd01e90df169f2f1622e12.png

Keep运动:

WeChat6cf17a9ec85683a361ca9d5d6f233605.png

支付宝运动:

WeChat16e0decaaa54a96b19bd3ba2bdb79939.png

总结

还记得我们正常的步数是3288吗!

代码写入10000步后,各个App差异就比较大了。我们可以在设置中查看哪个第三方App写入了数据:

WeChataa9bf6981aefb074e970b9c80624fb3e.png

其次查询HKSource数据来源,也可以知道当前数据来源一共有四种:

WeChataf0ba4042742a1420360fd78358bc761.png

首先是我们代码获取的,这是可以预见的,毕竟我们只是过滤了用户在健康App手动输入的,对于第三方App写入的数据并没有处理。

健康App也并不是我们想象的那样直接将与之前的数值相加,而是做了一定的处理,这也是可以预见的,在第三方写入的时候,在不同的时间段的重复数据进行优化!

其中QQ运动也飘了,而且奇怪的是再删除了步骤一和步骤二添加的20000步时,其他的App或多或少都对应更新数据,但QQ好像是将数据本地持久化了,一直不变;而且笔者测试了多次,有时候QQ的算法跟我们代码实现的数值一致,即总步数 - 编辑的步数,但有的时候却又不一样。不知道是否是刷新的问题?但可以肯定的是QQ的运动步数存在问题。

而Keep依旧,并没有过滤手动输入和第三方写入的数据。

微信和支付宝还是稳啊!依然是3288

我们也可以用代码输出所有的样本信息,因为篇幅原因,我们取用前后的截图:

WeChat878cd22849ce68f329cdc7b9d7ecf1d7.png

WeChate8275736ad1efdcf69cf49458f7cbe1c.png

由此我们可以准确知道在查询时间内每个样本的采样时间、来源、数值、方式等。

也可以知道为什么第三方写入步数数据后,健康App没有直接相加的原因。因为我们写入的时间段是:00:00:00 ~ 13:54:36,这个时间段中,iPhone和iWatch都有运动的记录,三者数据或多或少有一定的交叉,甚至是手机和手表在相同的时间段中的步数也有一定的差异,健康App应该做了一定的优化和算法处理,得到他认为合理的数值。

而微信和支付宝则直接摒弃了所有外来的数据。哪一种处理方式合理,其实我们实验结果已经很明了了。

不足之处

这点笔者很遗憾,研究了一个下午的时候也没有弄清微信的优化和算法。有知道的大佬还请不吝赐教,感谢!

最后附录步数数据:

// 时间段数据(设备 开始时间  结束时间  步数)
iPhone  08:15:47   08:15:49   4
iPhone  08:15:21   08:15:24   5
Apple Watch  08:11:43   08:15:10   265
iPhone  08:07:55   08:15:21   786
iPhone  08:07:08   08:07:55   9
Apple Watch  08:04:02   08:11:43   584
iPhone  07:56:55   08:04:05   194
Apple Watch  07:52:31   07:59:13   254
iPhone  07:46:04   07:55:40   83
iPhone  07:22:51   07:22:56   12

Apple Watch 运动时间 = 1069.82 s
iPhone 运动时间 = 1509.27 s
运动总时间 = 1074.98 s
真实运动步数(总步数 - 编辑的步数) = 1115
健康App手动编辑的步数 = 0
第三方App写入的步数 = 0