正确使用 beginBackgroundTaskWithExpirationHandler

Posted

技术标签:

【中文标题】正确使用 beginBackgroundTaskWithExpirationHandler【英文标题】:Proper use of beginBackgroundTaskWithExpirationHandler 【发布时间】:2012-05-06 08:56:27 【问题描述】:

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

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

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

那么为了安全起见,用beginBackgroundTaskWithExpirationHandler 包装每个网络事务(我不是在谈论下载大量数据,主要是一些简短的 xml)是否被接受/良好做法?

【问题讨论】:

另见here 也见this 【参考方案1】:

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

【讨论】:

是的,我愿意...否则当应用程序进入后台时它们会停止。 我们需要在 applicationDidEnterBackground 中做些什么吗? 仅当您想将其用作开始网络操作的点时。如果您只想完成现有操作,根据@Eyal 的问题,您无需在 applicationDidEnterBackground 中执行任何操作 感谢这个清晰的例子! (只是将beingBackgroundUpdateTask改为beginBackgroundUpdateTask。) 如果你连续多次调用doUpdate而没有完成工作,你将覆盖self.backgroundUpdateTask,因此之前的任务无法正常结束。您应该每次都存储任务标识符以便正确结束它,或者在开始/结束方法中使用计数器。【参考方案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]];

        

    

【讨论】:

真的很喜欢这个解决方案。不过有一个问题:typedefCompletionBlock 你是怎么做的?很简单:typedef void (^CompletionBlock)(); 你明白了。 typedef void (^CompletionBlock)(void); @joel,谢谢,但是这个实现的源代码链接在哪里,即 BackGroundTaskManager ? 如上所述,“为简洁起见,排除了单一的东西”。 [BackgroundTaskManager sharedTasks] 返回一个单例。上面提供了单例的胆量。 赞成使用单例。我真的不认为他们像人们所说的那么糟糕!【参考方案3】:

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

【讨论】:

接受答案中的相同问题。过期处理程序不会取消实际任务,而只会将其标记为已结束。更多的过度封装导致我们自己无法做到这一点。这就是为什么 Apple 暴露了这个处理程序,所以这里的封装是错误的。 @ArielBogdziewicz 确实,这个答案没有为begin 方法中的额外清理提供机会,但很容易看出如何添加该功能。【参考方案4】:

我实施了 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

【讨论】:

谢谢。我的objective-c不是很好。你能添加一些代码来说明如何使用它吗? 你能举一个完整的例子来说明如何使用你的代码 非常好。谢谢。【参考方案5】:

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

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

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

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

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

操作队列可以(也应该)是后台队列;因此,无需担心在您的任务中执行异步代码,因为操作 异步代码。 (实际上,在操作中执行 另一个 级别的异步代码是没有意义的,因为操作会在该代码甚至可以开始之前完成。如果您需要这样做,您将使用另一个操作.)

这是一个可能的操作子类:

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

以上是关于正确使用 beginBackgroundTaskWithExpirationHandler的主要内容,如果未能解决你的问题,请参考以下文章

如何正确使用 Composer 安装 Laravel 扩展包

如何正确使用 Composer 安装 Laravel 扩展包

如何正确使用 Composer 安装 Laravel 扩展包

如何正确强制正确使用类方法?

如何正确使用 Composer 安装 Laravel 扩展包

C#注释的正确的使用方法有哪些?