2010年10月8日金曜日

viewDidUnload は呼ばれない(メモリ不足時だけ呼ばれる)

UIViewControllerの画面を閉じる時に通常 viewDidUnloadが呼び出されることは無い。このメソッドが呼び出されるのはメモリ不足の時のみ。名前が viewDidLoad と対になっているが、動作は対になっていない。


UIViewController の各種メソッド呼び出しタイミング


通常の動作
viewDidLoad
 |
 |閉じる
 ↓
dealloc

メモリ不足発生時の動作
viewDidLoad
 |
 |メモリ不足発生
 ↓
didReceiveMemoryWarning
 |
viewDidUnload
 |
 :
 ↓
viewDidLoad
 |
 |閉じる
 ↓
dealloc
てっきり通常 viewDidUnload が呼び出されると勘違いしていたがそうではない。
となると、メモリの解放は viewDidUnload だけでは駄目で dealloc にも実装しないとメモリリークが発生することになる。サンプルを作って確認してみた。


サンプル


RootとSub、SubSubの3つの UIViewController を用意して、SubViewController上の UIImageViewの retainCount を確認する。


RootViewController 上の childImageView プロパティは、SubViewControllerを閉じて deallocが呼ばれた後に retainCountがどうなっているかを確認する為に assign で参照している(assignだと retainCountは増えない)。


RootViewControllerのコード
@interface RootViewController : UIViewController {
 UIImageView* childImageView;
}

@property (nonatomic, assign) UIImageView* childImageView;

- (IBAction)next:(id)sender;

@end
- (void)printRetainCount
{
 NSLog(@"[Root] retainCount=%d", [self.childImageView retainCount]-1);
}

- (void)viewDidAppear:(BOOL)animated {
 
 if (self.childImageView) {
  [self performSelector:@selector(printRetainCount)
       withObject:nil
       afterDelay:1.0];
 }
}
- (IBAction)next:(id)sender
{
 SubViewController* vc = [[SubViewController alloc]
            initWithNibName:@"SubViewController" bundle:nil];
 vc.rootViewController = self;
 [self.navigationController pushViewController:vc animated:YES];
 [vc release];
}
SubViewControllerを閉じて RootViewController へ戻ってきた時に viewDidAppear: で retainCountを表示している。なお SubViewControllerの dealloc の後で確実に確認するために1秒間の遅延をもたせている。

次に SubViewController。
@class RootViewController;
@interface SubViewController : UIViewController {
 
 UIImageView* imageView;
 RootViewController* rootViewController;

}

@property (nonatomic, retain) IBOutlet UIImageView* imageView;
@property (nonatomic, retain) RootViewController* rootViewController;

-(IBAction)next:(id)sender;

@end

@implementation SubViewController
@synthesize imageView;
@synthesize rootViewController;

- (void)viewDidLoad {
    [super viewDidLoad];

 // for debug
 [self.imageView retain];

 NSLog(@"[Sub ] viewDidLoad|retainCount=%d", [self.imageView retainCount]-1);
 
 rootViewController.childImageView = self.imageView;
}

- (void)didReceiveMemoryWarning {
    NSLog(@"[Sub ] didReceiveMemoryWarning|retainCount=%d", [self.imageView retainCount]-1);
    [super didReceiveMemoryWarning];    
}

- (void)viewDidUnload {
    [super viewDidUnload];
 NSLog(@"[Sub ] viewDidUnload|retainCount=%d", [self.imageView retainCount]-1);
 self.imageView = nil;
}

- (void)dealloc {
    NSLog(@"[Sub ] dealloc|retainCount=%d", [self.imageView retainCount]-1);
    [super dealloc];
}

- (IBAction)next:(id)sender
{
 SubSubViewController* vc = [[SubSubViewController alloc]
           initWithNibName:@"SubSubViewController" bundle:nil];
 [self.navigationController pushViewController:vc animated:YES];
 [vc release];
}
@end
retainCount==0 のケースはメモリ解放後のオブジェクトになる為、実行時には retainCountメッセージを送るとアプリがクラッシュする。そこで -[SubViewController viewDidLoad]内で retain して+1水増ししておき、表示する箇所で−1して正しい数値に直している。

実行してみよう。まず通常のケース。
(1)pushViewController:SubViewController
(2)popViewController

実行結果
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=1
retainCount=2の内訳は [1]UIImageViewの親ビューによるretain [2]@property(retain)宣言とIBOutlet接続によるretainによる。また、viewDidUnload が呼ばれていないことがわかる。

一方、RootViewController へ戻った時に retainCountが1残っている。SubViewController は dealloc が呼ばれているので破棄されたことになるが、IBOutlet(retain)で接続した UIImageViewはメモリに残ったまま、つまりメモリリークが起きているのがわかる。これは dealloc 内で IBOutlet(retain)接続した UIImageViewの解放を行っていないため。viewDidUnloadで解放(=nil)しているが、このメソッドは通常は呼ばれない。

つまり IBOutlet(retain)接続したオブジェクトは deallocで解放しない場合メモリリークを引き起こす。

dealloc内に解放コードを追加してみる。
- (void)dealloc {
 NSLog(@"[Sub ] dealloc|retainCount=%d", [self.imageView retainCount]-1);
 
 self.imageView = nil;
    [super dealloc];
}

実行結果
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0
retainCountは0となり、メモリリークが起きていない。


最後にメモリ不足をシミュレートしたケース (dealloc内解放コードあり)。

(1)pushViewController:SubViewController
(2)pushViewController:SubSubViewController
(3)メモリ不足をシミュレート
(4)popViewController
(5)popViewController

[Sub ] viewDidLoad|retainCount=2
Received simulated memory warning.
[Sub ] didReceiveMemoryWarning|retainCount=3
[Sub ] viewDidUnload|retainCount=1
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0

メモリ不足発生直後に viewDidUnload が呼ばれているのがわかる。


奇妙なのが didReceiveMemoryWarningが呼ばれた時点で retainCountが3に増えている。これはなんだろう?

試しに Root => Sub => SubSub のケースをメモリ不足なしで実行してみた。
[Sub ] viewDidLoad|retainCount=2
[Sub ] dealloc|retainCount=3
[Root] retainCount=0
すると SubViewController の dealloc 時点では retainCountが3になっている。Sub => SubSub へ遷移することによって何故か retainCountが増加している。しかも Rootに戻った時はきちんと0にもどっている。

retainCount がどの時点で増えたか確認するため、SubSubController が表示された時点での retainCountを確認するコードを追加する。
@interface SubSubViewController : UIViewController {
 UIImageView* parentImageView;
}
@property (nonatomic, assign) UIImageView* parentImageView;


@end
- (void)viewDidLoad {
    [super viewDidLoad];
 NSLog(@"[SubSub] viewDidLoad|retainCount=%d", [self.parentImageView retainCount]-1);
  
}
- (void)viewWillAppear:(BOOL)animated {
 NSLog(@"[SubSub] viewWillAppear|retainCount=%d", [self.parentImageView retainCount]-1);
 
}
- (void)viewDidAppear:(BOOL)animated {
 NSLog(@"[SubSub] viewDidAppear|retainCount=%d", [self.parentImageView retainCount]-1);
 
}

SubViewControllerへ以下を追加
- (void)viewWillDisappear:(BOOL)animated
{
 NSLog(@"[Sub ] viewWillDisappear|retainCount=%d", [self.imageView retainCount]-1);
}
- (void)viewDidDisappear:(BOOL)animated
{
 NSLog(@"[Sub ] viewDidDisappear|retainCount=%d", [self.imageView retainCount]-1);
}
- (IBAction)next:(id)sender
{
          :
 [self.navigationController pushViewController:vc animated:YES];
 vc.parentImageView = self.imageView;
          :
}

実行結果
[Sub ] viewDidLoad|retainCount=2
[SubSub] viewDidLoad|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[SubSub] viewWillAppear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[SubSub] viewDidAppear|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[Sub ] dealloc|retainCount=3
[Root] retainCount=0
SubSubViewControllerを pushした時点で retainCountが1増加している。理由はわからず。うーむ?
(わかる方、どうぞコメントに書き込んで教えて下さい)

メモリ不足シミュレート時の動作も記録として残しておく。
[Sub ] viewDidLoad|retainCount=2
[SubSub] viewDidLoad|retainCount=3
[Sub ] viewWillDisappear|retainCount=3
[SubSub] viewWillAppear|retainCount=3
[Sub ] viewDidDisappear|retainCount=3
[SubSub] viewDidAppear|retainCount=3
Received simulated memory warning.
[Sub ] didReceiveMemoryWarning|retainCount=3
[Sub ] viewDidUnload|retainCount=1
[Sub ] viewDidLoad|retainCount=2
[Sub ] viewWillDisappear|retainCount=2
[Sub ] viewDidDisappear|retainCount=2
[Sub ] dealloc|retainCount=2
[Root] retainCount=0


ソースコード


GitHubからどうぞ。
ViewControllerMemorySample at 2010-10-08b from xcatsan's iOS-Sample-Code - GitHub


まとめ


viewDidLoad で確保したメモリは、viewDidUnload および dealloc 両方で解放する

以前書いた記事では viewDidLoadで確保したメモリの deallocでの解放には触れていなかった。これは後日訂正する。
Cocoaの日々: UIViewController でのメモリ管理見本
解放コードのサンプルは(後日)そちらを参照されたい。


参考情報


viewDidUnload と dealloc でのメモリ解放の話題は調べてみると結構見つかって、多くの人が戸惑っている様子が伺える。

OBJECTIVE C - Release in viewDidUnload and dealloc both? - efreedom

When should I release objects in -(void)viewDidUnload rather than in -dealloc? - Stack Overflow

答えはどれも先のまとめで示した通りで両方でメモリ解放することが推奨されている。


- - - -
今回の件はかなりショックだった。過去のプログラムを見直せねば。。

2 件のコメント:

  1. サンプルを動かしてみたのですが、viewDidUnloadで

    self.imageView = nil;

    を行った時と、

    // self.imageView = nil;

    コメントアウトを行った時と、retain countは変わらないようです。これは、つまり、nibで生成したものはここでの解放処理は不要ということなんでしょうか?
    試しに、

    NSLog(@"[Sub ] viewDidUnload|retainCount=%d", [self.imageView retainCount]-1);
    self.imageView = nil;
    NSLog(@"[Sub ] viewDidUnload|retainCount=%d", [self.imageView retainCount]-1);

    解放後にもログを出力するようにしてみたら、後のものは retainCount=-1 となっていました。

    メモリ関係は謎が多いですね

    返信削除
  2. こんにちは。指摘の件確認してみたところ、その通りでした。
    viewDidUnload 内の self.imageView = nil の有無に関係なく解放処理がうまく入っていますね。

    よくわらかないのが、self.imageView = nil の直前で retainCount = 1 だったのが、直後に -1 になっていること。retainCount が2も減っている。

    #そもそも計測の仕方に問題があるのだろうか。


    指摘ありがとうございました。
    もう少し調べてみます。

    返信削除