为什么HKAnchoredObjectQuery和enableBackgroundDeliveryForType总是在应用程序处于后台时触发?

我正在试验一下,以便熟悉HKAnchoredObjectQuery,并在我的应用程序处于非活动状态时获取结果。 我启动应用程序,切换到苹果健康,input血糖的结果; 有时会立即调用结果处理程序(如打印到控制台所certificate的那样),但有时在处理程序切换回我的应用程序之前,不会调用处理程序。 删除结果以及添加结果也是如此。 任何人有任何指导?

这些代码大部分是来自digital数据库的一个问题,在这里,当应用程序在后台并且login到控制台的时候,这个问题就可以在这里得到更新。 请参阅: iOS 9中的HealthKenAnchoredObjectQuery,不返回HKDeletedObject

class HKClient : NSObject { var isSharingEnabled: Bool = false let healthKitStore:HKHealthStore? = HKHealthStore() let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)! override init(){ super.init() } func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) { let dataTypesToRead : Set<HKObjectType> = [ glucoseType ] if(!HKHealthStore.isHealthDataAvailable()) { // let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"]) self.isSharingEnabled = false return } self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in self.isSharingEnabled = true authorizationCompleted(success: success, error: error) } } func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:uint, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!) { let queryEndDate = NSDate(timeIntervalSinceNow: NSTimeInterval(60.0 * 60.0 * 24)) let queryStartDate = NSDate.distantPast() let sampleType: HKSampleType = glucoseType as! HKSampleType let predicate: NSPredicate = HKAnchoredObjectQuery.predicateForSamplesWithStartDate(queryStartDate, endDate: queryEndDate, options: HKQueryOptions.None) var hkAnchor: HKQueryAnchor if(anchor != nil){ hkAnchor = anchor! } else { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = { (query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in var added = [String]() var deleted = [String]() if (addedObjects?.count > 0){ for obj in addedObjects! { let quant = obj as? HKQuantitySample if(quant?.UUID.UUIDString != nil){ let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! ) let msg : String = (quant?.UUID.UUIDString)! + " " + String(val) added.append(msg) } } } if (deletedObjects?.count > 0){ for del in deletedObjects! { let value : String = del.UUID.UUIDString deleted.append(value) } } if(callback != nil){ callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError) } } // remove predicate to see deleted objects let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults) // added - query should be always running anchoredQuery.updateHandler = onAnchorQueryResults // added - allow query to pickup updates when app is in backgroun healthKitStore?.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate) { (success, error) in if (!success) {print("enable background error")} } healthKitStore?.executeQuery(anchoredQuery) } let AnchorKey = "HKClientAnchorKey" func getAnchor() -> HKQueryAnchor? { let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey) if(encoded == nil){ return nil } let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor return anchor } func saveAnchor(anchor : HKQueryAnchor) { let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor) NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey) NSUserDefaults.standardUserDefaults().synchronize() } } class ViewController: UIViewController { let debugLabel = UILabel(frame: CGRect(x: 10,y: 20,width: 350,height: 600)) override func viewDidLoad() { super.viewDidLoad() self.view = UIView(); self.view.backgroundColor = UIColor.whiteColor() debugLabel.textAlignment = NSTextAlignment.Center debugLabel.textColor = UIColor.blackColor() debugLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping debugLabel.numberOfLines = 0 self.view.addSubview(debugLabel) let hk = HKClient() hk.requestGlucosePermissions(){ (success, error) -> Void in if(success){ let anchor = hk.getAnchor() hk.getGlucoseSinceAnchor(anchor, maxResults: 0) { (source, added, deleted, newAnchor, error) -> Void in var msg : String = String() if(deleted?.count > 0){ msg += "Deleted: \n" + (deleted?[0])! for s in deleted!{ msg += s + "\n" } } if (added?.count > 0) { msg += "Added: " for s in added!{ msg += s + "\n" } } if(error != nil) { msg = "Error = " + (error?.description)! } if(msg.isEmpty) { msg = "No changes" } debugPrint(msg) if(newAnchor != nil && newAnchor != anchor){ hk.saveAnchor(newAnchor!) } dispatch_async(dispatch_get_main_queue(), { () -> Void in self.debugLabel.text = msg }) } } } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } 

我还在各种应用程序状态更改中添加了print()。 控制台日志的样本(这是在Xcode的iPhone 6s设备上运行的)显示了在我进入后台之后,但是在重新进入前台之前有时被调用的处理程序,以及仅在重新进入前台之后才重新进入前台。

 app did become active "No changes" app will resign active app did enter background app will enter foreground "Added: E0340084-6D9A-41E4-A9E4-F5780CD2EADA 99.0\n" app did become active app will resign active app did enter background "Added: CEBFB656-0652-4109-B994-92FAA45E6E55 98.0\n" app will enter foreground "Added: E2FA000A-D6D5-45FE-9015-9A3B9EB1672C 97.0\n" app did become active app will resign active app did enter background "Deleted: \nD3124A07-23A7-4571-93AB-5201F73A4111D3124A07-23A7-4571-93AB-5201F73A4111\n92244E18-941E-4514-853F-D890F4551D76\n" app will enter foreground app did become active app will resign active app did enter background app will enter foreground "Added: 083A9DE4-5EF6-4992-AB82-7CDDD1354C82 96.0\n" app did become active app will resign active app did enter background app will enter foreground "Added: C7608F9E-BDCD-4CBC-8F32-94DF81306875 95.0\n" app did become active app will resign active app did enter background "Deleted: \n15D5DC92-B365-4BB1-A40C-B870A48A70A415D5DC92-B365-4BB1-A40C-B870A48A70A4\n" "Deleted: \n17FB2A43-0828-4830-A229-7D7DDC6112DB17FB2A43-0828-4830-A229-7D7DDC6112DB\n" "Deleted: \nCEBFB656-0652-4109-B994-92FAA45E6E55CEBFB656-0652-4109-B994-92FAA45E6E55\n" app will enter foreground "Deleted: \nE0340084-6D9A-41E4-A9E4-F5780CD2EADAE0340084-6D9A-41E4-A9E4-F5780CD2EADA\n" app did become active 

我build议使用HKObserverQuery并仔细设置它。

有一个algorithm可以监视当你启用后台交付时如何以及何时调用HKObserverQuery的“完成”处理程序。 这个细节不太清楚。 苹果开发者论坛上有人称之为“三击”规则,但苹果公司并没有发布任何可以find的行为的文档。

https://forums.developer.apple.com/thread/13077

我注意到的一件事是,如果你的应用程序响应一个HKObserverQuery的后台交付,创build一个HKAnchoredObjectQuery,并在该HKAnchoredObjectQuery中设置UpdateHandler,这个UpdateHandler通常会导致多次的callback。 我怀疑,也许因为这些额外的callback正在执行后,你已经告诉苹果,你已经完成了你的工作,以响应后台交付,你多次打电话给完成处理程序,也许他们给你一些“分”,给你打电话不太常见的不良行为。

通过执行以下操作,获得了一致的callback,我获得了最大的成功:

  1. 使用ObserverQuery并确保“完成”处理程序的调用被调用一次,并在您的工作结束。
  2. 在后台运行时不要在HKAnchoredObjectQuery中设置更新处理程序(有助于实现1)。
  3. 专注于使我的查询处理程序,AppDelegate和ViewController尽可能快。 我注意到,当我把所有的callback缩减到只是一个print语句时,HealthKit的callback立即更加一致。 所以说苹果绝对是在关注执行时间。 所以尽量在可能的情况下静态声明事物,并注重速度。

自从我使用Xamarin.iOS之后,我开始使用Xamarin.iOS这个原始的项目,而不是很快,所以我没有跟上原来的代码。 但是这里有一个更新(未经testing)的代码版本,应该考虑到这些变化(速度改进除外):

 // // HKClient.swift // HKTest import UIKit import HealthKit class HKClient : NSObject { var isSharingEnabled: Bool = false let healthKitStore:HKHealthStore? = HKHealthStore() let glucoseType : HKObjectType = HKObjectType.quantityTypeForIdentifier(HKQuantityTypeIdentifierBloodGlucose)! override init(){ super.init() } func requestGlucosePermissions(authorizationCompleted: (success: Bool, error: NSError?)->Void) { let dataTypesToRead : Set<HKObjectType> = [ glucoseType ] if(!HKHealthStore.isHealthDataAvailable()) { // let error = NSError(domain: "com.test.healthkit", code: 2, userInfo: [NSLocalizedDescriptionKey: "Healthkit is not available on this device"]) self.isSharingEnabled = false return } self.healthKitStore?.requestAuthorizationToShareTypes(nil, readTypes: dataTypesToRead){(success, error) -> Void in self.isSharingEnabled = true authorizationCompleted(success: success, error: error) } } func startBackgroundGlucoseObserver( maxResultsPerQuery: Int, anchorQueryCallback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!)->Void { let onBackgroundStarted = {(success: Bool, nsError : NSError?)->Void in if(success){ //Background delivery was successfully created. We could use this time to create our Observer query for the system to call when changes occur. But we do it outside this block so that even when background deliveries don't work, //we will have the observer query working when are in the foreground at least. } else { debugPrint(nsError) } let obsQuery = HKObserverQuery(sampleType: self.glucoseType as! HKSampleType, predicate: nil) { query, completion, obsError in if(obsError != nil){ //Handle error debugPrint(obsError) abort() } var hkAnchor = self.getAnchor() if(hkAnchor == nil) { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } self.getGlucoseSinceAnchor(hkAnchor, maxResults: maxResultsPerQuery, callContinuosly:false, callback: { (source, added, deleted, newAnchor, error) -> Void in anchorQueryCallback(source: self, added: added, deleted: deleted, newAnchor: newAnchor, error: error) //Tell Apple we are done handling this event. This needs to be done inside this handler completion() }) } self.healthKitStore?.executeQuery(obsQuery) } healthKitStore?.enableBackgroundDeliveryForType(glucoseType, frequency: HKUpdateFrequency.Immediate, withCompletion: onBackgroundStarted ) } func getGlucoseSinceAnchor(anchor:HKQueryAnchor?, maxResults:Int, callContinuosly:Bool, callback: ((source: HKClient, added: [String]?, deleted: [String]?, newAnchor: HKQueryAnchor?, error: NSError?)->Void)!){ let sampleType: HKSampleType = glucoseType as! HKSampleType var hkAnchor: HKQueryAnchor; if(anchor != nil){ hkAnchor = anchor! } else { hkAnchor = HKQueryAnchor(fromValue: Int(HKAnchoredObjectQueryNoAnchor)) } let onAnchorQueryResults : ((HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, NSError?) -> Void)! = { (query:HKAnchoredObjectQuery, addedObjects:[HKSample]?, deletedObjects:[HKDeletedObject]?, newAnchor:HKQueryAnchor?, nsError:NSError?) -> Void in var added = [String]() var deleted = [String]() if (addedObjects?.count > 0){ for obj in addedObjects! { let quant = obj as? HKQuantitySample if(quant?.UUID.UUIDString != nil){ let val = Double( (quant?.quantity.doubleValueForUnit(HKUnit(fromString: "mg/dL")))! ) let msg : String = (quant?.UUID.UUIDString)! + " " + String(val) added.append(msg) } } } if (deletedObjects?.count > 0){ for del in deletedObjects! { let value : String = del.UUID.UUIDString deleted.append(value) } } if(callback != nil){ callback(source:self, added: added, deleted: deleted, newAnchor: newAnchor, error: nsError) } } let anchoredQuery = HKAnchoredObjectQuery(type: sampleType, predicate: nil, anchor: hkAnchor, limit: Int(maxResults), resultsHandler: onAnchorQueryResults) if(callContinuosly){ //The updatehandler should not be set when responding to background observerqueries since this will cause multiple callbacks anchoredQuery.updateHandler = onAnchorQueryResults } healthKitStore?.executeQuery(anchoredQuery) } let AnchorKey = "HKClientAnchorKey" func getAnchor() -> HKQueryAnchor? { let encoded = NSUserDefaults.standardUserDefaults().dataForKey(AnchorKey) if(encoded == nil){ return nil } let anchor = NSKeyedUnarchiver.unarchiveObjectWithData(encoded!) as? HKQueryAnchor return anchor } func saveAnchor(anchor : HKQueryAnchor) { let encoded = NSKeyedArchiver.archivedDataWithRootObject(anchor) NSUserDefaults.standardUserDefaults().setValue(encoded, forKey: AnchorKey) NSUserDefaults.standardUserDefaults().synchronize() } }