[iOS] バックグラウンド実行見本(Task Completion)

2011年4月8日金曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

(2011-12-01 追記あり)UIApplicationDelegateの呼び出しが iOS5 から変わった件。

Task Completion を使った iOS4 でのバックグラウンド実行サンプルを作ってみた。

サンプル


実行するとキューにたまった 30個のデータが順番に処理されてテーブルから消えていく。

処理は GCD を使い別スレッドで実行される。右上の[+]ボタンを押すとキューへデータが追加されていく。途中でホームボタンを押してアプリを切り替えても Task Completion によって処理は停止すること無く実行され続ける。わかりやすいようにアプリのアイコンバッヂに残タスク数を表示してみた。
バッジの数字は時間と共にカウントダウンされていくので処理が行われていることが確認できる。


Task Completion とは?


Task Completion は、iOS4 から導入されたマルチタスキングの機能の一つで、これを利用すると最大10分間を上限にバックグラウンドで処理を実行できる。利用するには -[UIApplication beginBackgroundTaskWithExpirationHandler:] を使う。このメソッドは iOS に対してバックグラウンド時の処理続行を依頼するものなので通常はアプリケーションがバックグラウンド状態になるタイミング(applicationDidEnterBackground:)などで呼び出す。公式リファレンスでは次のようなコードが紹介されている。
iOS Application Programming Guide: Executing Code in the Background より

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    UIApplication*    app = [UIApplication sharedApplication];
 
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Do the work associated with the task.
 
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}
beginBackgroundTaskWithExiprationHandler: を呼び出すと、それ以降別のアプリを使用している間も処理を続行させることができる。このメソッドの引数 blocks には、処理が10分過ぎても終わらない時に実行する処理を書いておく。endBackgroundTask: はバックグラウンド処理が終わった時に呼び出す。この引数は beginBackgroundTaskWithExiprationHandler: の戻り値を渡す。


ソースコード解説


サンプルでの Task Completion の利用方法を見ていく。今回は簡易的なキュー(FIFOバッファ)を用意して、そこに入っているデータを単純に取り出すだけの処理を行うスレッドを実行させた。キューの定義はこんな感じ。
@interface Queue : NSObject {

}
@property (retain) NSMutableArray* queue;

- (void)putObject:(id)object;
- (id)getObject;
- (void)removeObject;
- (NSUInteger)count;
- (NSArray*)list;
@end
次にアプリケーション初期化処理。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // [1] creating sample data and put into the queue
    Queue* queue = [[Queue alloc] init];
    for (int i=0; i < 60; i++) {
        NSString* str = [NSString stringWithFormat:@"DATA-%02d", i];
        [queue putObject:str];
    }
    self.rootViewController.queue = queue;
    [queue release];

    // [2] init window    
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    // [3] start thread
    dispatch_queue_t gcd_queue =
       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gcd_queue, ^{
        
        UIApplication* app = [UIApplication sharedApplication];
        app.applicationIconBadgeNumber = [queue count];
        for(;;) {
            if ([queue count] > 0) {
                id object = [queue getObject];
                NSLog(@"processing: %@", object);
                [NSThread sleepForTimeInterval:1.0];    // dummy wait
                NSLog(@"done: %@", object);
                [queue removeObject];
                app.applicationIconBadgeNumber = [queue count];
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.rootViewController.tableView reloadData];
                });
                
                if ([queue count] == 0 && backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"finished!");
                        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                            [app endBackgroundTask:backgroundTaskIdentifer];
                            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
                        }
                    });
                }
            } else {
                [NSThread sleepForTimeInterval:1.0];
            }
        }
        
    });
    
    return YES;
}
前半ではキューへデータを入れたり[1]、ウィンドウの初期化[2]を行っている。後半の [3]で GCDを使いスレッドを一つ作成し、そこで定期的にキュー内のデータを処理させている。このスレッド内ではキューから1づつデータを取り出し処理(単純に1秒スリープ)を繰り返し行う。すべての処理が終わったときに Task Completion が有効( != UIBackgroundTaskInvalid)なら endBackgroundTask: を読んでバックグラウンド処理を終了させている。キューが空になった時には1秒間スリープし、その後キューにデータがあれば処理を実行し、無ければ再びスリープする動作を繰り返す。

Task Completion を有効にする処理は applicationWillResignActive: に書いている。
- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    UIApplication* app = [UIApplication sharedApplication];
    
    NSAssert(backgroundTaskIdentifer == UIBackgroundTaskInvalid, nil);
    
    backgroundTaskIdentifer = [app beginBackgroundTaskWithExpirationHandler:^{
        
        NSLog(@"expired!");
        dispatch_async(dispatch_get_main_queue(), ^{
            if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:backgroundTaskIdentifer];
                backgroundTaskIdentifer = UIBackgroundTaskInvalid;
            }
        });
    }];
    
}
なお Task Completion を有効にした後に別アプリへ切り替え、さらにその後再びこのアプリへ戻ってきた時にまだ Task Completion が有効だった場合には endBackgroundTask: を呼び出してやる必要がある。これを applicationDidBecomeActive: に書いておく。
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    
    UIApplication* app = [UIApplication sharedApplication];
    dispatch_async(dispatch_get_main_queue(), ^{
        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:backgroundTaskIdentifer];
            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
        }
    });
}

Task Completion の制御を
applicationDidEnterBackground:
applicationWillEnterForeground:
の組ではなく
applicationWillResignActive:
applicationDidBecomeActive:
の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。後者であればスリープ => 復帰の時に呼び出されるので今回の目的に適している。

(2011-12-01 追記)iOS5からスリープボタンを押した時でも前者の組み合わせが呼び出されるとのこと。(@hkato193 さん Thanksです)

参考:Cocoaの日々: UIApplicationDelegate のマルチタスキング関連メソッド調査

ちなみに Task Completion を使うとスリープ中もバックグラウンド処理は走り続ける(上限10分)。


ダウンロード


サンプルのソースは GitHub からどうぞ。

BackgroundQueueSample/BackgroundQueueSample at 2011-04-08b from xcatsan/iOS-Sample-Code - GitHub


参考情報



Task Completion の情報は @hkato193さんが書いている第2章の「マルチタスキング」が詳しい。入手可能な日本語の情報ではこれが一番いい。
同様に GCD や Blocks の解説もこの本の第5章「マルチスレッド」(@splhackさん執筆)が詳しくておすすめ。

iOS Application Programming Guide: Executing Code in the Background
Apple提供情報。

Responses

  1. KatokichiSoft says:
    2011年11月30日 16:03

    いつも記事を拝見しております。補足になりますが、iOS5.0以降では

    > applicationWillResignActive:
    > applicationDidBecomeActive:
    > の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。

    が呼ばれるようになってしまいました。スリープ(スクリーンロック)もバックグラウンド状態として扱われるようになったためです。

    そこまでは良いのですが、現時点(5.0)ではスリープとホームボタンとを区別する公式の方法が無いため、iOS4では動いていたアプリもiOS5では動かない問題が色々と出ていますね。
    たとえば、バックグラウンド再生に対応していない音楽再生アプリでスリープすると再生が止まってしまうなど・・・なかなかに厄介です。

  2. KatokichiSoft says:
    2011年11月30日 16:03

    いつも記事を拝見しております。補足になりますが、iOS5.0以降では

    > applicationWillResignActive:
    > applicationDidBecomeActive:
    > の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。

    が呼ばれるようになってしまいました。スリープ(スクリーンロック)もバックグラウンド状態として扱われるようになったためです。

    そこまでは良いのですが、現時点(5.0)ではスリープとホームボタンとを区別する公式の方法が無いため、iOS4では動いていたアプリもiOS5では動かない問題が色々と出ていますね。
    たとえば、バックグラウンド再生に対応していない音楽再生アプリでスリープすると再生が止まってしまうなど・・・なかなかに厄介です。

  3. xcatsan says:
    2011年12月1日 22:50

    KatokichiSoft さん、こんばんは。
    毎回情報どうも

    そうなんですか-。
    公式に示されていない仕様には、
    やはり頼るべきではないということですね。
    # そうもいかないから余計に厄介なのですが。

  4. xcatsan says:
    2011年12月1日 22:50

    KatokichiSoft さん、こんばんは。
    毎回情報どうも

    そうなんですか-。
    公式に示されていない仕様には、
    やはり頼るべきではないということですね。
    # そうもいかないから余計に厄介なのですが。

Leave a Response

人気の投稿(過去 30日間)