我对如何以及何时使用beginBackgroundTaskWithExpirationHandler
感到有些困惑。
Apple在他们的示例中显示要在applicationDidEnterBackground
委托中使用它,以便有更多时间来完成一些重要任务,通常是网络事务。
在查看我的应用程序时,似乎我的大多数网络内容都很重要,当一个人启动时,如果用户按下主页按钮,我想完成它。
因此,包含每个网络事务(并且我不是在谈论下载大块数据,大多数是一些短xml)以及beginBackgroundTaskWithExpirationHandler
安全方面是否接受/良好实践?
答案 0 :(得分:153)
如果您希望您的网络事务在后台继续,那么您需要将其包装在后台任务中。完成后调用endBackgroundTask
也非常重要 - 否则应用程序将在其分配的时间到期后被杀死。
- (void) doUpdate
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundUpdateTask];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * responseData = [NSURLConnection sendSynchronousRequest: request returningResponse: &response error: &error];
// Do something with the result
[self endBackgroundUpdateTask];
});
}
- (void) beginBackgroundUpdateTask
{
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}
- (void) endBackgroundUpdateTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
我为每个后台任务设置了UIBackgroundTaskIdentifier
属性
Swift中的等效代码
func doUpdate () {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = beginBackgroundUpdateTask()
var response: NSURLResponse?, error: NSError?, request: NSURLRequest?
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
// Do something with the result
endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
答案 1 :(得分:20)
接受的答案非常有用,在大多数情况下应该没问题,但有两件事让我感到困扰:
正如许多人所指出的那样,将任务标识符存储为属性意味着如果多次调用该方法可以覆盖它,从而导致任务永远不会优雅地结束,直到被强制结束为止操作系统在到期时。
对于每次调用beginBackgroundTaskWithExpirationHandler
,此模式都需要一个唯一的属性,如果你有一个包含大量网络方法的更大的应用程序,这似乎很麻烦。
为了解决这些问题,我编写了一个单例来处理所有管道并跟踪字典中的活动任务。无需跟踪任务标识符所需的属性。似乎运作良好。用法简化为:
//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];
//do stuff
//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];
可选地,如果你想提供一个完成块,它可以完成任务(内置任务),你可以调用:
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
//do stuff
}];
下面提供了相关的源代码(为简洁起见,排除了单例内容)。评论/反馈欢迎。
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
@synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
@synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
@synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
}
答案 2 :(得分:17)
这是一个封装运行后台任务的Swift class:
class BackgroundTask {
private let application: UIApplication
private var identifier = UIBackgroundTaskInvalid
init(application: UIApplication) {
self.application = application
}
class func run(application: UIApplication, handler: (BackgroundTask) -> ()) {
// NOTE: The handler must call end() when it is done
let backgroundTask = BackgroundTask(application: application)
backgroundTask.begin()
handler(backgroundTask)
}
func begin() {
self.identifier = application.beginBackgroundTaskWithExpirationHandler {
self.end()
}
}
func end() {
if (identifier != UIBackgroundTaskInvalid) {
application.endBackgroundTask(identifier)
}
identifier = UIBackgroundTaskInvalid
}
}
使用它的最简单方法:
BackgroundTask.run(application) { backgroundTask in
// Do something
backgroundTask.end()
}
如果您需要在结束前等待委托回调,请使用以下内容:
class MyClass {
backgroundTask: BackgroundTask?
func doSomething() {
backgroundTask = BackgroundTask(application)
backgroundTask!.begin()
// Do something that waits for callback
}
func callback() {
backgroundTask?.end()
backgroundTask = nil
}
}
答案 3 :(得分:1)
我实施了Joel的解决方案。这是完整的代码:
.h文件:
#import <Foundation/Foundation.h>
@interface VMKBackgroundTaskManager : NSObject
+ (id) sharedTasks;
- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;
@end
.m文件:
#import "VMKBackgroundTaskManager.h"
@interface VMKBackgroundTaskManager()
@property NSUInteger taskKeyCounter;
@property NSMutableDictionary *dictTaskIdentifiers;
@property NSMutableDictionary *dictTaskCompletionBlocks;
@end
@implementation VMKBackgroundTaskManager
+ (id)sharedTasks {
static VMKBackgroundTaskManager *sharedTasks = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedTasks = [[self alloc] init];
});
return sharedTasks;
}
- (id)init
{
self = [super init];
if (self) {
[self setTaskKeyCounter:0];
[self setDictTaskIdentifiers:[NSMutableDictionary dictionary]];
[self setDictTaskCompletionBlocks:[NSMutableDictionary dictionary]];
}
return self;
}
- (NSUInteger)beginTask
{
return [self beginTaskWithCompletionHandler:nil];
}
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
{
//read the counter and increment it
NSUInteger taskKey;
@synchronized(self) {
taskKey = self.taskKeyCounter;
self.taskKeyCounter++;
}
//tell the OS to start a task that should continue in the background if needed
NSUInteger taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endTaskWithKey:taskKey];
}];
//add this task identifier to the active task dictionary
[self.dictTaskIdentifiers setObject:[NSNumber numberWithUnsignedLong:taskId] forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//store the completion block (if any)
if (_completion) [self.dictTaskCompletionBlocks setObject:_completion forKey:[NSNumber numberWithUnsignedLong:taskKey]];
//return the dictionary key
return taskKey;
}
- (void)endTaskWithKey:(NSUInteger)_key
{
@synchronized(self.dictTaskCompletionBlocks) {
//see if this task has a completion block
CompletionBlock completion = [self.dictTaskCompletionBlocks objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (completion) {
//run the completion block and remove it from the completion block dictionary
completion();
[self.dictTaskCompletionBlocks removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
}
}
@synchronized(self.dictTaskIdentifiers) {
//see if this task has been ended yet
NSNumber *taskId = [self.dictTaskIdentifiers objectForKey:[NSNumber numberWithUnsignedLong:_key]];
if (taskId) {
//end the task and remove it from the active task dictionary
[[UIApplication sharedApplication] endBackgroundTask:[taskId unsignedLongValue]];
[self.dictTaskIdentifiers removeObjectForKey:[NSNumber numberWithUnsignedLong:_key]];
NSLog(@"Task ended");
}
}
}
@end
答案 4 :(得分:1)
如此处所述以及在回答其他SO问题时,您不想仅在应用程序进入后台时才使用beginBackgroundTask
;相反,您应该对任何耗时的操作使用后台任务,即使应用程序确实进入后台,您也要确保其完成性。
因此,您的代码可能最终会重复使用相同的样板代码,以连贯地调用beginBackgroundTask
和endBackgroundTask
。为了防止这种重复,将样板包装成单个封装的实体当然是合理的。
我喜欢这样做的一些现有答案,但是我认为最好的方法是使用Operation子类:
您可以将Operation排队到任何OperationQueue上,并根据需要操纵该队列。例如,您可以自由地提前取消队列上的任何现有操作。
如果您要做的事情不只一件事,则可以链接多个后台任务操作。操作支持依赖性。
操作队列可以(并且应该)是后台队列;因此,无需担心在任务内部执行异步代码,因为操作是异步代码。 (实际上,在一个操作中执行另一个级别的异步代码是没有意义的,因为该操作将在该代码甚至无法启动之前完成。如果需要执行此操作,则可以使用另一个操作。)
这是一个可能的Operation子类:
class BackgroundTaskOperation: Operation {
var whatToDo : (() -> ())?
var cleanup : (() -> ())?
override func main() {
guard !self.isCancelled else { return }
guard let whatToDo = self.whatToDo else { return }
var bti : UIBackgroundTaskIdentifier = .invalid
bti = UIApplication.shared.beginBackgroundTask {
self.cleanup?()
UIApplication.shared.endBackgroundTask(bti) // cancellation
}
guard bti != .invalid else { return }
whatToDo()
UIApplication.shared.endBackgroundTask(bti) // completion
}
}
如何使用它应该很明显,但是如果没有,请想象我们有一个全局的OperationQueue:
let backgroundTaskQueue : OperationQueue = {
let q = OperationQueue()
q.maxConcurrentOperationCount = 1
return q
}()
因此对于典型的耗时的一批代码,我们会说:
let task = BackgroundTaskOperation()
task.whatToDo = {
// do something here
}
backgroundTaskQueue.addOperation(task)
如果可以将您耗时的代码批次划分为多个阶段,那么如果您的任务被取消,则可能希望提早退出。在这种情况下,只需从闭包中过早返回即可。请注意,您在闭包内部对任务的引用需要比较弱,否则您将获得保留周期。这是一个人工插图:
let task = BackgroundTaskOperation()
task.whatToDo = { [weak task] in
guard let task = task else {return}
for i in 1...10000 {
guard !task.isCancelled else {return}
for j in 1...150000 {
let k = i*j
}
}
}
backgroundTaskQueue.addOperation(task)
万一您有清理工作,以防后台任务本身被过早取消,我提供了一个可选的cleanup
处理程序属性(在前面的示例中未使用)。有人批评其他一些答案,因为其中不包括这一点。
答案 5 :(得分:-1)
背景任务应满足以下要求:
beginBackgroundTaskWithExpirationHandler:
是异步工作的,因此,如果在applicationDidEnterBackground:
的末尾调用该方法,则它不会注册后台任务,而是会立即调用到期处理程序。cancel
。否则,即使我们将后台任务标记为已结束,也有可能以无法预测的方式终止它。beginBackgroundTaskWithExpirationHandler:
的代码可以在任何线程上的任何地方调用。不必是应用程序委托applicationDidEnterBackground:
的方法。applicationDidEnterBackground:
(请阅读doc https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground?language=objc),对于少于5秒的同步操作,则没有意义applicationDidEnterBackground
必须在不到5秒的时间内执行,因此所有后台任务都应在第二个线程上启动。示例:
class MySpecificBackgroundTask: NSObject, URLSessionDataDelegate {
// MARK: - Properties
let application: UIApplication
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier
var task: URLSessionDataTask? = nil
// MARK: - Initializers
init(application: UIApplication) {
self.application = application
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
}
// MARK: - Actions
func start() {
self.backgroundTaskIdentifier = self.application.beginBackgroundTask {
self.cancel()
}
self.startUrlRequest()
}
func cancel() {
self.task?.cancel()
self.end()
}
private func end() {
self.application.endBackgroundTask(self.backgroundTaskIdentifier)
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid
}
// MARK: - URLSession methods
private func startUrlRequest() {
let sessionConfig = URLSessionConfiguration.background(withIdentifier: "MySpecificBackgroundTaskId")
let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
guard let url = URL(string: "https://example.com/api/my/path") else {
self.end()
return
}
let request = URLRequest(url: url)
self.task = session.dataTask(with: request)
self.task?.resume()
}
// MARK: - URLSessionDataDelegate methods
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.end()
}
// Implement other methods of URLSessionDataDelegate to handle response...
}
它可以在我们的应用程序委托中使用:
func applicationDidEnterBackground(_ application: UIApplication) {
let myBackgroundTask = MySpecificBackgroundTask(application: application)
myBackgroundTask.start()
}