Ordnungsgemäße Verwendung von beginBackgroundTaskWithExpirationHandler

107

Ich bin etwas verwirrt darüber, wie und wann ich es verwenden soll beginBackgroundTaskWithExpirationHandler.

Apple zeigt in seinen Beispielen, wie man es als applicationDidEnterBackgroundDelegat verwendet, um mehr Zeit für die Ausführung einer wichtigen Aufgabe zu erhalten, normalerweise einer Netzwerktransaktion.

Wenn ich auf meine App schaue, scheint es, dass die meisten meiner Netzwerk-Inhalte wichtig sind, und wenn eine gestartet wird, möchte ich sie abschließen, wenn der Benutzer die Home-Taste drückt.

Ist es also akzeptabel / eine gute Praxis, jede Netzwerktransaktion (und ich spreche nicht über das Herunterladen großer Datenmengen, meistens einige kurze XML-Dateien) zu verpacken beginBackgroundTaskWithExpirationHandler, um auf der sicheren Seite zu sein?

Eyal
quelle
Siehe auch hier
Honey

Antworten:

165

Wenn Sie möchten, dass Ihre Netzwerktransaktion im Hintergrund fortgesetzt wird, müssen Sie sie in eine Hintergrundaufgabe einschließen. Es ist auch sehr wichtig, dass Sie anrufen, endBackgroundTaskwenn Sie fertig sind. Andernfalls wird die App nach Ablauf der zugewiesenen Zeit beendet.

Meine neigen dazu, ungefähr so ​​auszusehen:

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

Ich habe eine UIBackgroundTaskIdentifierEigenschaft für jede Hintergrundaufgabe


Äquivalenter Code in 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
quelle
1
Ja, das tue ich ... sonst hören sie auf, wenn die App in den Hintergrund tritt.
Ashley Mills
1
Müssen wir irgendetwas in applicationDidEnterBackground tun?
Dips
1
Nur wenn Sie dies als Punkt zum Starten des Netzwerkbetriebs verwenden möchten . Wenn Sie nur möchten, dass ein vorhandener Vorgang gemäß der Frage von @ Eyal abgeschlossen wird, müssen Sie in applicationDidEnterBackground
Ashley Mills
2
Danke für dieses klare Beispiel! (Gerade geändert seinBackgroundUpdateTask, umBackgroundUpdateTask zu beginnen.)
Newenglander
30
Wenn Sie doUpdate mehrmals hintereinander aufrufen, ohne dass die Arbeit erledigt ist, überschreiben Sie self.backgroundUpdateTask, sodass vorherige Aufgaben nicht ordnungsgemäß beendet werden können. Sie sollten entweder die Aufgabenkennung jedes Mal speichern, damit Sie sie ordnungsgemäß beenden, oder einen Zähler in den Start- / Endmethoden verwenden.
Thejaz
23

Die akzeptierte Antwort ist sehr hilfreich und sollte in den meisten Fällen in Ordnung sein, aber zwei Dinge haben mich daran gestört:

  1. Wie eine Reihe von Personen festgestellt hat, bedeutet das Speichern der Aufgabenkennung als Eigenschaft, dass sie überschrieben werden kann, wenn die Methode mehrmals aufgerufen wird. Dies führt zu einer Aufgabe, die erst dann ordnungsgemäß beendet wird, wenn sie vom Betriebssystem zum Zeitpunkt des Ablaufs beendet werden muss .

  2. Dieses Muster erfordert für jeden Aufruf eine eindeutige Eigenschaft, beginBackgroundTaskWithExpirationHandlerdie umständlich erscheint, wenn Sie eine größere App mit vielen Netzwerkmethoden haben.

Um diese Probleme zu lösen, habe ich einen Singleton geschrieben, der sich um alle Installationen kümmert und aktive Aufgaben in einem Wörterbuch verfolgt. Es sind keine Eigenschaften erforderlich, um die Aufgabenkennungen zu verfolgen. Scheint gut zu funktionieren. Die Verwendung wird vereinfacht, um:

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

//do stuff

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

Wenn Sie einen Abschlussblock bereitstellen möchten, der über das Beenden der integrierten Aufgabe hinausgeht, können Sie optional Folgendes aufrufen:

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

Relevanter Quellcode unten verfügbar (Singleton-Material aus Gründen der Kürze ausgeschlossen). Kommentare / Feedback willkommen.

- (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
quelle
1
mag diese Lösung wirklich. Eine Frage: Wie / als was hast du typedefCompletionBlock? Einfach das:typedef void (^CompletionBlock)();
Joseph
Du hast es. typedef void (^ CompletionBlock) (void);
Joel
@joel, danke, aber wo ist der Link des Quellcodes für diese Implementierung, d. h. BackGroundTaskManager?
Özgür
Wie oben erwähnt "Singleton-Sachen aus Gründen der Kürze ausgeschlossen". [BackgroundTaskManager sharedTasks] gibt einen Singleton zurück. Die Eingeweide des Singleton sind oben angegeben.
Joel
Upvoted für die Verwendung eines Singleton. Ich denke wirklich nicht, dass sie so schlecht sind, wie die Leute ausmachen!
Craig Watkinson
20

Hier ist eine Swift-Klasse , die das Ausführen einer Hintergrundaufgabe kapselt:

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

Der einfachste Weg, es zu benutzen:

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

Wenn Sie vor dem Beenden auf einen Rückruf eines Delegierten warten müssen, verwenden Sie Folgendes:

class MyClass {
    backgroundTask: BackgroundTask?

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

    func callback() {
        backgroundTask?.end()
        backgroundTask = nil
    } 
}
Phatmann
quelle
Das gleiche Problem wie in der akzeptierten Antwort. Der Ablauf-Handler bricht die eigentliche Aufgabe nicht ab, sondern markiert sie nur als beendet. Mehr über Kapselung führt dazu, dass wir das nicht selbst tun können. Aus diesem Grund hat Apple diesen Handler verfügbar gemacht, sodass die Kapselung hier falsch ist.
Ariel Bogdziewicz
@ArielBogdziewicz Es stimmt, dass diese Antwort keine Möglichkeit für eine zusätzliche Bereinigung der beginMethode bietet, aber es ist leicht zu erkennen, wie diese Funktion hinzugefügt wird.
Matt
6

Wie hier und in den Antworten auf andere SO-Fragen erwähnt, möchten Sie NICHT beginBackgroundTasknur verwenden, wenn Ihre App in den Hintergrund tritt. im Gegenteil, sollten Sie eine Hintergrundaufgabe für verwenden jederzeitaufwendiger Vorgang , dessen Abschluss Sie wollen auch , um sicherzustellen , wenn die App nicht in den Hintergrund rücken .

Daher wird Ihr Code wahrscheinlich mit Wiederholungen desselben Boilerplate-Codes zum Aufrufen beginBackgroundTaskund endBackgroundTaskkohärent gespickt sein . Um diese Wiederholung zu verhindern, ist es sicherlich vernünftig, die Boilerplate in eine einzelne gekapselte Einheit zu packen.

Ich mag einige der vorhandenen Antworten dafür, aber ich denke, der beste Weg ist, eine Operations-Unterklasse zu verwenden:

  • Sie können die Operation in eine beliebige OperationQueue einreihen und diese Warteschlange nach Belieben bearbeiten. Sie können beispielsweise vorhandene Vorgänge in der Warteschlange vorzeitig abbrechen.

  • Wenn Sie mehr als eine Aufgabe haben, können Sie mehrere Vorgänge für Hintergrundaufgaben verketten. Operationen unterstützen Abhängigkeiten.

  • Die Operationswarteschlange kann (und sollte) eine Hintergrundwarteschlange sein. somit gibt es keine Notwendigkeit , über die Durchführung asynchrone Codes innerhalb Ihrer Aufgabe zu kümmern, weil der Betrieb ist der asynchrone Code. (In der Tat ist es nicht sinnvoll, eine andere Ebene von asynchronem Code innerhalb einer Operation auszuführen , da die Operation beendet würde, bevor dieser Code überhaupt gestartet werden könnte. Wenn Sie dies tun müssten, würden Sie eine andere Operation verwenden.)

Hier ist eine mögliche Operations-Unterklasse:

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

Es sollte offensichtlich sein, wie dies verwendet wird, aber falls dies nicht der Fall ist, stellen Sie sich vor, wir haben eine globale OperationQueue:

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

Für einen typischen zeitaufwändigen Code-Stapel würden wir also sagen:

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

Wenn Ihr zeitaufwändiger Codestapel in Phasen unterteilt werden kann, möchten Sie sich möglicherweise frühzeitig zurückziehen, wenn Ihre Aufgabe abgebrochen wird. In diesem Fall kehren Sie einfach vorzeitig vom Verschluss zurück. Beachten Sie, dass Ihr Verweis auf die Aufgabe innerhalb des Abschlusses schwach sein muss, sonst erhalten Sie einen Aufbewahrungszyklus. Hier ist eine künstliche Illustration:

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)

Falls Sie eine Bereinigung durchführen müssen, falls die Hintergrundaufgabe selbst vorzeitig abgebrochen wird, habe ich eine optionale cleanupHandler-Eigenschaft bereitgestellt (in den vorhergehenden Beispielen nicht verwendet). Einige andere Antworten wurden dafür kritisiert, dass sie dies nicht beinhalteten.

matt
quelle
Ich habe dies jetzt als Github-Projekt bereitgestellt: github.com/mattneub/BackgroundTaskOperation
matt
1

Ich habe Joels Lösung implementiert. Hier ist der vollständige Code:

.h Datei:

#import <Foundation/Foundation.h>

@interface VMKBackgroundTaskManager : NSObject

+ (id) sharedTasks;

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

@end

.m Datei:

#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
quelle
1
Danke dafür. Mein Ziel-c ist nicht großartig. Könnten Sie einen Code hinzufügen, der zeigt, wie man ihn benutzt?
Pomo
Können Sie bitte ein vollständiges Beispiel für die Verwendung Ihres Codes geben?
Amr Angry
Sehr schön. Vielen Dank.
Alyoshak