2010/10/15

ViewControllerにおけるビュー管理サイクルとメモリ警告シミュレーションによるアンロード処理について

ビューコントローラにおけるビューの管理にはロードサイクルとアンロードサイクルという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が再帰的に呼び出される)
    1. 画面サイズのルートビューを作成 (ステータスバーやタブバーがあればその分のサイズを減らす)
    2. サブビューを作成し、ルートビューのaddSubview:で追加して、すぐにrelease
    3. ルートビューをコントローラのviewに代入して、すぐにrelease
  • Unloadedからオブジェクトを解放する役割を持つメソッドであるdidReceiveMemoryWarningdeallocが呼ばれる可能性があるため、viewDidLoadでは解放したオブジェクトを二重解放しないようにnilにする必要があります。
  • LoadedやUnloadedの状態に関係なくdidReceiveMemoryWarningは必ず呼ばれます。一方で、didUnloadViewはviewが非nilなら呼ばれますが、nilなら呼ばれないため、メモリ警告が連続すると2回目以降はdidReceiveMemoryWarningのみが呼ばれるようになります
  • メモリ警告ではアプリケーションデリゲードのメソッドapplicationDidReceiveMemoryWarning:も呼ばれるため、コントローラから独立したデータなどはそちらで対処することもできます

メモリ警告のシミュレーション

iPhoneシミュレータ上でメモリ警告シミュレーション ([Device]→[Simulate Memory Warning]) を行うと、メモリ警告をシミュレートするこができます。今回はこれを用いてメモリ不足の状態のときのビューコントローラの挙動を調べました。

まず、ビューコントローラ上でdidReceiveMemoryWarningviewDidUnloadを次のように記述します。loadViewdeallocでも同様のログを出力するようにします。

- (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におけるビュー管理サイクルとメモリ警告シミュレーションを用いたアンロードサイクルの処理の流れについて述べました。

関連項目

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。