Правильне використання BeginBackgroundTaskWithExpirationHandler


107

Я трохи розгублений у тому, як і коли використовувати beginBackgroundTaskWithExpirationHandler.

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

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

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


Також дивіться тут
Мед

Відповіді:


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властивість для кожного фонового завдання


Еквівалентний код у Свіфті

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, вам не потрібно нічого робити в програміDidEnterBackground
Ешлі Міллз

2
Дякуємо за цей зрозумілий приклад! (Тільки що змінився, будучиBackgroundUpdateTask, щоб розпочатиBackgroundUpdateTask.)
newenglander

30
Якщо ви зателефонуєте doUpdate кілька разів поспіль, не виконавши роботу, ви перезапишете self.backgroundUpdateTask, щоб попередні завдання не змогли закінчитися належним чином. Вам слід або зберігати ідентифікатор завдання кожен раз, щоб закінчити його належним чином, або використовувати лічильник у методах початку / кінця.
thejaz

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) (void);
Джоель

@joel, спасибі, але де посилання вихідного коду для цієї реалізації, я, e, BackGroundTaskManager?
Özgür

Як зазначалося вище, "одиночні речі виключені для стислості". [BackgroundTaskManager sharedTasks] повертає сингл. Кишки одинарних наведені вище.
Джоель

Запропоновано використовувати одиночну кнопку. Я насправді не думаю, що вони такі погані, як люди з них!
Крейг Уоткінсон

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
    } 
}

Та ж проблема, що і у прийнятій відповіді. Обробник терміну дії не скасовує справжнє завдання, а лише позначає його як закінчене. Більше, ніж інкапсуляція призводить до того, що ми не в змозі це зробити самі. Ось чому Apple викрила цей обробник, тому інкапсуляція тут неправильна.
Аріель Богдзевич

@ArielBogdziewicz Це правда, що ця відповідь не дає можливості додаткового очищення beginметоду, але легко зрозуміти, як додати цю функцію.
мат

6

Як зазначено тут та у відповідях на інші запитання щодо ТА, ви НЕ хочете використовувати їх beginBackgroundTaskлише тоді, коли ваш додаток відійде на другий план; навпаки, ви повинні використовувати фонову завдання для будь-якого трудомісткою операції поповнення якого ви хочете переконатися , навіть якщо додаток дійсно йти в фоновому режимі.

Тому ваш код, ймовірно, закінчиться повтореннями одного й того самого кодового коду для виклику beginBackgroundTaskта endBackgroundTaskузгоджено. Для запобігання цього повторення, безумовно, розумно хочете упакувати плиту котла в якесь одне капсульоване ціле.

Мені подобаються деякі з існуючих відповідей для цього, але я думаю, що найкращим способом є використання підкласу Операція:

  • Ви можете задіяти Операцію на будь-якій черзі операцій та маніпулювати цією чергою, як вважаєте за потрібне. Наприклад, ви можете передчасно скасувати будь-які існуючі операції в черзі.

  • Якщо у вас є кілька справ, ви можете зв'язати кілька операцій із фоновим завданням. Операції підтримують залежності.

  • Черга операцій може (і повинна) бути фоновою чергою; таким чином, не потрібно турбуватися про виконання асинхронного коду всередині вашого завдання, оскільки Операція - це асинхронний код. (Дійсно, не має сенсу виконувати інший рівень асинхронного коду всередині операції, оскільки Операція закінчиться до того, як цей код навіть міг би розпочатися. Якщо вам потрібно було це зробити, ви використовуєте іншу операцію.)

Ось можливий підклас Операція:

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
    }
}

Повинно бути очевидним, як це використовувати, але, якщо це не так, уявіть, що у нас є глобальна операційна черга:

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
Дякую за це Моя ціль-с не велика. Чи можете ви додати якийсь код, який показує, як його використовувати?
pomo

ви можете, будь ласка, надати повний приклад того, як використовувати ур-код
Amr Angry

Дуже хороша. Дякую.
Альошак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.