正确使用beginBackgroundTaskWithExpirationHandler

时间:2012-04-25 16:16:11

标签: ios objective-c network-programming multitasking background-thread

我对如何以及何时使用beginBackgroundTaskWithExpirationHandler感到有些困惑。

Apple在他们的示例中显示要在applicationDidEnterBackground委托中使用它,以便有更多时间来完成一些重要任务,通常是网络事务。

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

因此,包含每个网络事务(并且我不是在谈论下载大块数据,大多数是一些短xml)以及beginBackgroundTaskWithExpirationHandler安全方面是否接受/良好实践?

6 个答案:

答案 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)

接受的答案非常有用,在大多数情况下应该没问题,但有两件事让我感到困扰:

  1. 正如许多人所指出的那样,将任务标识符存储为属性意味着如果多次调用该方法可以覆盖它,从而导致任务永远不会优雅地结束,直到被强制结束为止操作系统在到期时。

  2. 对于每次调用beginBackgroundTaskWithExpirationHandler,此模式都需要一个唯一的属性,如果你有一个包含大量网络方法的更大的应用程序,这似乎很麻烦。

  3. 为了解决这些问题,我编写了一个单例来处理所有管道并跟踪字典中的活动任务。无需跟踪任务标识符所需的属性。似乎运作良好。用法简化为:

    //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;相反,您应该对任何耗时的操作使用后台任务,即使应用程序确实进入后台,您也要确保其完成性。

因此,您的代码可能最终会重复使用相同的样板代码,以连贯地调用beginBackgroundTaskendBackgroundTask。为了防止这种重复,将样板包装成单个封装的实体当然是合理的。

我喜欢这样做的一些现有答案,但是我认为最好的方法是使用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)

首先请阅读文档: https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio

背景任务应满足以下要求:

  • 背景任务应尽快报告,但不必在实际任务开始之前报告。方法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()
}