Prawidłowe użycie beginBackgroundTaskWithExpirationHandler

107

Nie wiem, jak i kiedy używać beginBackgroundTaskWithExpirationHandler.

Apple pokazuje na swoich przykładach, aby użyć go jako applicationDidEnterBackgrounddelegata, aby uzyskać więcej czasu na wykonanie ważnego zadania, zwykle transakcji sieciowej.

Patrząc na moją aplikację, wydaje się, że większość rzeczy w mojej sieci jest ważna, a kiedy ktoś jest uruchomiony, chciałbym je zakończyć, jeśli użytkownik nacisnął przycisk home.

Czy zatem jest akceptowaną / dobrą praktyką zawijanie każdej transakcji sieciowej (i nie mówię o pobieraniu dużych porcji danych, głównie krótkiego pliku xml), beginBackgroundTaskWithExpirationHandleraby być po bezpiecznej stronie?

Eyal
źródło
Zobacz także tutaj
Honey

Odpowiedzi:

165

Jeśli chcesz, aby transakcja sieciowa była kontynuowana w tle, musisz ją opakować w zadanie w tle. Bardzo ważne jest również, aby zadzwonić endBackgroundTaskpo zakończeniu - w przeciwnym razie aplikacja zostanie zabita po upływie wyznaczonego czasu.

Mój zwykle wygląda mniej więcej tak:

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

Mam UIBackgroundTaskIdentifierwłaściwość dla każdego zadania w tle


Równoważny kod w 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)
}
Ashley Mills
źródło
1
Tak, robię ... w przeciwnym razie zatrzymają się, gdy aplikacja przejdzie w tło.
Ashley Mills,
1
czy musimy coś zrobić w applicationDidEnterBackground?
spadki
1
Tylko jeśli chcesz użyć tego jako punktu do rozpoczęcia operacji w sieci. Jeśli chcesz po prostu zakończyć istniejącą operację, zgodnie z pytaniem @ Eyal, nie musisz nic robić w applicationDidEnterBackground
Ashley Mills
2
Dzięki za ten jasny przykład! (Właśnie zmieniono beingBackgroundUpdateTask na beginBackgroundUpdateTask.)
newenglander
30
Jeśli wywołasz doUpdate wiele razy z rzędu bez wykonania pracy, nadpiszesz self.backgroundUpdateTask, aby poprzednie zadania nie mogły zostać poprawnie zakończone. Powinieneś albo przechowywać identyfikator zadania za każdym razem, aby zakończyć je poprawnie, albo użyć licznika w metodach begin / end.
thejaz
23

Przyjęta odpowiedź jest bardzo pomocna i w większości przypadków powinna wystarczyć, jednak martwiły mnie dwie rzeczy:

  1. Jak zauważyło wiele osób, przechowywanie identyfikatora zadania jako właściwości oznacza, że ​​można go nadpisać, jeśli metoda jest wywoływana wiele razy, co prowadzi do zadania, które nigdy nie zostanie pomyślnie zakończone, dopóki system operacyjny nie zostanie zmuszony do zakończenia w momencie wygaśnięcia. .

  2. Ten wzorzec wymaga unikalnej właściwości dla każdego wywołania, beginBackgroundTaskWithExpirationHandlerco wydaje się kłopotliwe, jeśli masz większą aplikację z wieloma metodami sieciowymi.

Aby rozwiązać te problemy, napisałem singletona, który zajmuje się wszystkimi hydraulikami i śledzi aktywne zadania w słowniku. Żadne właściwości nie są potrzebne do śledzenia identyfikatorów zadań. Wydaje się, że działa dobrze. Użycie jest uproszczone do:

//start the task
NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTask];

//do stuff

//end the task
[[BackgroundTaskManager sharedTasks] endTaskWithKey:taskKey];

Opcjonalnie, jeśli chcesz zapewnić blok ukończenia, który robi coś poza zakończeniem zadania (który jest wbudowany), możesz wywołać:

NSUInteger taskKey = [[BackgroundTaskManager sharedTasks] beginTaskWithCompletionHandler:^{
    //do stuff
}];

Odpowiedni kod źródłowy dostępny poniżej (dla zwięzłości wykluczono pojedyncze elementy). Komentarze / opinie mile widziane.

- (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]];

        }

    }
}
Joel
źródło
1
naprawdę podoba mi się to rozwiązanie. jedno pytanie: jak / co zrobiłeś typedefCompletionBlock? Po prostu to:typedef void (^CompletionBlock)();
Joseph
Masz to. typedef void (^ CompletionBlock) (void);
Joel
@joel, dzięki, ale gdzie jest łącze do kodu źródłowego tej implementacji, to znaczy BackGroundTaskManager?
Özgür
Jak wspomniano powyżej, „pojedyncze rzeczy wyłączone ze względu na zwięzłość”. [BackgroundTaskManager sharedTasks] zwraca singleton. Wnętrzności singletona przedstawiono powyżej.
Joel
Głos za korzystaniem z singletona. Naprawdę nie sądzę, że są tak źli, jak się ludziom wydaje!
Craig Watkinson
20

Oto klasa Swift, która hermetyzuje uruchamianie zadania w tle:

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

Najprostszy sposób użycia:

BackgroundTask.run(application) { backgroundTask in
   // Do something
   backgroundTask.end()
}

Jeśli musisz poczekać na wywołanie zwrotne pełnomocnika przed zakończeniem, użyj czegoś takiego:

class MyClass {
    backgroundTask: BackgroundTask?

    func doSomething() {
        backgroundTask = BackgroundTask(application)
        backgroundTask!.begin()
        // Do something that waits for callback
    }

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
phatmann
źródło
Ten sam problem, co w zaakceptowanej odpowiedzi. Moduł obsługi przedawnienia nie anuluje prawdziwego zadania, a jedynie oznacza je jako zakończone. Co więcej, nadmierna hermetyzacja powoduje, że sami nie jesteśmy w stanie tego zrobić. Dlatego Apple ujawnił ten program obsługi, więc hermetyzacja jest tutaj niewłaściwa.
Ariel Bogdziewicz
@ArielBogdziewicz To prawda, że ​​ta odpowiedź nie daje możliwości dodatkowego porządkowania beginmetody, ale łatwo zobaczyć, jak dodać tę funkcję.
mat.
6

Jak wspomniano tutaj i w odpowiedziach na inne pytania SO, NIE chcesz używać beginBackgroundTasktylko wtedy, gdy Twoja aplikacja przejdzie w tło; Przeciwnie, należy użyć zadania tła dla każdej operacji czasochłonnego którego ukończenie chcesz mieć pewność, nawet jeśli aplikacja ma iść w tle.

Dlatego twój kod prawdopodobnie zostanie usiany powtórzeniami tego samego standardowego kodu do wywoływania beginBackgroundTaski endBackgroundTaskspójnie. Aby zapobiec takim powtórzeniom, z pewnością rozsądne jest zapakowanie schematu w jakąś pojedynczą zamkniętą całość.

Podoba mi się niektóre z istniejących odpowiedzi za to, ale myślę, że najlepszym sposobem jest użycie podklasy Operation:

  • Możesz umieścić Operację w kolejce do dowolnej kolejki OperationQueue i manipulować tą kolejką według własnego uznania. Na przykład możesz przedwcześnie anulować wszelkie istniejące operacje w kolejce.

  • Jeśli masz więcej niż jedną rzecz do zrobienia, możesz połączyć wiele operacji zadań w tle. Operacje obsługują zależności.

  • Kolejka operacji może (i powinna) być kolejką w tle; w związku z tym nie ma potrzeby martwić się wykonywaniem kodu asynchronicznego wewnątrz zadania, ponieważ operacja jest kodem asynchronicznym. (Rzeczywiście, nie ma sensu wykonywanie innego poziomu kodu asynchronicznego wewnątrz Operacji, ponieważ Operacja zakończyłaby się, zanim ten kod mógłby się nawet rozpocząć. Gdybyś musiał to zrobić, użyłbyś innej Operacji.)

Oto możliwa podklasa 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
    }
}

Powinno być oczywiste, jak tego użyć, ale jeśli tak nie jest, wyobraź sobie, że mamy globalną OperationQueue:

let backgroundTaskQueue : OperationQueue = {
    let q = OperationQueue()
    q.maxConcurrentOperationCount = 1
    return q
}()

Tak więc w przypadku typowej, czasochłonnej partii kodu powiedzielibyśmy:

let task = BackgroundTaskOperation()
task.whatToDo = {
    // do something here
}
backgroundTaskQueue.addOperation(task)

Jeśli twoją czasochłonną partię kodu można podzielić na etapy, możesz chcieć się wycofać wcześniej, jeśli zadanie zostanie anulowane. W takim przypadku po prostu wróć przedwcześnie z zamknięcia. Zwróć uwagę, że odniesienie do zadania od wewnątrz musi być słabe, w przeciwnym razie otrzymasz cykl utrzymania. Oto sztuczna ilustracja:

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)

W przypadku konieczności wyczyszczenia w przypadku przedwczesnego anulowania samego zadania w tle, zapewniam opcjonalną cleanupwłaściwość obsługi (nie używana w poprzednich przykładach). Niektóre inne odpowiedzi były krytykowane za nieuwzględnianie tego.

matowe
źródło
Udostępniłem to teraz jako projekt github: github.com/mattneub/BackgroundTaskOperation
Matt
1

Wdrożyłem rozwiązanie Joela. Oto pełny kod:

plik .h:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

plik .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
vomako
źródło
1
Dzięki za to. Mój cel-c nie jest świetny. Czy możesz dodać kod, który pokaże, jak go używać?
pomo
czy możesz podać pełny przykład, jak używać kodu ur
Amr Angry
Bardzo dobrze. Dzięki.
Alyoshak