it-swarm.com.de

Ordnungsgemäße Verwendung von beginBackgroundTaskWithExpirationHandler

Ich bin etwas verwirrt darüber, wie und wann beginBackgroundTaskWithExpirationHandler verwendet wird.

Apple zeigt in seinen Beispielen, wie es in applicationDidEnterBackground delegate verwendet werden soll, um mehr Zeit für wichtige Aufgaben zu erhalten, normalerweise eine Netzwerktransaktion.

Bei der Suche in meiner App scheint das meiste meines Netzwerks wichtig zu sein, und wenn eines gestartet wird, möchte ich es abschließen, wenn der Benutzer die Home-Taste drückt.

Ist es also eine akzeptierte Praxis, jede Netzwerktransaktion einzuwickeln (und ich spreche nicht vom Herunterladen großer Datenmengen, meistens einer kurzen XML-Datei) mit beginBackgroundTaskWithExpirationHandler auf der sicheren Seite? 

94
Eyal

Wenn Sie möchten, dass Ihre Netzwerktransaktion im Hintergrund fortgesetzt wird, müssen Sie sie in eine Hintergrundaufgabe packen. Es ist auch sehr wichtig, dass Sie endBackgroundTask aufrufen, wenn 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 UIBackgroundTaskIdentifier-Eigenschaft für jede Hintergrundaufgabe


Gleichwertiger Code in Swift

func doUpdate () {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {

        let taskID = beginBackgroundUpdateTask()

        var response: NSURLResponse?, 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.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}

func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
    UIApplication.sharedApplication().endBackgroundTask(taskID)
}
153
Ashley Mills

Die akzeptierte Antwort ist sehr hilfreich und sollte in den meisten Fällen in Ordnung sein, jedoch störten mich zwei Dinge:

  1. Eine Reihe von Leuten hat bereits darauf hingewiesen, dass das Speichern der Taskkennung als Eigenschaft bedeutet, dass sie überschrieben werden kann, wenn die Methode mehrmals aufgerufen wird. Dies führt zu einer Task, die niemals ordnungsgemäß beendet wird, bis sie vom Betriebssystem zum Ablaufzeitpunkt beendet wird .

  2. Dieses Muster erfordert für jeden Aufruf von beginBackgroundTaskWithExpirationHandler eine eindeutige Eigenschaft, die bei einer größeren App mit vielen Netzwerkmethoden umständlich erscheint. 

Um diese Probleme zu lösen, habe ich ein Singleton geschrieben, das sich um alle Klempnerarbeiten kümmert und aktive Aufgaben in einem Wörterbuch nachverfolgt. Keine Eigenschaften zum Verfolgen von Task-IDs erforderlich. Scheint gut zu funktionieren. Die Verwendung wird vereinfacht zu:

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

//do stuff

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

Wenn Sie einen Beendigungsblock bereitstellen möchten, der über das Beenden der Task hinausgeht (die in den integrierten Task integriert ist), können Sie optional Folgendes aufrufen: 

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

Relevanter Quellcode unten verfügbar (Singleton-Zeug aus Gründen der Kürze). 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]];

        }

    }
}
20
Joel

Hier ist eine Swift-Klasse , die die Ausführung 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 Delegaten-Rückruf 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
    } 
}
15
phatmann

Wie hier und in Antworten auf andere SO Fragen erwähnt, möchten Sie beginBackgroundTask NICHT nur dann verwenden, wenn Ihre App in den Hintergrund tritt. Im Gegenteil, Sie sollten eine Hintergrundaufgabe für jede zeitaufwändige Operation verwenden, deren Fertigstellung Sie sicherstellen möchten, auch wenn die App in den Hintergrund tritt .

Aus diesem Grund wird Ihr Code wahrscheinlich mit Wiederholungen desselben Boilerplate-Codes gespickt, um beginBackgroundTask und endBackgroundTask kohärent aufzurufen. Um diese Wiederholung zu verhindern, ist es sicherlich sinnvoll, die Kesselplatte in eine einzelne gekapselte Einheit zu packen.

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

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

  • Wenn Sie mehr als eine Aufgabe ausführen müssen, können Sie mehrere Vorgänge für Hintergrundaufgaben verketten. Betriebsunterstützungsabhängigkeiten.

  • Die Operationswarteschlange kann (und sollte) eine Hintergrundwarteschlange sein. Daher müssen Sie sich keine Gedanken über die Ausführung von asynchronem Code innerhalb Ihrer Aufgabe machen, da die Operation ist der asynchrone Code. (In der Tat macht es keinen Sinn, 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 Unterklasse für Operationen:

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

Es sollte offensichtlich sein, wie dies zu verwenden ist, 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 zeitaufwendigen Code-Stapel würden wir also sagen:

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

Wenn Ihr zeitaufwändiger Code-Stapel in Phasen unterteilt werden kann, möchten Sie sich möglicherweise vorzeitig 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 Beibehaltungszyklus. 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 vorzeitig abgebrochen wird, habe ich eine optionale cleanup -Handlereigenschaft angegeben (in den vorhergehenden Beispielen nicht verwendet). Einige andere Antworten wurden dafür kritisiert, dass sie dies nicht beinhalteten.

1
matt

Ich habe die Lösung von Joel 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
1
vomako

Bitte lesen Sie zuerst die Dokumentation: https://developer.Apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio

Die Hintergrundaufgabe sollte folgende Anforderungen erfüllen:

  • Die Hintergrundaufgabe sollte so bald wie möglich gemeldet werden, muss jedoch nicht vor dem Beginn unserer eigentlichen Aufgabe sein. Die Methode beginBackgroundTaskWithExpirationHandler: arbeitet asynchron. Wenn sie am Ende von applicationDidEnterBackground: aufgerufen wird, wird die Hintergrundaufgabe nicht registriert und der Ablaufhandler wird sofort aufgerufen.
  • Der Ablaufhandler muss unsere echte Aufgabe abbrechen und die Hintergrundaufgabe als beendet markieren. Es zwingt uns, die Kennung der Hintergrundaufgabe irgendwo zu speichern, beispielsweise als Attribut einer Klasse. Diese Eigenschaft sollte unter unserer Kontrolle stehen, damit sie nicht überschrieben werden kann.
  • Der Ablaufhandler wird vom Haupt-Thread aus ausgeführt. Ihre eigentliche Aufgabe sollte daher Thread-sicher sein, wenn Sie sie dort abbrechen möchten.
  • Unsere eigentliche Aufgabe sollte kündbar sein. Es bedeutet, dass unsere eigentliche Aufgabe die Methode cancel haben sollte. Andernfalls besteht das Risiko, dass es auf unvorhergesehene Weise beendet wird, selbst wenn wir eine Hintergrundaufgabe als beendet markieren.
  • Code, der beginBackgroundTaskWithExpirationHandler: enthält, kann überall und in jedem Thread aufgerufen werden. Es muss nicht die Methode des App-Delegierten applicationDidEnterBackground: sein.
  • Es macht keinen Sinn, dies für synchrone Operationen zu tun, die kürzer als 5 Sekunden sind, für die Methode applicationDidEnterBackground: (bitte lesen Sie doc https://developer.Apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground?language=objc )
  • Die Methode applicationDidEnterBackground muss in einer Zeit ausgeführt werden, die kürzer als 5 Sekunden ist, sodass alle Hintergrundaufgaben im zweiten Thread gestartet werden sollten.

Beispiel:

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

Es kann in unserem Bewerbungsdelegierten verwendet werden:

func applicationDidEnterBackground(_ application: UIApplication) {
    let myBackgroundTask = MySpecificBackgroundTask(application: application)
    myBackgroundTask.start()
}
0