web-dev-qa-db-ja.com

beginBackgroundTaskWithExpirationHandlerの適切な使用

beginBackgroundTaskWithExpirationHandlerをいつどのように使用するかについて少し混乱しています。

Appleは、例でapplicationDidEnterBackgroundデリゲートでそれを使用し、いくつかの重要なタスク(通常はネットワークトランザクション)を完了する時間を確保することを示しています。

私のアプリを見ると、私のネットワーク関連のものの大部分が重要であるように思われます。1つが起動したら、ユーザーがホームボタンを押したらそれを完了したいと思います。

したがって、すべてのネットワークトランザクションをラップすることは受け入れられています/グッドプラクティスです(そして、データの大きな塊をダウンロードすることについて話しているのではなく、主にいくつかの短いxml)beginBackgroundTaskWithExpirationHandlerで安全な側にありますか?

98
Eyal

ネットワークトランザクションをバックグラウンドで続行する場合は、バックグラウンドタスクでラップする必要があります。また、終了時に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)
}
153
Ashley Mills

受け入れられた答えは非常に有用であり、ほとんどの場合問題ありませんが、次の2つのことが気になりました。

  1. 多くの人が指摘しているように、タスクIDをプロパティとして保存すると、メソッドが複数回呼び出された場合に上書きできるため、有効期限時に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]];

        }

    }
}
20
Joel

以下は、バックグラウンドタスクの実行をカプセル化する 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
    } 
}
17
phatmann

ここで述べたように、他のSOの質問への回答では、アプリがバックグラウンドに移行するときだけbeginBackgroundTaskを使用したくありません。逆に、使用する必要がありますany時間のかかる操作のバックグラウンドタスク。アプリdoesがバックグラウンドに移行した場合でも完了を確認する必要があります。

したがって、あなたのコードはbeginBackgroundTaskendBackgroundTaskを首尾一貫して呼び出すための同じ定型コードの繰り返しで散らばってしまうでしょう。この繰り返しを防ぐために、ボイラープレートを単一のカプセル化されたエンティティにパッケージ化することは確かに合理的です。

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

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

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

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

可能な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?()
            UIApplication.shared.endBackgroundTask(bti) // cancellation
        }
        guard bti != .invalid else { return }
        whatToDo()
        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ハンドラープロパティを提供しました(前の例では使用していません)。他のいくつかの答えはそれを含まないことで批判されました。

1
matt

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
vomako

最初にドキュメントをお読みください: https://developer.Apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio

バックグラウンドタスクは次の要件を満たす必要があります。

  • バックグラウンドタスクはできるだけ早く報告する必要がありますが、実際のタスクを開始する前に報告する必要はありません。メソッドbeginBackgroundTaskWithExpirationHandler:は非同期に機能するため、applicationDidEnterBackground:の最後に呼び出された場合、バックグラウンドタスクを登録せず、すぐに期限切れハンドラーを呼び出します。
  • 有効期限ハンドラは、実際のタスクをキャンセルし、バックグラウンドタスクを終了としてマークする必要があります。バックグラウンドタスクの識別子をどこかに、たとえばクラスの属性として保存することを強制します。このプロパティは、上書きできないように制御する必要があります。
  • 有効期限ハンドラはメインスレッドから実行されるため、そこでキャンセルする場合は、実際のタスクをスレッドセーフにする必要があります。
  • 実際のタスクはキャンセル可能でなければなりません。つまり、実際のタスクにはcancelメソッドが必要です。そうしないと、バックグラウンドタスクを終了としてマークしても、予期しない方法で終了するリスクがあります。
  • beginBackgroundTaskWithExpirationHandler:を含むコードは、どこでも、どのスレッドでも呼び出すことができます。アプリのデリゲートapplicationDidEnterBackground:のメソッドである必要はありません。
  • メソッドapplicationDidEnterBackground:の場合、5秒未満の同期操作に対して行う意味はありません(ドキュメントをお読みください https://developer.Apple.com/documentation/uikit/uiapplicationdelegate/1622997-applicationdidenterbackground ?language = objc
  • メソッドapplicationDidEnterBackgroundは5秒よりも短い時間で実行する必要があるため、すべてのバックグラウンドタスクは2番目のスレッドで起動する必要があります。

例:

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

アプリケーションデリゲートで使用できます。

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