2011年 8月 04日(木曜日) 20:53

時間のかかる処理をバックグラウンドで実行する方法

評価:
(0 票)

大量のデータ処理等、時間のかかる処理をメインスレッドで実行してしまうと、UIが固まってしまってよろしくありません。これを防ぐためには、時間のかかる処理は別スレッドをつくってそちらで行うようにします。Cocoaにはこれを非常に簡単に実現する方法が用意されていますが、いくつか気をつけるべき点があったので書いておきます。

1.performSelectorInBackground:withObject:メソッドを呼ぶ

NSObjectにはperformSelectorInBackground:withObject:というメソッドが用意されていて、バックグラウンドで処理を行うメソッドのセレクタを指定するだけで、非常に簡単に別スレッドで実行してくれます。

例えばこんな感じ。

- (void)runVeryHeavyMethodInBackground {
    [self performSelectorInBackground:@selector(veryHeavyMethod) withObject:nil];
}

バックグラウンドで動作させるメソッドを次のように用意しておきます。

- (void)veryHeavyMethod {
    // 時間のかかる処理をここで行う。
}

2.バックグラウンドで動作させる入り口のメソッドでAutoreleasePoolをつくる

基本的にはこれだけでちゃんとバックグラウンドで実行してくれるのですが、実際にこのまま実行してみると次のような警告メッセージが大量に出てしまいました。

__NSAutoreleaseNoPool(): Object 0x710e9c0 of class NSCFString autoreleased with no pool in place - just leaking

どうやら、Autorelease Poolが存在しないところでautoreleaseが呼ばれたので、このままではメモリを開放できず、リークしてしまうということのようです。

ことのときに、デバッガで止めてスタックトレースを見てみると、こんな感じになっていました。

#0 -[TestObject veryHeavyMethod] (self=0x527830, _cmd=0x148efe) at xxxx
#1 0x3354a388 in -[NSThread main] ()
#2 0x335bc5cc in __NSThread__main__ ()
#3 0x35daf310 in _pthread_start ()
#4 0x35db0bbc in thread_start ()

なるほど、メインスレッドから呼ばれる場合は、フレームワークの方でAutorelease Poolを作ってくれているのですが、自分でスレッドを作った場合には、Autorelease Poolも自分で作らなければならないわけですね。

そこで、別スレッドで実行するメソッドを次のように変更します。

- (void)veryHeavyMethod {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // 時間のかかる処理をここで行う。

    [pool release];
}

これで、上の警告メッセージも出なくなりました。

3.バックグラウンドでの動作が完了したことをメインスレッドに戻して通知する

このままだと、バックグラウンドでの処理がいつ完了したのかわかりません。完了したタイミングでメッセージを表示したり、UIActivityIndicatorを止めたりといった処理をするには、何らかの方法で通知を受け取る必要があります。

通知を受ける方法はいろいろあると思いますが、ここではdelegateを使った方法を試してみました。

- (void)veryHeavyMethod {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // 時間のかかる処理をここで行う。

    // デリゲートに通知する
    if ([delegate respondsToSelector:@selector(testObjectDidFinishVeryHeavyMethod)]) {
        [delegate testObjectDidFinishVeryHeavyMethod];
    }

    [pool release];
}

すると、ちゃんと通知を受けることはできるのですが、このままだとバックグラウンドのスレッドでずっと実行され続けてしまいます。メインスレッドからでなければ実行できない処理も多いので、このままでは困ります。

調べてみると、NSObjectにはメインスレッドに処理を戻すperformSelectorOnMainThread:withObject:waitUntilDone:というメソッドがありました。これを使って、次のように書き換えます。

- (void)veryHeavyMethod {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    // 時間のかかる処理をここで行う。

    // デリゲートに通知する
    [self performSelectorOnMainThread:@selector(notifyVeryHeavyMethodDidFinish) withObject:nil waitUntilDone:NO];

    [pool release];
}

- (void)notifyVeryHeavyMethodDidFinish {
    if ([delegate respondsToSelector:@selector(testObjectDidFinishVeryHeavyMethod)]) {
        [delegate testObjectDidFinishVeryHeavyMethod];
    }
}

これで、デリゲートで通知を受けたときには既にメインスレッドに処理が戻っているので、続けて他の処理を行うことができるようになりました。

Cocoaには、他にもマルチスレッドで処理を行う方法が充実しているようですが、ちょっと時間のかかる処理をバックグラウンドで行いたいだけの時には、この方法は手軽で便利です。

最終更新日: 2011年 8月 07日(日曜日) 13:38
くらち たかよし

くらち たかよし

モバイル・Webアプリ作家。最近は主にiPhoneアプリ制作を手がける。企画から、UIデザイン、設計、実装、テスト、多言語対応、ユーザーサポートまでを1人〜数人の個人で行う全人的開発手法の確立を目指している。

使う言語はObjective-C, C++, C#, Java, PHPなど。Web関連で使うものはCakePHP, MySQL, Joomla! CMSなど。デザインはシロウトながらPhotoshopとIllustratorをなんとかがんばって使う。

場所や時間に縛られない、インターネット時代の新しい働き方、自由な生き方を模索中。海外移住、低予算&低リスク起業、キャリアデザイン、心理学などにも興味あり。

Web: awaresoft.jp/