正确使用beginBackgroundTaskWithExpirationHandler

Eya*_*yal 99 network-programming objective-c multitasking ios background-thread

我对如何以及何时使用感到困惑beginBackgroundTaskWithExpirationHandler.

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

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

因此,beginBackgroundTaskWithExpirationHandler为了安全起见,包装每个网络事务(并且我不是在谈论下载大量数据,大多数是短xml)是接受/良好做法 吗?

Ash*_*lls 156

如果您希望网络事务在后台继续,那么您需要将其包装在后台任务中.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;
}
Run Code Online (Sandbox Code Playgroud)

UIBackgroundTaskIdentifier为每个后台任务都有一个属性


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)
}
Run Code Online (Sandbox Code Playgroud)

  • 如果连续多次调用doUpdate而没有完成工作,则会覆盖self.backgroundUpdateTask,因此以前的任务无法正常结束.您应该每次都存储任务标识符,以便正确结束或在开始/结束方法中使用计数器. (29认同)
  • 谢谢你这个明显​​的例子!(刚刚将beingBackgroundUpdateTask更改为beginBackgroundUpdateTask.) (2认同)

Joe*_*oel 20

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

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

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

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

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

//do stuff

//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];
Run Code Online (Sandbox Code Playgroud)

或者,如果您想提供一个完成块,它可以执行超出任务(内置任务)的操作,您可以调用:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
    //do stuff
}];
Run Code Online (Sandbox Code Playgroud)

下面提供了相关的源代码(为简洁起见,排除了单例内容).评论/反馈欢迎.

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

        }

    }
}
Run Code Online (Sandbox Code Playgroud)


pha*_*ann 19

这是一个封装运行后台任务的Swift类:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

最简单的使用方法:

BackgroundTask.run(application) { backgroundTask in
   // Do something
   backgroundTask.end()
}
Run Code Online (Sandbox Code Playgroud)

如果您需要在结束前等待委托回调,请使用以下内容:

class MyClass {
    backgroundTask: BackgroundTask?

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

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
Run Code Online (Sandbox Code Playgroud)


mat*_*att 7

正如此处以及其他 SO 问题的答案中所指出的,您不想beginBackgroundTask仅在您的应用程序进入后台时才使用;相反,您应该为任何耗时的操作使用后台任务,即使应用程序确实进入后台,您也希望确保其完成。

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

我喜欢这样做的一些现有答案,但我认为最好的方法是使用 Operation 子类:

  • 您可以将 Operation 排入任何 OperationQueue 并按照您认为合适的方式操作该队列。例如,您可以提前取消队列上的任何现有操作。

  • 如果你有不止一件事情要做,你可以链接多个后台任务操作。操作支持依赖。

  • 操作队列可以(也应该)是后台队列;因此,无需担心在您的任务中执行异步代码,因为 Operation异步代码。(实际上,在 Operation 中执行另一层异步代码是没有意义的,因为 Operation 甚至会在该代码开始之前完成。如果需要这样做,您将使用另一个 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?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        UIApplication.shared.endBackgroundTask(bti) // completion
    }
}
Run Code Online (Sandbox Code Playgroud)

如何使用它应该是显而易见的,但如果不是,请想象我们有一个全局 OperationQueue:

let backgroundTaskQueue : OperationQueue = {
    let q = OperationQueue()
    q.maxConcurrentOperationCount = 1
    return q
}()
Run Code Online (Sandbox Code Playgroud)

因此,对于典型的耗时的一批代码,我们会说:

let task = BackgroundTaskOperation()
task.whatToDo = {
    // do something here
}
backgroundTaskQueue.addOperation(task)
Run Code Online (Sandbox Code Playgroud)

如果您的一批耗时的代码可以分为多个阶段,那么如果您的任务被取消,您可能希望尽早退出。在这种情况下,只需从关闭中过早返回即可。请注意,您在闭包中对任务的引用需要弱,否则您将获得一个保留周期。这是一个人工插图:

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)
Run Code Online (Sandbox Code Playgroud)

如果您有清理工作要做,以防后台任务本身过早取消,我提供了一个可选的cleanup处理程序属性(未在前面的示例中使用)。其他一些答案因不包括这一点而受到批评。