正确使用beginBackgroundTaskWithExpirationHandler


107

我对如何使用和何时使用感到困惑beginBackgroundTaskWithExpirationHandler

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

在我的应用程序上浏览时,似乎我的大多数网络内容都很重要,并且当我启动某个应用程序时,如果用户按下主页按钮,我想完成它。

因此,包装每一个网络事务(并且我不是在谈论下载大块数据,主要是一些短xml)beginBackgroundTaskWithExpirationHandler是一种安全做法 吗?


另请参阅此处
亲爱的人

Answers:


165

如果您希望网络事务在后台继续进行,则需要将其包装在后台任务中。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: 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)
}

1
是的,我愿意...否则当应用程序进入后台时它们会停止。
Ashley Mills 2012年

1
我们需要在applicationDidEnterBackground中做任何事情吗?
2012年

1
仅当您希望以此为起点来开始网络操作时。如果您只想完成一个现有操作(按照@Eyal的问题),则无需在applicationDidEnterBackground中做任何事情
Ashley Mills

2
感谢您给出的明确示例!(只需将beingingBackgroundUpdateTask更改为beginBackgroundUpdateTask。)
newenglander 2013年

30
如果您在未完成工作的情况下连续多次调用doUpdate,则将覆盖self.backgroundUpdateTask,这样以前的任务将无法正确结束。您应该每次都存储任务标识符以便正确结束它,或者在begin / end方法中使用计数器。
thejaz

23

可接受的答案非常有帮助,并且在大多数情况下应该很好,但是有两件事困扰着我:

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

  2. 此模式要求每个调用都具有唯一的属性,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]];

        }

    }
}

1
非常喜欢这种解决方案。但是,一个问题是:您如何/作为typedefCompletionBlock?简而言之:typedef void (^CompletionBlock)();
约瑟夫

你说对了。typedef void(^ CompletionBlock)(void);
乔尔

@joel,谢谢,但是此实现的源代码链接在哪里,即BackGroundTaskManager?
Özgür的

如上所述,“为简洁起见,不包含单个东西”。[BackgroundTaskManager sharedTasks]返回单例。上面提供了单例的胆量。
乔尔

赞成使用单例。我真的不认为它们像人们发现的那样糟糕!
克雷格·沃特金森

20

这是一个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
    }
}

最简单的使用方式:

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

像接受的答案一样的问题。到期处理程序不会取消实际任务,而只会将其标记为已结束。更多的封装导致我们无法自己做到这一点。这就是Apple公开此处理程序的原因,因此此处的封装是错误的。
Ariel Bogdziewicz

@ArielBogdziewicz的确,此答案没有提供进一步清除begin方法的机会,但是很容易看到如何添加该功能。
马特

6

如此处所述以及在其他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?()
            self.cancel()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        guard !self.isCancelled else { return }
        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处理程序属性(在前面的示例中未使用)。有人批评其他答案,因为其中不包括这一点。


我现在将其作为github项目提供:github.com/mattneub/BackgroundTaskOperation
matt

1

我实现了乔尔的解决方案。这是完整的代码:

.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

1
谢谢你 我的Objective-C不好。您可以添加一些代码来显示如何使用它吗?
pomo

能否请您给出有关如何使用您的代码的完整示例
Amr Angry

非常好。谢谢。
Alyoshak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.