Правильное использование beginBackgroundTaskWithExpirationHandler

Я немного смущен о том, как и когда использовать beginBackgroundTaskWithExpirationHandler.

Apple показывает в своих примерах использовать его в делегате applicationDidEnterBackground, чтобы получить больше времени для выполнения важной задачи, обычно это сетевая транзакция.

При просмотре моего приложения кажется, что большая часть моего сетевого материала важна, и когда он запускается, я хотел бы заполнить его, если пользователь нажал кнопку "домой".

Так ли принято/хорошая практика обертывать каждую сетевую транзакцию (и я не говорю о загрузке большого фрагмента данных, в основном из небольшого xml) с beginBackgroundTaskWithExpirationHandler, чтобы быть в безопасности?

Ответ 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)
}

Ответ 2

Принятый ответ очень полезен, и в большинстве случаев это должно быть хорошо, однако мне все это беспокоило меня:

  • Как заметил ряд людей, сохранение идентификатора задачи в качестве свойства означает, что его можно перезаписать, если метод вызывается несколько раз, что приводит к задаче, которая никогда не будет законно закончена, пока не будет принудительно завершена OS в момент истечения срока действия.

  • Этот шаблон требует уникального свойства для каждого вызова 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]];

        }

    }
}

Ответ 3

Вот класс 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
    } 
}

Ответ 4

Я реализовал решение Joel. Вот полный код:

.h файл:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

- (NSUInteger)beginTask;
- (NSUInteger)beginTaskWithCompletionHandler:(CompletionBlock)_completion;
- (void)endTaskWithKey:(NSUInteger)_key;

@end

.m file:

#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

Ответ 5

Как отмечено здесь и в ответах на другие вопросы SO, вы НЕ хотите использовать beginBackgroundTask только тогда, когда ваше приложение перейдет в фоновый режим; напротив, вы должны использовать фоновую задачу для любой трудоемкой операции, завершение которой вы хотите гарантировать, даже если приложение уходит в фоновый режим.

Поэтому ваш код, вероятно, будет в конечном итоге изобиловать повторениями одного и того же стандартного кода для beginBackgroundTask вызова beginBackgroundTask и endBackgroundTask. Чтобы предотвратить это повторение, безусловно, разумно хотеть упаковать шаблон в какой-то один инкапсулированный объект.

Мне нравятся некоторые из существующих ответов для этого, но я думаю, что лучший способ - это использовать подкласс 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?()
            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 (не использовалось в предыдущих примерах). Некоторые другие ответы были раскритикованы за то, что они не были включены.

Ответ 6

  • (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 { [[UAppication sharedApplication] endBackgroundTask: self.backgroundUpdateTask]; self.backgroundUpdateTask = UIBackgroundTaskInvalid; }

Спасибо Эшли Миллс, он отлично работает для меня

Ответ 7

Сначала прочтите документацию: 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()
}