Proper use of beginBackgroundTaskWithExpirationHandler
Я немного озадачен тем, как и когда использовать beginBackgroundTaskWithExpirationHandler
,
Apple показывает в своих примерах, чтобы использовать его в applicationDidEnterBackground
делегировать, чтобы получить больше времени для выполнения какой-либо важной задачи, обычно сетевой транзакции.
Когда я смотрю на мое приложение, кажется, что большая часть моей работы в сети важна, и когда она запускается, я хотел бы завершить ее, если пользователь нажал кнопку "Домой".
Так что принято / хорошая практика - оборачивать каждую сетевую транзакцию (и я не говорю о загрузке большого куска данных, это в основном какой-то короткий xml) с beginBackgroundTaskWithExpirationHandler
быть в безопасности?
5 ответов
Если вы хотите, чтобы ваша сетевая транзакция продолжалась в фоновом режиме, вам нужно будет обернуть ее в фоновую задачу. Также очень важно, чтобы вы позвонили 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)
}
Принятый ответ очень полезен и в большинстве случаев должен быть в порядке, однако меня беспокоит две вещи:
Как отметили многие люди, сохранение идентификатора задачи в качестве свойства означает, что он может быть перезаписан, если метод вызывается несколько раз, что приводит к задаче, которая никогда не будет корректно завершена до тех пор, пока ОС не завершит работу по истечении времени,
Этот шаблон требует уникального свойства для каждого вызова
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]];
}
}
}
Вот класс 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
}
}
Как отмечено здесь и в ответах на другие вопросы SO, вы НЕ хотите использовать beginBackgroundTask
только когда ваше приложение уйдет в фоновый режим; напротив, вы должны использовать фоновую задачу для любой трудоемкой операции, завершение которой вы хотите гарантировать, даже если приложение уходит в фоновый режим.
Поэтому ваш код, вероятно, будет в конечном итоге из-за повторения одного и того же стандартного кода для вызова beginBackgroundTask
а также endBackgroundTask
когерентно. Чтобы предотвратить это повторение, безусловно, разумно хотеть упаковать шаблон в какой-то один инкапсулированный объект.
Мне нравятся некоторые из существующих ответов для этого, но я думаю, что лучший способ - это использовать подкласс Operation:
Вы можете ставить Операцию в очередь в любой OperationQueue и манипулировать этой очередью по своему усмотрению. Например, вы можете досрочно отменить любые существующие операции в очереди.
Если вам нужно выполнить несколько задач, вы можете объединить несколько операций фоновой задачи. Операции поддерживают зависимости.
Очередь операций может (и должна) быть фоновой очередью; таким образом, вам не нужно беспокоиться о выполнении асинхронного кода внутри вашей задачи, потому что 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?()
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
свойство handler (не использовалось в предыдущих примерах). Некоторые другие ответы были раскритикованы за то, что они не были включены.
Я реализовал решение Джоэла. Вот полный код:
.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
(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:
&ошибка];
// 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; }
Спасибо, Эшли Миллс, это отлично работает для меня
Сначала прочтите документацию: https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio
Фоновая задача должна соответствовать следующим требованиям:
- Фоновая задача должна быть сообщена как можно скорее, но это не обязательно перед началом нашей реальной задачи. метод
beginBackgroundTaskWithExpirationHandler:
работает асинхронно, так что если он вызывается в концеapplicationDidEnterBackground:
тогда он не зарегистрирует фоновую задачу и немедленно вызовет обработчик срока действия. - Обработчик срока действия должен отменить нашу реальную задачу и пометить фоновую задачу как завершенную. Это заставляет нас хранить идентификатор фоновой задачи, хранящийся где-то, например, как атрибут некоторого класса. Это свойство должно находиться под нашим контролем, чтобы его нельзя было перезаписать.
- Обработчик истечения выполняется из основного потока, поэтому ваша реальная задача должна быть поточно-безопасной, если вы хотите отменить ее там.
- Наша настоящая задача должна быть отменена. Это означает, что наша реальная задача должна иметь метод
cancel
, В противном случае существует риск, что он будет прерван непредсказуемым образом, даже если мы отметим фоновую задачу как завершенную. - Код, содержащий
beginBackgroundTaskWithExpirationHandler:
можно вызывать везде и в любой теме. Это не должен быть метод делегата приложенияapplicationDidEnterBackground:
, - Нет смысла делать это для синхронных операций короче 5 секунд в случае метода
applicationDidEnterBackground:
(пожалуйста, прочтите документ https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground?language=objc) - метод
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()
}