Objective-C
Xcode
iOS
ARC

ARC環境でメモリが枯渇するケース

More than 3 years have passed since last update.

※以前ブログで書いた内容ではありますが、宙ぶらりんになっていたため、再編してお送りします。

概要

ARC環境になり、メモリの管理が大変楽になりましたが、注意しないとメモリが枯渇するケースはいくつかあります。

有名どころは以下の2点でしょうか。

ところが、以前とあるアプリを作っている時に、
ゆっくり操作している時は大丈夫だけど、素早く操作するとメモリ不足でクラッシュするという症状に出くわしました。

この症状について記録を残しておきます。

  • Xcode 5.1.1 + SDK 7.1 + iPad Air + iOS 7.1.1

再現方法

以下、再現を目的としたシンプルコードです。

ViewController.m
#import "ViewController.h"

#define ImageSize CGSizeMake(3000, 3000)

@implementation ViewController
{
    int _tap;
    UIImageView *_imageView;
}

- (void)loadView
{
    [super loadView];
    self.view.backgroundColor = [UIColor whiteColor];
    self.view.multipleTouchEnabled = NO;
    _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 300, 300)];
    [self.view addSubview:_imageView];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"tap=%d", ++_tap);

    // でかい画像を作る
    UIGraphicsBeginImageContext(ImageSize);
    [[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.image = img;
}

@end

一枚UIImageViewを貼って、タップする度に巨大な画像を作ってImageViewにセットするだけの内容です。
ARC環境なので、この程度でメモリリークなど起きるはずが無い!と思ったら大間違い。

まず、0.5秒間隔ぐらいでタップした結果がこちら。

2014-05-29 09:28:45.345 MemoryTest[24093:60b] tap=1
2014-05-29 09:28:46.427 MemoryTest[24093:60b] tap=2
2014-05-29 09:28:46.977 MemoryTest[24093:60b] tap=3
2014-05-29 09:28:47.511 MemoryTest[24093:60b] tap=4
2014-05-29 09:28:48.044 MemoryTest[24093:60b] tap=5
2014-05-29 09:28:48.544 MemoryTest[24093:60b] tap=6
2014-05-29 09:28:49.077 MemoryTest[24093:60b] tap=7
(中略)
2014-05-29 09:29:35.614 MemoryTest[24093:60b] tap=97
2014-05-29 09:29:36.113 MemoryTest[24093:60b] tap=98
2014-05-29 09:29:36.564 MemoryTest[24093:60b] tap=99
2014-05-29 09:29:37.031 MemoryTest[24093:60b] tap=100

100回タップしても大丈夫ですね。

続いて、(両手使って)超連打した結果がこちら。

2014-05-29 09:30:06.484 MemoryTest[24104:60b] tap=1
2014-05-29 09:30:06.565 MemoryTest[24104:60b] tap=2
2014-05-29 09:30:06.640 MemoryTest[24104:60b] tap=3
2014-05-29 09:30:06.718 MemoryTest[24104:60b] tap=4
2014-05-29 09:30:06.792 MemoryTest[24104:60b] tap=5
2014-05-29 09:30:06.866 MemoryTest[24104:60b] tap=6
2014-05-29 09:30:06.943 MemoryTest[24104:60b] tap=7
2014-05-29 09:30:07.020 MemoryTest[24104:60b] tap=8
2014-05-29 09:30:07.095 MemoryTest[24104:60b] tap=9
2014-05-29 09:30:07.170 MemoryTest[24104:60b] tap=10
2014-05-29 09:30:07.245 MemoryTest[24104:60b] tap=11
2014-05-29 09:30:07.319 MemoryTest[24104:60b] tap=12
2014-05-29 09:30:07.408 MemoryTest[24104:60b] tap=13
2014-05-29 09:30:07.540 MemoryTest[24104:60b] tap=14
2014-05-29 09:30:07.648 MemoryTest[24104:60b] tap=15
2014-05-29 09:30:07.773 MemoryTest[24104:60b] Received memory warning.
2014-05-29 09:30:07.774 MemoryTest[24104:60b] tap=16
2014-05-29 09:30:07.904 MemoryTest[24104:60b] tap=17
2014-05-29 09:30:08.041 MemoryTest[24104:60b] Received memory warning.
2014-05-29 09:30:08.045 MemoryTest[24104:60b] tap=18

19回ぐらいタップしたらクラッシュしました...

スクリーンショット 2014-05-29 10.04.08.png

Profileしてみた

スクリーンショット 2014-05-29 10.12.53.png

00:10で初回タップしてから、Instrumentの設定をいじっていたため、しばらく変化無しの時間が続いています。
00:25から00:35まではゆっくりタップしていましたが、UIImage# Livingは1で安定していました。
00:35からは猛烈にタップしたので、メモリ使用量が増加していき、その後UIImage# Livingも21となり、クラッシュしてしまいました。

考察

UIImageの解放が遅れているせいでメモリが枯渇しているであろうことは予想が付きますので、@autoreleasepoolを入れてみた結果、症状の改善がみられました。

暫定回避コード
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"tap=%d", ++_tap);

    @autoreleasepool {
        // でかい画像を作る
        UIGraphicsBeginImageContext(ImageSize);
        [[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.image = img;
    }
}

ただ、@autoreleasepoolはスタックの上流のどこかに居るはずで(実際ゆっくりタップしているうちは解放されているわけですので)手動で入れる必要は無いと思っていたのですが、これらの挙動を見ると、
CPUが暇な時にreleaseを実行するプール機構が存在している???などと考えてしまうのですが、どうなんでしょうか。

もしARCの挙動にお詳しい方がいらっしゃいましたら、この辺りの症状についてコメントを頂ければ幸いです。