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それぞれのバックグラウンドタスクのプロパティを


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で何もする必要はありません
Ashley Mills

2
この明確な例をありがとう!(beingBackgroundUpdateTaskをbeginBackgroundUpdateTaskに変更しただけです。)
newenglander 2013年

30
作業を行わずにdoUpdateを続けて複数回呼び出すと、self.backgroundUpdateTaskが上書きされるため、前のタスクを適切に終了できません。タスク識別子を毎回保存して適切に終了するか、begin / endメソッドでカウンタを使用する必要があります。
thejaz 2013年

23

受け入れられた回答は非常に役に立ち、ほとんどの場合は問題ないはずですが、次の2つのことが気になります。

  1. 多くの人が指摘しているように、タスク識別子をプロパティとして保存することは、メソッドが複数回呼び出された場合に上書きされる可能性があることを意味し、期限切れ時にOSによって強制終了されるまでタスクが正常に終了しない。

  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
このソリューションが本当に好きです。しかし、1つの質問:どのように/どのようにしてtypedefCompletionBlockを実行しましたか?単にこれ:typedef void (^CompletionBlock)();
ジョセフ

了解しました。typedef void(^ CompletionBlock)(void);
Joel

@joel、ありがとうございます。ただし、この実装のソースコード、つまりBackGroundTaskManagerのリンクはどこにありますか?
Özgür

上記のように、「簡潔にするためにシングルトンのものは除外されています」。[BackgroundTaskManager sharedTasks]はシングルトンを返します。シングルトンの内臓は上記に提供されています。
Joel

シングルトンを使用することに賛成。私は本当に人々が気づくほど悪いとは思わない!
クレイグワトキンソン2016年

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がこのハンドラを公開した理由であり、カプセル化はここでは間違っています。
Ariel Bogdziewicz

@ArielBogdziewiczこの答えがbeginメソッドで追加のクリーンアップを行う機会を提供しないことは事実ですが、その機能を追加する方法を簡単に確認できます。
マット

6

ここと他のSOの質問への回答で述べたようbeginBackgroundTaskに、アプリがバックグラウンドになるときだけ使用するのは望ましくありません。逆に、あなたがのためのバックグラウンドタスクを使用する必要があります任意のその完成アプリがあっても確実にしたい時間のかかる操作バックグラウンドに入ります。

したがって、あなたのコードは、呼び出し元に同じ定型コードの繰り返しがちりばめ終わる可能性があるbeginBackgroundTaskendBackgroundTaskコヒーレント。この繰り返しを防ぐために、ボイラープレートをいくつかの単一のカプセル化されたエンティティーにパッケージ化することは確かに合理的です。

私はそれを行うための既存の回答のいくつかが好きですが、最善の方法はOperationサブクラスを使用することだと思います:

  • オペレーションを任意のOperationQueueにエンキューし、必要に応じてそのキューを操作できます。たとえば、キュ​​ー上の既存の操作を途中でキャンセルすることができます。

  • やることが複数ある場合は、複数のバックグラウンドタスクの操作を連鎖させることができます。操作は依存関係をサポートします。

  • 操作キューはバックグラウンドキューにすることができます(そうする必要があります)。したがって、操作非同期コードであるため、タスク内で非同期コードを実行することを心配する必要はありません。(実際、オペレーション内で別のレベルの非同期コードを実行しても、コードが開始する前にオペレーションが完了するため、意味がありません。必要に応じて、別のオペレーションを使用します。)

可能な操作サブクラスは次のとおりです。

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
matt

1

Joelのソリューションを実装しました。完全なコードは次のとおりです。

.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

urコードの使用方法の完全な例を教えていただけませんか
Amr Angry

非常に素晴らしい。ありがとう。
Alyoshak 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.