正确使用 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]];
【讨论】:
真的很喜欢这个解决方案。不过有一个问题:typedef
CompletionBlock 你是怎么做的?很简单: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
;相反,您应该为任何耗时的操作使用后台任务,即使应用程序确实进入后台,您也希望确保其完成。
因此,您的代码最终可能会重复使用相同的样板代码,以便连贯地调用 beginBackgroundTask
和 endBackgroundTask
。为了防止这种重复,将样板文件打包到某个单独的封装实体中当然是合理的。
我喜欢这样做的一些现有答案,但我认为最好的方法是使用 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 扩展包