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

107

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

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

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

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

Эяль
источник
Также смотрите здесь
Honey

Ответы:

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
Да, я ... в противном случае они останавливаются, когда приложение переходит в фоновый режим.
Эшли Миллс
1
нам нужно что-нибудь делать в applicationDidEnterBackground?
проваливается
1
Только если вы хотите использовать это как точку для запуска сетевой операции. Если вы просто хотите завершить существующую операцию, в соответствии с вопросом @Eyal, вам не нужно ничего делать в applicationDidEnterBackground
Эшли Миллс,
2
Спасибо за этот наглядный пример! (Просто поменял beBackgroundUpdateTask на beginBackgroundUpdateTask.)
newenglander
30
Если вы вызываете doUpdate несколько раз подряд, не выполняя никаких действий, вы перезапишете self.backgroundUpdateTask, чтобы предыдущие задачи не могли быть завершены должным образом. Вы должны либо сохранять идентификатор задачи каждый раз, чтобы правильно завершить ее, либо использовать счетчик в методах начала / конца.
thejaz 01
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) (недействительно);
Joel
@joel, спасибо, а где ссылка на исходный код этой реализации, то есть BackGroundTaskManager?
Özgür
Как отмечалось выше, «одноэлементный материал исключен для краткости». [BackgroundTaskManager sharedTasks] возвращает одноэлемент. Внутренности синглтона представлены выше.
Joel
Проголосовали за использование синглтона. Я действительно не думаю, что они такие плохие, как думают люди!
Крейг Уоткинсон,
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
    } 
}
Phatmann
источник
Та же проблема, что и в принятом ответе. Обработчик истечения срока не отменяет реальную задачу, а только отмечает ее как завершенную. Более того, инкапсуляция приводит к тому, что мы не можем сделать это сами. Вот почему Apple предоставила этот обработчик, поэтому инкапсуляция здесь неверна.
Ариэль Богдзевич
@ArielBogdziewicz Верно, что этот ответ не дает возможности для дополнительной очистки beginметода, но легко увидеть, как добавить эту функцию.
матовый
6

Как отмечено здесь и в ответах на другие вопросы SO, вы НЕ хотите использовать 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?()
            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
матовый
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
Спасибо за это. Моя цель-c невелика. Не могли бы вы добавить код, показывающий, как его использовать?
pomo
не могли бы вы привести полный пример того, как использовать ваш код?
Amr Angry
Очень хорошо. Спасибо.
Алешак