iOS Swift - 使用 NSTimer 为应用程序后台重新加载位置功能不起作用
iOS Swift - Reload location function with NSTimer for app background doesn't work
我在定位服务方面遇到了问题。我无法设置通过 NSTimer 在后台更新我的位置坐标的功能。这是我来自 appDelegate 的代码:
var locationManager = CLLocationManager()
func applicationDidEnterBackground(application: UIApplication) {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.theTimer = NSTimer(fireDate: NSDate(), interval: 40, target: self, selector: "handleTimer", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.theTimer, forMode: NSDefaultRunLoopMode)
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locValue:CLLocationCoordinate2D = manager.location.coordinate
println("dinBack = \(locValue.latitude) \(locValue.longitude)")
self.locationManager.stopUpdatingLocation()
}
func handleTimer(){
println("started")
self.locationManager.startUpdatingLocation()
}
PS。 - 当然,我已经导入了核心定位。
- 当我回到应用程序时,控制台会打印本应在后台打印的内容。
当您的应用程序处于后台时,您无法使 NSTimer
像这样工作。 NSTimer
不是“实时机制”。来自 official documentation:
Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
强调我的。
重要的一点是,当您的应用程序在后台时,您的计时器本应安排在的任何 运行 循环都不会主动 运行ning。
一旦您的应用 returns 进入前台,此 运行 循环就会重新启动,发现您的计时器已过期,并向选择器发送消息。
使用 iOS 7 及更高版本,如果您想在后台执行操作,您可以告诉 OS 您想要执行“后台获取”。
要设置它,我们必须首先告诉 OS 我们想要获取数据的频率,因此在 didFinishLaunching...
中添加以下方法:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
我们可以在这里传递任何时间间隔(例如,如果我们只想每天检查一次)。然而,我们传入的值仅定义了两次检查之间应该经过的最短时间。无法告诉 OS 检查之间的最长时间。
现在,我们必须实现在 OS 让我们有机会进行后台工作时实际调用的方法:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do background work
}
我们可以在此方法中做任何我们想做的事。然而,有两个问题。
- 当我们的应用程序在后台时调用此方法。 OS 将我们限制在(我相信)三十秒内。三十秒后,我们的时间到了。
- 我们必须调用
completionHandler()
(否则OS会认为我们用完了所有时间)。
传入的completionHandler
接受一个枚举,UIBackgroundFetchResult
。我们应该通过 .Failed
、.NewData
或 .NoData
,这取决于我们的实际结果(这种方法 通常 用于检查新数据的服务器)。
因此,我们的方法可能如下所示:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do stuff
if let _ = error {
completionHandler(.Failed)
} else if results.count > 0 {
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
}
请记住,我们绝对 零 控制 OS 实际让我们 运行 在后台执行此代码的频率。 OS 使用多个指标来优化用户体验。
我 认为 如果您的应用向完成处理程序报告 .Failed
,OS 可能很快会给您第二次机会,但是如果您滥用 .Failed
,OS 可能会将您的应用程序列入黑名单,禁止使用后台获取(Apple 可能 拒绝您的应用程序)。
如果您的应用未报告 .NewData
,OS 将让您的应用减少后台工作的频率。我这么说并不是因为我建议您始终报告 .NewData
。你绝对应该准确报告。 OS 在安排工作方面非常聪明。如果您在没有新数据时传递 .NewData
,OS 将使您的应用程序比它可能需要的更频繁地工作,这将更快地耗尽用户的电池(并可能导致他们完全卸载您的应用程序)。
但是,当您的应用程序开始执行后台工作时,还涉及其他指标。 OS 不太可能让任何应用程序在用户积极使用他们的设备时进行后台工作,而更有可能让应用程序在用户不使用他们的设备时进行后台工作。此外,OS 更有可能在连接到 WiFi 和插入某种充电器时进行后台工作。
OS 还会查看用户使用您的应用程序的频率,或者他们通常使用它的时间。如果用户每天下午 6 点使用您的应用程序,并且从不在任何其他时间使用您的应用程序,那么您的应用程序很可能总是有机会在 5:30pm 和下午 6 点之间(就在用户使用该应用程序之前)进行后台工作) 和 从不 在一天的任何其他时间。如果用户很少使用您的应用,则可能需要几天、几周或几个月才能有机会在后台工作。
我在定位服务方面遇到了问题。我无法设置通过 NSTimer 在后台更新我的位置坐标的功能。这是我来自 appDelegate 的代码:
var locationManager = CLLocationManager()
func applicationDidEnterBackground(application: UIApplication) {
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.theTimer = NSTimer(fireDate: NSDate(), interval: 40, target: self, selector: "handleTimer", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.theTimer, forMode: NSDefaultRunLoopMode)
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var locValue:CLLocationCoordinate2D = manager.location.coordinate
println("dinBack = \(locValue.latitude) \(locValue.longitude)")
self.locationManager.stopUpdatingLocation()
}
func handleTimer(){
println("started")
self.locationManager.startUpdatingLocation()
}
PS。 - 当然,我已经导入了核心定位。 - 当我回到应用程序时,控制台会打印本应在后台打印的内容。
当您的应用程序处于后台时,您无法使 NSTimer
像这样工作。 NSTimer
不是“实时机制”。来自 official documentation:
Timers work in conjunction with run loops. To use a timer effectively, you should be aware of how run loops operate—see NSRunLoop and Threading Programming Guide. Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs during a long callout or while the run loop is in a mode that is not monitoring the timer, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
强调我的。
重要的一点是,当您的应用程序在后台时,您的计时器本应安排在的任何 运行 循环都不会主动 运行ning。
一旦您的应用 returns 进入前台,此 运行 循环就会重新启动,发现您的计时器已过期,并向选择器发送消息。
使用 iOS 7 及更高版本,如果您想在后台执行操作,您可以告诉 OS 您想要执行“后台获取”。
要设置它,我们必须首先告诉 OS 我们想要获取数据的频率,因此在 didFinishLaunching...
中添加以下方法:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
application.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum)
return true
}
我们可以在这里传递任何时间间隔(例如,如果我们只想每天检查一次)。然而,我们传入的值仅定义了两次检查之间应该经过的最短时间。无法告诉 OS 检查之间的最长时间。
现在,我们必须实现在 OS 让我们有机会进行后台工作时实际调用的方法:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do background work
}
我们可以在此方法中做任何我们想做的事。然而,有两个问题。
- 当我们的应用程序在后台时调用此方法。 OS 将我们限制在(我相信)三十秒内。三十秒后,我们的时间到了。
- 我们必须调用
completionHandler()
(否则OS会认为我们用完了所有时间)。
传入的completionHandler
接受一个枚举,UIBackgroundFetchResult
。我们应该通过 .Failed
、.NewData
或 .NoData
,这取决于我们的实际结果(这种方法 通常 用于检查新数据的服务器)。
因此,我们的方法可能如下所示:
func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
// do stuff
if let _ = error {
completionHandler(.Failed)
} else if results.count > 0 {
completionHandler(.NewData)
} else {
completionHandler(.NoData)
}
}
请记住,我们绝对 零 控制 OS 实际让我们 运行 在后台执行此代码的频率。 OS 使用多个指标来优化用户体验。
我 认为 如果您的应用向完成处理程序报告 .Failed
,OS 可能很快会给您第二次机会,但是如果您滥用 .Failed
,OS 可能会将您的应用程序列入黑名单,禁止使用后台获取(Apple 可能 拒绝您的应用程序)。
如果您的应用未报告 .NewData
,OS 将让您的应用减少后台工作的频率。我这么说并不是因为我建议您始终报告 .NewData
。你绝对应该准确报告。 OS 在安排工作方面非常聪明。如果您在没有新数据时传递 .NewData
,OS 将使您的应用程序比它可能需要的更频繁地工作,这将更快地耗尽用户的电池(并可能导致他们完全卸载您的应用程序)。
但是,当您的应用程序开始执行后台工作时,还涉及其他指标。 OS 不太可能让任何应用程序在用户积极使用他们的设备时进行后台工作,而更有可能让应用程序在用户不使用他们的设备时进行后台工作。此外,OS 更有可能在连接到 WiFi 和插入某种充电器时进行后台工作。
OS 还会查看用户使用您的应用程序的频率,或者他们通常使用它的时间。如果用户每天下午 6 点使用您的应用程序,并且从不在任何其他时间使用您的应用程序,那么您的应用程序很可能总是有机会在 5:30pm 和下午 6 点之间(就在用户使用该应用程序之前)进行后台工作) 和 从不 在一天的任何其他时间。如果用户很少使用您的应用,则可能需要几天、几周或几个月才能有机会在后台工作。