UIImage::imageNamed:を使う上で気をつけること

ゼミの後輩が、実機で自作アプリを動かそうとしたら、メモリ不足で動かなかったというので、ソースコードを見せてもらった。
内容は単純なパラパラ漫画。UIImageViewのimageを次々に変えるというもの。

標準のアニメーション機能は非効率

最初に、目に付いたのはUIImageViewのアニメーション機能。
この機能は、UIImageをNSArrayに格納して、UIImageViewのanimationImagesプロパティに渡すだけで、自動的にイメージを切り替えてくれるという簡単で便利なものだけれど、デメリットとして、最初にすべてのイメージのUIImageをインスタンス化しなければならないということがある。そのため、読み込み時間とメモリ消費のコストが大きい。

そこで、自分が他のアプリのために作ったアニメーションクラスを移植した。
このアニメーションクラスは、NSTimerを内部に持ち、設定した時間間隔で、その都度UIImageを作って、UIImageViewに入れるというもの。まあ、animationImagesを使うよりは効率的。
使用メモリを吐くコード(以下に記載)を入れて実行。

#import <mach/mach.h>

struct task_basic_info t_info;
mach_msg_type_number_t t_info_count = TASK_BASIC_INFO_COUNT;
 
if (task_info(current_task(), TASK_BASIC_INFO, (task_info_t)&t_info, &t_info_count)!= KERN_SUCCESS)
{
    NSLog(@"%s: Error in tank_info(): %s", __FUNCTION__, strerror(errno));
}

u_int mem = t_info.resident_size;
NSLog(@"Used memory: %0.1f MB", mem/1024.0/1024.0);

しかし、結果は同じくメモリ不足。

前回と違うのは、パラパラが再生されて、しばらくすると、
received memory warning level 1
そして、そのすぐ後に
received memory warning level 2
が出て落ちるということ。

メモリリークか? と思ったが、UIImage生成にはimageNamed:を使っているので、勝手に解放されるはず。
Objective-Cのautoreleaseについて自分の理解が足りないのかと資料を読み返したものの、autoreleaseを管理するNSAutoreleasePoolは、イベントループが一周されるごとにプール内のオブジェクトに一斉にreleaseを送ると書いてある。
今回はそこまで高速にUIImageを生成しているわけでもないので、メモリが占有されるはずもなく……。
(それに、このクラスは他のアプリでの使用時にテスト済みで致命的なリークもないはず)

imageNamed:は、自動的にイメージをキャッシュする

途方に暮れていたが、昔どこかで「imageNamed:が裏側でイメージをキャッシュをしている」というのを読んだのを思い出し、さっそく調べてみたところ、たしかにimageNamed:は裏でファイル名をKeyにして、イメージのポインタをコンテナにため込んでいることが判明。しかも、自分の調べた範囲では、このキャッシュは手動では解放できないらしい。
ちなみに、キャッシュ機能があるのはimageNamed:だけ。initWithContentsOfFile:やimageWithContentsOfFile:はしない。

ということで、アニメーションクラス内部のUIImage生成部分を、initWithContentsOfFile:に書き換えて、使用メモリの推移を見てみると、一定に。きちんと解放されていることが確認できた。

必要なときに力を発揮するimageNamed:

小さなアプリが望ましいモバイル環境では、imageNamed:は局所的(表示する機会が多い画像・同じ画像を使い回すときなど)に使うに留めるべきだと思われる。

ただ、ひとつ注意しなければならないのは、imageNamed:の代わりとなるinitWithContentsOfFile:やimageWithContentsOfFile:は、NSBundleのpathForResource:ofType:を使用して、パスを渡しているので、Retinaディスプレイ用に自動的に@2xを表示することに対応していない。
そこは自分でデバイス情報を取得して、ファイル名に@2xを追加する処理を書く必要がある。