如何正确使用 startBackoundTaskWithExpirationHandler

对于如何以及何时使用 beginBackgroundTaskWithExpirationHandler,我有点困惑。

苹果在他们的示例中展示了如何使用它在 applicationDidEnterBackground中委托,以获得更多的时间来完成一些重要的任务,通常是网络交易。

当查看我的应用程序时,似乎我的大部分网络内容都很重要,当一个应用程序启动时,如果用户按下主页按钮,我想完成它。

那么,使用 beginBackgroundTaskWithExpirationHandler来包装每个网络事务(我并不是说要下载大块数据,它大多是一些简短的 xml)是否可以接受/好的做法?

57104 次浏览

If you want your network transaction to continue in the background, then you'll need to wrap it in a background task. It's also very important that you call endBackgroundTask when you're finished - otherwise the app will be killed after its allotted time has expired.

Mine tend look something like this:

- (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;
}

I have a UIBackgroundTaskIdentifier property for each background task


Equivalent code in Swift

func doUpdate () {


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {


let taskID = beginBackgroundUpdateTask()


var response: URLResponse?, 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.shared.beginBackgroundTask(expirationHandler: ({}))
}


func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}

The accepted answer is very helpful and should be fine in most cases, however two things bothered me about it:

  1. As a number of people have noted, storing the task identifier as a property means that it can be overwritten if the method is called multiple times, leading to a task that will never be gracefully ended until forced to end by the OS at the time expiration.

  2. This pattern requires a unique property for every call to beginBackgroundTaskWithExpirationHandler which seems cumbersome if you have a larger app with lots of network methods.

To solve these issues, I wrote a singleton that takes care of all the plumbing and tracks active tasks in a dictionary. No properties needed to keep track of task identifiers. Seems to work well. Usage is simplified to:

//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];


//do stuff


//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

Optionally, if you want to provide a completion block that does something beyond ending the task (which is built in) you can call:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
//do stuff
}];

Relevant source code available below (singleton stuff excluded for brevity). Comments/feedback welcome.

- (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]];


}


}
}

Here is a Swift class that encapsulates running a background task:

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
}
}

The simplest way to use it:

BackgroundTask.run(application) { backgroundTask in
// Do something
backgroundTask.end()
}

If you need to wait for a delegate callback before you end, then use something like this:

class MyClass {
backgroundTask: BackgroundTask?


func doSomething() {
backgroundTask = BackgroundTask(application)
backgroundTask!.begin()
// Do something that waits for callback
}


func callback() {
backgroundTask?.end()
backgroundTask = nil
}
}

I implemented Joel's solution. Here is the complete code:

.h file:

#import <Foundation/Foundation.h>


@interface VMKBackgroundTaskManager : NSObject


+ (id) sharedTasks;


- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;


@end

.m file:

#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

As noted here and in answers to other SO questions, you do NOT want to use beginBackgroundTask only just when your app will go into the background; on the contrary, you should use a background task for any time-consuming operation whose completion you want to ensure even if the app does go into the background.

Therefore your code is likely to end up peppered with repetitions of the same boilerplate code for calling beginBackgroundTask and endBackgroundTask coherently. To prevent this repetition, it is certainly reasonable to want to package up the boilerplate into some single encapsulated entity.

I like some of the existing answers for doing that, but I think the best way is to use an Operation subclass:

  • You can enqueue the Operation onto any OperationQueue and manipulate that queue as you see fit. For example, you are free to cancel prematurely any existing operations on the queue.

  • If you have more than one thing to do, you can chain multiple background task Operations. Operations support dependencies.

  • The Operation Queue can (and should) be a background queue; thus, there is no need to worry about performing asynchronous code inside your task, because the Operation is the asynchronous code. (Indeed, it makes no sense to execute another level of asynchronous code inside an Operation, as the Operation would finish before that code could even start. If you needed to do that, you'd use another Operation.)

Here's a possible Operation subclass:

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?()
self.cancel()
UIApplication.shared.endBackgroundTask(bti) // cancellation
}
guard bti != .invalid else { return }
whatToDo()
guard !self.isCancelled else { return }
UIApplication.shared.endBackgroundTask(bti) // completion
}
}

It should be obvious how to use this, but in case it isn't, imagine we have a global OperationQueue:

let backgroundTaskQueue : OperationQueue = {
let q = OperationQueue()
q.maxConcurrentOperationCount = 1
return q
}()

So for a typical time-consuming batch of code we would say:

let task = BackgroundTaskOperation()
task.whatToDo = {
// do something here
}
backgroundTaskQueue.addOperation(task)

If your time-consuming batch of code can be divided into stages, you might want to bow out early if your task is cancelled. In that case, just return prematurely from the closure. Note that your reference to the task from within the closure needs to be weak or you'll get a retain cycle. Here's an artificial illustration:

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)

In case you have cleanup to do in case the background task itself is cancelled prematurely, I've provided an optional cleanup handler property (not used in the preceding examples). Some other answers were criticised for not including that.