Implementing Delayed Loading

再申請したアプリの結果は、今日の時点ではまだ返ってきていない。大体in reviewになってから結果がくるまでに営業日で7日ぐらいかかるようだ。アプリの審査が通ったら、お披露目しようと思うのだけど、また要再提出となると、また一週間ということになって・・・。iPhoneアプリはこのあたりが一番難しいですね。

任天堂やソニーなんかはもっと速いんだけど、審査をする本数も全然違うというのもあるんだろう。そういえば任天堂にはマリオクラブ(いつのまにか会社になっててびっくりした)というのがあり、初めて名前を聞いたときは秘密結社?などと思ったのだけど、ソフトのクォリティコントロールをしているところだ。ここが、OKを出さないとリリースさせてもらえない。ソフトウェア上のバグから、どのロットのハードではちゃんと動かないなどというレポートまで報告してくれる。どのロットのハードでもちゃんと動かないとダメというのが、任天堂は遊べるということには強いこだわりがある会社なんだなという印象をうけた思い出がある。

さて、今回申請したアプリはたくさんの画像を扱うもので、すべてをメモリ上に持っておくことが出来ないので、必要になったらロードしないといけない。とはいえ、いちいち待たされてから、画像の内容を判断するのはもどかしいので、まずは荒くでも見たい。最初に軽いけど荒い画像を表示しておいて、そのうちに隙を見て正式な画像をロードする、つまり遅延読み込み(delayed loading)だ。

Cocoa(Touch)フレームワークの機能を使うと、かなり簡単に遅延読み込みは実装できる。いくつか抑えないといけないポイントはあるにせよスレッド的な扱いはかなり楽だ。

まずは、

@interface PhotoImage : NSObject
{
NSString *_imagePath;
UIImage *_image;
}
@property (retain,readonly) UIImage *image;
- (void)requestImage;
@end

などとクラスを作成する。いろんな実装方法がある中で、今回は読み終わったらNotificationで通知されるという形を考える。

requestImageが呼ばれると画像のローディングを開始する。このメソッドの中で呼んでしまうとそこでブロックされてしまうので、裏スレッドで読み込むのだけど、Cocoaにはすべてのクラスの基底クラスNSObjectに、メソッドを裏スレッドで実行するというとても便利なメソッドが用意されている(つまりどのクラスでもその仕組みが使える)。それがperformSelectorInBackground:withObject:というメソッドだ。

- (void)requestImage
{
if (!_image)
[self performSelecortInBackground:@selector(_loadImage) withObject:nil];
[self _sendCompletedNotify];
}

もし既にロードされていた場合は即完了通知が返るようにしておく。

performSelectorInBackground:withObject:は、”This method creates a new thread in your application, putting your application into multithreaded mode if it was not already.(このメソッドは新たにスレッドを作成し、もしまだそうでない場合はアプリケーションをマルチスレッドモードにします。)”とドキュメントにあるように、自動的にスレッドを作ってそちらで指定したメソッドを実行してくれる。スレッドの開始処理や終了処理なんかは考えなくて良いので楽なのだけど、一つやっておかないといけないおまじないみたいなものがある。

- (void)_loadImage
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
UIImage *image = [UIImage imageContentsOfFile:_imagePath];
self.image = image;
[self _sendCompletedNotify];
[pool drain];
}

普段はあらかじめ用意してくれているAutoreleasePoolを使用しているのだけど、裏スレッドなど別スレッドの場合は自分で用意しないといけない。alloc、initして最後にdrainする。drainは、Cocoa touchの場合、今のところドキュメントにある通り”In a reference-counted environment, this method behaves the same as release.(参照カウンタ方式の環境の場合、このメソッドの振る舞いはreleaseとおなじです。)”、releaseと同じ動作なのでreleaseでもいいのだけど、来るべきGCに備えてdrainの方が良いのではないかと思う。

裏スレッドでは、他にもスレッドセーフではないシステムコールなんかが使えないというのも心にとめておく必要がある。裏スレッドでUIGraphicsBeginImageContextを使って画像を作成するというようなことが出来ない(絶対に出来ないわけではないけど、動作の仕組みをよく考慮する必要がある)。

読み込みが終わったら終了通知を行う。ここで使うのがCocoaフレームワークのNotificationという仕組みだ。NotificationCenterというところに、NotificationをPostすると、それを待っているオブジェクトにその通知が届くという仕組みだ。

- (void)_sendCompletedNotify
{
[[NSNotificationCenter defaultCenter] postNotificationName:PhotoImageDidLoadNotification object:self];
}

ここのPhotoImageDidLoadNotificationというのはただのNSStringで

NSString * const PhotoImageDidLoadNotification = @"PhotoImageDidLoadNotification";

などと定義しておく。

Model側を実装したのでView側を実装する。

@interface PhotoView : UIView
{
PhotoImage *_photoImage;
}
@property (nonatomic,retain) PhotoImage *photoImage;
@end

photoImageがセットされたときに、画像のリクエストとその完了通知を受け取る登録を行う。まずは以前のオブジェクトからのNotificationの受信を解除する(Viewを使い回してModelを入れ替えるという形を想定している)。

- (void)setPhotoImage:(PhotoImage *)photoImage
{
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
if (_photoImage)
[center removeObserver:self name:PhotoImageDidLoadNotification object:_photoImage];

そして、新しいオブジェクトからのNotificationの受信を登録して、画像のロードをリクエストする。

[photoImage retain];
[_photoImage release];
_photoImage = photoImage;
if  (photoImage)
{
[center addObserver:self selector:@selector(_photoImageDidLoad:) name:PhotoImageDidLoadNotification object:photoImage];
[photoImage requestImage];
}
}

通知の受け取りは、

- (void)_photoImageDidLoad:(NSNotification *)notification
{
[self setNeedsDisplay];
}

等とする。Notificationの送信と受け取りは同じスレッドのようなので、受け取った後にメインスレッドで実行したい場合は、NSObjectのメソッドperformSelectorOnMainThread:withObject:waitUntilDone:を使えばいい。

文章で書くと長くなってしまうのだけど、コードはとても短い。これだけで書けてしまうCocoaはとてもパワフルなフレームワークだと思うのだ。