ARCのおかげでメモリリークの心配も無くなりヒャッハーしていたところ、メモリが枯渇するという問題に打ち当たりました。
よくARCでも以下の話は耳(目)にしますね。
- autoreleaseオブジェクトがループ処理などで溜まり、メモリが枯渇する(@autoreleasepoolで回避)
- [UIImage imageNamed:]が画像をキャッシュするから避けた方がよい
- 相互参照などでメモリがリークしてしまう(弱参照などで回避)
この辺は自分でも気をつけて開発をしていたつもりなのですが、動作テストを行っているうちに、「ゆっくりと操作しているうちは問題無いのに、素早く操作をするとメモリが枯渇する」という挙動がみられました。
というわけで、ARCの挙動を確認すべく、簡単なサンプルを作ってみました。
#import "ViewController.h"
#define ImageSize CGSizeMake(2000, 2000)
@implementation ViewController
{
int _click;
UIImageView *_imageView;
}
- (void)loadView
{
[super loadView];
self.view.backgroundColor = [UIColor whiteColor];
self.view.multipleTouchEnabled = YES;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"%d", ++_click);
// 以前作ったものを破棄
if (_imageView != nil) {
[_imageView removeFromSuperview];
_imageView = nil;
}
// でかい画像を作る
UIGraphicsBeginImageContext(CGSizeMake(2000, 2000));
[[UIColor colorWithHue:(float)rand() / RAND_MAX saturation:1 brightness:1 alpha:1] set];
CGRect rc = {0};
rc.size = ImageSize;
UIRectFill(rc);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// ImageViewで貼る
_imageView = [[UIImageView alloc] initWithImage:img];
_imageView.frame = CGRectMake(50, 50, 300, 300);
[self.view addSubview:_imageView];
}
@end
ソースを見ていただければ一目瞭然ですが、タッチすると大きなサイズのUIImageを作って、UIImageViewにはめ込んでビューに追加するというものです。
タップ二度目以降は、前回のUIImageViewをremoveFromSuperviewしてnilしているので、参照カウンタが0になり、UIImageViewもUIImageもARCにより解放されるであろうと思って組んだものです。
(要はUIImageViewとUIImageは最大1つしか存在しないつもりで書いたプログラムです。)
実際にiPad2(iOS6.1.3)で動かしてみると、ゆっくりとタップしている場合は何回タップしてもメモリが枯渇する事はありませんが、両手を使って(マルチタッチ対応なので)素早く連打すると、だいたいカウンタ40ぐらいでメモリが枯渇してアプリがクラッシュします。
こういった挙動を見ると、「CPUが暇な時に何か解放する処理が走っている」と考えるのが自然だと思うのですが、そういった挙動が果たして公式な動作として存在するのかどうか。いや、有るならば本当にメモリが枯渇しそうになったときにアプリが一瞬止まっても構わないので溜まっているメモリを強制的に解放する術がないのか(むしろ勝手にやってくれないのか)など、疑問は尽きません。
この辺、お詳しい方がいらっしゃいましたら、メールまたはツイッター(@takabosoft)などでご教示願えると助かります。
(なんだかすごく初歩的な話のような気がしてならないですが。)
ちなみに、今回のテストプログラムに限っては以下の一行を入れると素早く操作しても枯渇しないで済みます。
// 以前作ったものを破棄
if (_imageView != nil) {
_imageView.image = nil; // この一行を入れる
[_imageView removeFromSuperview];
_imageView = nil;
}