ネットワークごしの画像をUIImageViewに表示したい。かつ、画像をキャッシュして次回からはすぐに表示したい。よくある話です。

githubにコミットされているSDWebImageを使うとこれらを手軽に実現できます。 今回はこのSDWebImageの使い方と内部実装などを説明したいと思います。

※ 今回はversion3.0時点のものを説明しています。多分大幅に変わることはないと思いますが・・・

https://github.com/rs/SDWebImage

MIT Licenseです。

URL指定で画像を表示する

SDWebImageを導入するとカテゴリメソッドが追加されます。

まずUIImageView+WebCache.hをimportします。よく使うようであればプリコンパイルヘッダでimportすると良いかもしれません。

#import <SDWebImage/UIImageView+WebCache.h>

これでUIImageViewにカテゴリメソッドが追加されURL指定で画像の表示ができるようになります。

NSURL *imageURL = [NSURL URLWithString:@"画像のURL"];
UIImage *placeholderImage = [UIImage imageNamed:@"画像読み込み完了までに表示するリソース画像"];
[_imageView setImageWithURL:imageURL
           placeholderImage:placeholderImage];

涙がでるほどに簡単ですね。当然、UITableViewCell#imageViewに対しても実行できるのでセル内にネットワーク画像を表示する場合も同様に実現できます。他にもMKAnnotationView+WebCache、UIButton+WebCacheをimportすれば、AnnotationViewやButtonの画像もURL指定で表示できます。

さらにメモリキャッシュとディスクキャッシュを行なってくれます。

キャッシュの管理

キャッシュの管理はSDWebImageManager#imageCache、つまりSDImageCacheクラスで行われてます。気になるのはキャッシュをクリアするタイミングはいつなのか?ということでしょう。

メモリキャッシュはiOS SDKで用意されているNSCacheを使っているので、そのポリシーに従って自動的にキャッシュの一部をクリアしてメモリを確保してくれるでしょう。

またSDImageCache.mのinitWithNamespaceメソッドを見るとわかるのですが、メモリ警告が通知された時にメモリキャッシュは完全にクリアされます。ディスクキャッシュのクリアはアプリ終了時に行われています。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearMemory) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanDisk) name:UIApplicationWillTerminateNotification object:nil];

メモリキャッシュはclearMemoryで完全にクリアされますが、ディスクキャッシュはcleanDiskで期限切れのファイルのみクリアしています。ディスクキャッシュの期限はデフォルトで1週間に設定されています。通常はこれで問題ないでしょう。

変更したい場合はSDImageCache#maxCacheAgeへ、その値を代入します。

SDImageCacheのキャッシュクリアに関するメソッドまとめ

  • clearMemory – メモリキャッシュを完全にクリアする
  • clearDisk – ディスクキャッシュを完全にクリアする
  • cleanDisk – ディスクキャッシュから期限が切れたものをクリアする

明示的にクリアしたい時は、これらを直接呼ぶとよいでしょう。

明示的なキャッシュ

UIImageView+WebCacheで追加されたカテゴリメソッドを使用すれば表示と同時にキャッシュも行なってくれて便利ですが、場合によっては明示的にキャッシュしたい場合があると思います。例えば以下の様なケースでその必要にかられるでしょう。

  • すぐに画像表示はしないがダウンロードだけしてキャッシュしておきたい
  • 画像取得に認証用トークンが必要でPOSTで行わねばならず単純なURL指定では表示できない

画像取得は状況に応じてプログラミングを行うとして、ここでは画像をキャッシュする方法のみ説明します。

まずはSDWebImageManagerからSDImageCacheオブジェクトを取得します。

SDImageCache *imageCache = [[SDWebImageMangaer sharedManager] imageCache];

次にstoreImageを使ってUIImageオブジェクトをキャッシュに入れます。

[imageCache storeImage:image forKey:cacheKey]

キャッシュの際にはキャッシュキーが必要になります。キャッシュから取り出す際もキャッシュキーが必要になります。当然、画像ごとにユニークなキャッシュキーを用意することになります。

さきほどのUIImageView+WebCacheのカテゴリメソッドを使って画像キャッシュする場合は、内部でURLをキャッシュキーとしています。ほとんどの場合はURLと画像は一対一なので、これにならってキャッシュキーをURLとすればよいでしょう。

[imageCache storeImage:image forKey:imageURL.absoluteString]

ただ問題は画像取得の際にサイズなどをPOSTパラメータで指定する必要があるといった場合、URLをキャッシュキーとして使うことはできません。このようなケースでは独自にキャッシュキーを作るルールが必要です。(キャッシュキーはPOSTパラメータをGETパラメータに変換したURLにするなど)

次に画像の取り出しについて。getImageというメソッドはなく、queryDiskCacheForKeyを使います。このメソッドは画像取得結果をブロック関数(done)の引数(image)へ返します。

以下はブロック関数へ返すまでの動作です。

  • まずメモリキャッシュを確認、あればそれをブロック関数の引数に渡す
  • 次にファイルキャッシュを確認、あればそれをブロック関数の引数に渡す
  • なければnilをブロック関数の引数に渡す

注意すべきなのは、ディスクキャッシュから取得した時、ブロック関数は非同期で終了するのですが、メモリキャッシュの場合は同期して終了するということです。ここは少し注意が必要でしょう。非同期前提でプログラミングするとバグの原因になりえます。

_image = nil; // メンバ変数をnilにしておく
SDWebImageManager *imageManager = [SDWebImageManager sharedManager];
[imageManager.imageCache queryDiskCacheForKey:@"*** 取り出す画像のキャッシュキー ***" done:^(UIImage *image, SDImageCacheType cacheType)
{
  if (image) {
    _image = image; // 取得したimageをメンバ変数へ代入
  } else {
    // キャッシュがないのでネットワークから読み出すなどの処理をする
  }
}];
// メモリキャッシュから取り出した際には、同期してブロック関数が終了する。よってこの時点で_image != nil
// ファイルキャッシュから取り出した際には、非同期でブロック関数が終了する。よってこの時点では_image == nil
BOOL isImageLoaded = _image != nil;

ディスクキャッシュのファイル名

ディスクキャッシュの際は、キャッシュキーをMD5したファイル名で保存されます。キャッシュキーが画像ごとにユニークであれば特に問題はないでしょう。

まとめ

  • UIImageViewにURL指定で画像表示ができる。さらにキャッシュもする
  • MKAnnotationView、UIButtonも同様にURLで画像表示ができる。
  • 明示的にキャッシュしたい場合はSDImageCache#storeImageを使う
  • 明示的にキャッシュから取り出したい時はSDImageCache#queryDiskCacheForKey:doneを使う

便利!

ちなみに自分はnimbusのネットワーク画像表示のNINetworkImageViewは使わずに、他のnimbusのクラスとSDWebImageを併用して使っています。nimbusはファイルキャッシュは、NSURLConnectionのキャッシュに依存しているようで、ちょっと扱いづらいのと仕組み的に今ひとつな気がしました。あくまで主観ですが。

SDWebImageはソースもシンプルで扱いやすいと思います。