[関連情報] Cocoaの日々: viewDidUnload は呼ばれない(メモリ不足時だけ呼ばれる)
UIViewController 内で使うオブジェクトのメモリ管理(作成と開放のタイミング)と初期設定についてまとめてみた。
実装見本
4種類のインスタンス変数をもつケースについて考えてみる。
[a] nibから生成されるコントロール
[b] 実行時に作成するコントロール
[c] 他クラスへの公開プロパティ
[d] 内部で使うインスタンス変数
ヘッダはこんな感じ。
@interface SampleViewController : UIViewController { UITextField* textField; // [a] nibから生成されるコントロール UIImageView* imageView; // [b] 実行時に作成するコントロール NSString* name; // [c] 他クラスへの公開プロパティ NSMutableArray* history; // [d] 内部で使うインスタンス変数 } @property (nonatomic, retain) IBOutlet UITextField* textField; @property (nonatomic, retain) UIImageView* imageView; @property (nonatomic, retain) NSString* name; @end
続いて実装。メモリ管理にのみフォーカスして書いたコードなので意味のある処理は行っていない。
#import "SampleViewController" @implementation SampleViewController @synthesize textField; @synthesize name; @synthesize imageView; - (id)init { if (self = [super init]) { // [c] 他クラスへの公開プロパティ [作成][初期設定] self.name = @"no name"; // [d] 内部で使うインスタンス変数 [作成][初期設定] history = [[NSMutableArray alloc] init]; } return self; } - (void)viewDidLoad { [super viewDidLoad]; // [a] nibから生成されるコントロール [初期設定] self.textField.text = @"(non)"; // [b] 実行時に作成するコントロール [作成][初期化] self.imageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"sample1.png"]] autorelease]; [self.view addSubview:self.imageView]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.textField.text = self.name; } - (void)viewDidUnload { self.textField = nil; // [a] nibから生成されるコントロール [開放] self.imageView = nil; // [b] 実行時に作成するコントロール [開放] } - (void)dealloc { self.textField = nil; // [a] nibから生成されるコントロール [開放] self.imageView = nil; // [b] 実行時に作成するコントロール [開放] self.name = nil; // [c] 他クラスへの公開プロパティ [開放] [history release]; // [d] 内部で使うインスタンス変数 [開放] [super dealloc]; } @end
4種類のインスタンス変数が参照するオブジェクトはそれぞれの生成タイミングの違いから、開放タイミングも異なる。順に追って見る。
[a] nibから生成されるコントロール
[作成] UIKitが自動的に nibから読み込んでメモリ内にインスタンス化する。その後 UIViewControllerのインスタンス変数へ設定してくれる。
[初期設定] 通常 InterfaceBuilderのインスペクタを使って設定を行うが、プログラムで初期設定を行う場合は viewDidLoadで行う。
[開放] viewDidUnload と dealloc の両方で行う。
コントロールの親viewはメモリ不足になると一旦開放されることがある。この為、UIViewControllerに比べて生存期間が短い上に、何度も作成と破棄が繰り返される場合が起こりうる。viewの開放タイミング(viewDidUnload)で、開放を行う。
[b] 実行時に作成するコントロール
[作成] viewと連携するコントロールの場合は、viewがインスタンス化して使えるようになっている必要がある。この為、作成は viewが使えるようになった時点、すなわち viewDidLoad で行う。
[初期設定] 作成と同様 viewDidLoad で行う。
[開放] viewDidUnLoad と dealloc の両方で行う。
作成を UIViewController の初期化タイミングで行うケースもある。この場合、開放は dealloc で行う。
[c] 他クラスへの公開プロパティ
[作成] 作成は UIViewController の初期化タイミングで行う。作成しないこともある(初期値 nilの場合など)。
[初期設定] 作成と同様。
[開放] dealloc で行う。
[d] 内部で使うインスタンス変数
[作成] 作成は UIViewController の初期化タイミングで行う。そうでないとクラス内で利用できない。
[初期設定] 作成と同様。
[開放] dealloc で行う。
まとめ
以上を表にまとめると次のようになる。
種類 | init* | viewDidLoad | viewDidUnLoad | dealloc |
---|---|---|---|---|
[a] nibから生成される コントロール | - | [初期設定] | [開放] | [開放] |
[b] 実行時に作成する コントロール | - | [作成] [初期設定] | [開放] | [開放] |
[c] 他クラスへの 公開プロパティ | [作成] [初期化] | - | - | [開放] |
[d] 内部で使う インスタンス変数 | [作成] [初期化] | - | - | [開放] |
メモリ開放の基本ルール
- nib 内にあるコントロールの開放は viewDidUnloadとdealloc両方で行う
- viewDidLoad で作成したコントロールの開放は viewDidUnloadとdealloc両方で行う
- init* で作成したインスタンスの開放は deallocで行う
- 他クラスから代入されたプロパティの開放は deallocで行う
その他
UIViewControllerの初期化
UINavigationBar を使っている場合は大抵の場合 init を使うので initで初期化を行う。一方、nibから UIViewController を作成する場合は initの代わりに initWithCoder: で初期化を行う(initや initWithNibName:bundle: は呼び出されない)。
メモリ不足
メモリ不足になった場合に didReceiveMemoryWarning が呼ばれる。この時、インスタンス変数で大きなサイズのデータを扱っている場合は必要に応じて開放する。次の2つが対象。
[c] 他クラスへの公開プロパティ
[d] 内部で使うインスタンス変数
viewに紐付けられる [a][b]などは、viewが開放された時に viewDidUnload が呼び出されるのでそのタイミングで開放される(このブログで示した実装になっているとして)。
参考情報
- Resource Programming Guide: Nib Files
- Nib経由でインスタンス化されるオブジェクトの初期化順序が解説されている。
- Cocoaの日々: UIView上のコントロールへの IBOutlet接続は retain で(assign改め)
- nib上のコントロールへの接続方法についての記事
ぽげむた
2010年10月17日 10:32
分かりやすい記事をありがとうございます。とても納得できました。
ところで、[a]と[b]については、viewDidUnLoadのみで解放処理を行っていますが、viewDidUnLoadはメモリ不足の時に呼ばれると理解しています。ですので、deallocでも[a]と[b]の解放を行わないとメモリリークしてしまうのではないかと考えてしまうのですが、これは不要なのでしょうか?
ぽげむた
2010年10月17日 10:32
分かりやすい記事をありがとうございます。とても納得できました。
ところで、[a]と[b]については、viewDidUnLoadのみで解放処理を行っていますが、viewDidUnLoadはメモリ不足の時に呼ばれると理解しています。ですので、deallocでも[a]と[b]の解放を行わないとメモリリークしてしまうのではないかと考えてしまうのですが、これは不要なのでしょうか?
xcatsan says:
2010年10月19日 16:33
ぽげむたさん、こんにちは。
指摘の通り dealloc での解放が必要で、このままではメモリリークが発生します。後ほど記事を見なおして描き直しておきます。
# viewDidUnloadは毎回呼ばれるものと勘違いしていました。
# メモリリーク時しか呼ばれないことを今更ながら知ってちょっとショックをうけています。。
有益な情報ありがとうございました。
xcatsan says:
2010年10月19日 16:33
ぽげむたさん、こんにちは。
指摘の通り dealloc での解放が必要で、このままではメモリリークが発生します。後ほど記事を見なおして描き直しておきます。
# viewDidUnloadは毎回呼ばれるものと勘違いしていました。
# メモリリーク時しか呼ばれないことを今更ながら知ってちょっとショックをうけています。。
有益な情報ありがとうございました。