ビューコントローラにおけるビューの管理にはロードサイクルとアンロードサイクルという2つのサイクルがあります。ロードサイクルではビューを利用可能な状態にし、アンロードサイクルは利用できない状態にします。なお、アンロードサイクルはアプリケーションがメモリ不足のときに発生します (コントローラの解放とは関係ありません)。
iPhoneはメモリを潤沢に持っていないため、使用メモリ量に関係なくアプリケーションのメモリ警告が発生することがあります。このとき、アプリケーションは前述のアンロードサイクルを含んだ処理を行ってメモリを解放しようとします。本記事ではこのアンロードサイクルの処理の流れについて調べてみます。
そこで、本記事ではまずコントローラのビュー管理についてまとめ、続いて、メモリ警告発生時の挙動をシミュレーションを通して確認してみます。なお、ビュー管理サイクルについては「View Controller Programming Guide for iOS」の「Understanding the View Management Cycle」の項を、メモリ警告シミュレーションについては「Memory Usage Performance Guidelines」の「Responding to Low-Memory Warnings in iOS」の項を参照してください。
ViewControllerにおけるビュー管理サイクル
上の図はUIViewControllerにおけるビュー管理サイクルです。
「Unloaded」や「Loaded」は丸四角はビューの状態を示しており、Unloadedはコントローラのviewがnilの状態、Loadedはコントローラのviewがnilでない状態であり、Releasedはコントローラ自体が破棄された状態です。黒丸とその側のタイプライタフォントで書かれた文字は呼び出されるメソッドを示しており、赤い文がそのメソッドにおける注意点など、青い文がその矢印のトリガとなる条件を示しています。
- ロードサイクル (Unloaded → Loaded) では、
view==nilのときにviewのgetterを呼び出そうとしたときにloadViewが呼び出されてviewの初期化を行います。また、これが呼び出された後はviewが非nilになります - アンロードサイクル (Loaded → Unloaded) では、低メモリ警告時に
view!=nilのオフスクリーン (見えていない) ビューのコントローラではviewDidUnloadが呼び出されてviewの解放を行います。また、これが呼び出された後はviewがnilになります 
また、次の点に注意してください。
loadViewは次のような手順で実装すべきです。なお、コントローラのviewに代入するまでそのgetterが呼び出されないようにします (そうしないとloadViewが再帰的に呼び出される)- 画面サイズのルートビューを作成 (ステータスバーやタブバーがあればその分のサイズを減らす)
 - サブビューを作成し、ルートビューのaddSubview:で追加して、すぐにrelease
 - ルートビューをコントローラのviewに代入して、すぐにrelease
 
- Unloadedからオブジェクトを解放する役割を持つメソッドである
didReceiveMemoryWarningやdeallocが呼ばれる可能性があるため、viewDidLoadでは解放したオブジェクトを二重解放しないようにnilにする必要があります。 - LoadedやUnloadedの状態に関係なく
didReceiveMemoryWarningは必ず呼ばれます。一方で、didUnloadViewはviewが非nilなら呼ばれますが、nilなら呼ばれないため、メモリ警告が連続すると2回目以降はdidReceiveMemoryWarningのみが呼ばれるようになります - メモリ警告ではアプリケーションデリゲードのメソッド
applicationDidReceiveMemoryWarning:も呼ばれるため、コントローラから独立したデータなどはそちらで対処することもできます 
メモリ警告のシミュレーション
 iPhoneシミュレータ上でメモリ警告シミュレーション ([Device]→[Simulate Memory Warning]) を行うと、メモリ警告をシミュレートするこができます。今回はこれを用いてメモリ不足の状態のときのビューコントローラの挙動を調べました。
まず、ビューコントローラ上でdidReceiveMemoryWarningとviewDidUnloadを次のように記述します。loadViewとdeallocでも同様のログを出力するようにします。
- (void)didReceiveMemoryWarning {
    NSLog(@"%@: didReceiveMemoryWarning begin", self.title);
    [super didReceiveMemoryWarning];
    NSLog(@"%@: didReceiveMemoryWarning end", self.title);
}
- (void)viewDidUnload {
    NSLog(@"%@: viewDidUnload begin", self.title);
    [super viewDidUnload];
    NSLog(@"%@: viewDidUnload end", self.title);
}このとき、ナビゲーションビューで次のように2回pushして、次のようにこのコントローラが3つある状態でメモリ警告を発生させたところ、次のようにログが表示されます (日付などは省略しています)。
  Received simulated memory warning. Nav (1): didReceiveMemoryWarning begin Nav (1): viewDidUnload begin Nav (1): viewDidUnload end Nav (1): didReceiveMemoryWarning end Nav (2): didReceiveMemoryWarning begin Nav (2): viewDidUnload begin Nav (2): viewDidUnload end Nav (2): didReceiveMemoryWarning end Nav (3): didReceiveMemoryWarning begin Nav (3): didReceiveMemoryWarning end
上記のように、オフスクリーンビューについてはメソッド呼び出しが入れ子になっており、オンスクリーンビューではdidReceiveMemoryWarningのみが呼ばれるのが普通ですが、モーダルビューを入れると若干挙動が変化します。次のような状態でメモリ警告を発生させた場合には次のようにログが表示されます。
  Received simulated memory warning. Nav (1): didReceiveMemoryWarning begin Nav (1): viewDidUnload begin Nav (1): viewDidUnload end Nav (1): didReceiveMemoryWarning end Nav (3): viewDidUnload begin Nav (3): viewDidUnload end Nav (2): didReceiveMemoryWarning begin Nav (2): viewDidUnload begin Nav (2): viewDidUnload end Nav (2): didReceiveMemoryWarning end Nav (3): didReceiveMemoryWarning begin Nav (3): didReceiveMemoryWarning end Modal (1): didReceiveMemoryWarning begin Modal (1): didReceiveMemoryWarning end
Nav (3)だけはviewDidUnloadが先に呼び出され、didReceiveMemoryWarningはその後で呼び出されていることがわかります。さらにモーダルビューからモーダルビューを呼び出した次のような状態でメモリ警告を発生させた場合には次のようにログが表示されます。
  Received simulated memory warning. Nav (1): didReceiveMemoryWarning begin Nav (1): viewDidUnload begin Nav (1): viewDidUnload end Nav (1): didReceiveMemoryWarning end Nav (3): viewDidUnload begin Nav (3): viewDidUnload end Nav (2): didReceiveMemoryWarning begin Nav (2): viewDidUnload begin Nav (2): viewDidUnload end Nav (2): didReceiveMemoryWarning end Nav (3): didReceiveMemoryWarning begin Nav (3): didReceiveMemoryWarning end Modal (1): didReceiveMemoryWarning begin Modal (1): didReceiveMemoryWarning end Modal (1): viewDidUnload begin Modal (1): viewDidUnload end Modal (2): didReceiveMemoryWarning begin Modal (2): didReceiveMemoryWarning end
シミュレータ上でメモリ警告のシミュレーションを行なった場合の挙動についてまとめると次のようになります。
didReceiveMemoryWarningはLoadedやUnloadedに関係なく、必ず全てのコントローラに対して呼び出される- 通常は
didReceiveMemoryWarning内でviewDidUnloadが自動で呼ばれるような挙動になる - ただし、
viewDidUnloadが先に呼ばれる場合もある - バックグラウンド中にメモリ警告があってもその時は処理せず、フォアグラウンド復帰時に処理を行う
 
警告レベルについて (2011.6.26追記)
Stack Overflowの記事によると、警告レベルは0から3の4レベルがあり、関数OSMemoryNotificationCurrentLevel()で次のenum値が取得できるみたいです。
typedef enum {
    OSMemoryNotificationLevelAny      = -1,
    OSMemoryNotificationLevelNormal   =  0,
    OSMemoryNotificationLevelWarning  =  1,
    OSMemoryNotificationLevelUrgent   =  2,
    OSMemoryNotificationLevelCritical =  3
} OSMemoryNotificationLevel;
なお、どのような条件で各レベルの警告が発生するのかは文書化されていないため、開発者は警告レベルを気にする必要はなく、ただ-didReceiveMemoryWarningに対応すればよいとのことです。
まとめ
ViewControllerにおけるビュー管理サイクルとメモリ警告シミュレーションを用いたアンロードサイクルの処理の流れについて述べました。
関連項目
- safx: iOS 4のマルチタスク処理における状態遷移について
 - View Controller Programming Guide for iOS ViewControllerのビュー管理サイクルについてはUnderstanding the View Management Cycleの項を参照してください
 - Memory Usage Performance Guidelines メモリ警告シミュレーションについてはResponding to Low-Memory Warnings in iOSの項を参照してください
 
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。