beginBackgroundTaskWithExpirationHandler
をいつどのように使用するかについて少し混乱しています。
Appleは、例でapplicationDidEnterBackground
デリゲートでそれを使用し、いくつかの重要なタスク(通常はネットワークトランザクション)を完了する時間を確保することを示しています。
私のアプリを見ると、私のネットワーク関連のものの大部分が重要であるように思われます。1つが起動したら、ユーザーがホームボタンを押したらそれを完了したいと思います。
したがって、すべてのネットワークトランザクションをラップすることは受け入れられています/グッドプラクティスです(そして、データの大きな塊をダウンロードすることについて話しているのではなく、主にいくつかの短いxml)beginBackgroundTaskWithExpirationHandler
で安全な側にありますか?
ネットワークトランザクションをバックグラウンドで続行する場合は、バックグラウンドタスクでラップする必要があります。また、終了時に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)
}
受け入れられた答えは非常に有用であり、ほとんどの場合問題ありませんが、次の2つのことが気になりました。
多くの人が指摘しているように、タスクIDをプロパティとして保存すると、メソッドが複数回呼び出された場合に上書きできるため、有効期限時にOSによって強制終了されるまでタスクが正常に終了することはありません。
このパターンには、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]];
}
}
}
以下は、バックグラウンドタスクの実行をカプセル化する 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
}
}
ここで述べたように、他のSOの質問への回答では、アプリがバックグラウンドに移行するときだけbeginBackgroundTask
を使用したくありません。逆に、使用する必要がありますany時間のかかる操作のバックグラウンドタスク。アプリdoesがバックグラウンドに移行した場合でも完了を確認する必要があります。
したがって、あなたのコードはbeginBackgroundTask
とendBackgroundTask
を首尾一貫して呼び出すための同じ定型コードの繰り返しで散らばってしまうでしょう。この繰り返しを防ぐために、ボイラープレートを単一のカプセル化されたエンティティにパッケージ化することは確かに合理的です。
私はそれを行うための既存の回答のいくつかが好きですが、最善の方法は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
ハンドラープロパティを提供しました(前の例では使用していません)。他のいくつかの答えはそれを含まないことで批判されました。
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
最初にドキュメントをお読みください: 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()
}