UIViewControllerのview解放の罠

こんにちは。
現在キュージーは、次期バージョン v2.3.1 のリリースに向けて最終の調整を行っています。

日々度重なるテストの中で、あれれ、とか、おやおやと思うことも沢山あるわけですが。そんな中でもiOSのバージョン毎にたびたび仕様が変更され、いつまでたっても制御が(自分の中で)安定しない UIViewController 。先日見事にハマってしまった、これの罠についてすこし書こうと思います。
というわけで、今回ははじめて?となる、プログラミングの突っ込んだお話です。

iOSアプリとは必ず何かしらの画面が表示されるもので、プログラムから見ると、その描画作業は UIWindow に格納された UIViewController が担当し、この UIViewController の中の view プロパティ(UIView)が、各パーツを収めた画面要素の集合、つまりカンバスとでも呼ぶべきものとなっています。

通常、このビューコントローラは生成・初期化を経て、以下のような順番でメソッドが呼び出されることにより、描画処理を行います。

alloc -> init (初期化)

loadView (self.view の生成)
viewDidLoad (実際の描画処理など)

そしてデバイスのメモリーが逼迫してくると、

didReceiveMemoryWarning

が呼び出され、可能であればここで描画領域(self.view)を破棄し、メモリの解放を行います。
可能である場合、というのは現在の画面が別のビューコントローラで描画されており、呼び出されたビューコントローラのビュー(self.view)が、実際には表示されていない場合です。

iOS View Controller プログラミングガイド」は、呼び出されたビューコントローラが実際に表示されているか否かの判定を、以下のようなコードで実装すると解説しています。

if ([self.view window] == nil){

}

UIView の window プロパティは、そのビューが表示されている UIWindow を返します。つまりこの window プロパティが nil の場合、そのビューは実際に画面には表示されていないということになります。
通常、メモリの警告を受け取り、この window プロパティが nil の場合、呼び出されたビューコントローラは自らの描画領域(self.view)を(以下のコードを使って)解放し、同時にメモリの解放を行います。

- (void) didReceiveMemoryWarning ;
{
	[super didReceiveMemoryWarning] ;

	if ([self.view window] == nil){

		self.view = nil ;

	}
}

通常は UIViewController の継承クラスでも同じことを実行すればいいのですが、今回使っていたビューコントローラは UIViewController の孫クラスにあたり、self.view.window プロパティが nil の場合、それぞれの didReceiveMemoryWarning メソッド内で、viewプロパティ以外の解放処理も実行していました。

そしてこの「派生クラスをさかのぼる didReceiveMemoryWarning メソッド呼び出し」の中に、今回のバグの原因が潜んでいたのです。

システムによって派生クラスの didReceiveMemoryWarning メソッドが呼ばれると、先ずは、

[super didReceiveMemoryWarning] ;

として、親クラスの同名メソッドを呼び出します。一番底のクラスにおいて self.view を解放し、それ以降、世代を追ってそれぞれのクラスの解放処理を書くわけですが、当然のことながらどのクラスにおいても、このビューコントローラが実際に描画されているかどうかの判定をすることになるわけです。
つまり、各階層ごとに

[self.view window]

すなわち、

self.view.window

を呼び出すわけですね。

一見まともな処理を書いているように見えますし、自分自身もこれでビューが解放されているものと思い込んでおりました。
しかし、実際にログを吐いて検証してみると、どうやら self.view が nil を代入しているにもかかわらず空白になっていない。そればかりか didReceiveMemoryWarning メソッドのサイクル内で、viewLoad や viewDidLoad メソッドまで呼び出されている始末。

まるでゾンビのようによみがえる self.view に、一時ア然とさせられもしましたが、解決の糸口は前出のプログラミングガイドの中のたった一行、

「以後、viewプロパティがアクセスされれば、初回と同じ手順でビューを再ロードします。」

という所にありました。

"view プロパティがアクセスされれば"

つまり派生クラスの中で、判定のために使用していた self.view.window の self.view の部分が、プログラマの安易な予想に反して view プロパティを再生成し、結果として loadView や viewDidLoad が呼び出されていたということです。

この問題の対応策として、現在は self.view と連動する専用の判別フラグを用意するとともに、派生クラスではこのフラグを元に描画オブジェクトの解放を行っています。

ドキュメントはよく読め、というのが今回の教訓ですが。まあOSの海というのは中々に広漠で、すべてを見渡すというのは難しいものがありますね。というわけで、今回の失敗とリカバリの経験が、何かの参考になりましたら幸いです。

バージョン 2.3.1 のリリースも、ぜしぜしご期待下さいませー。

CueZy開発チーム・三橋


参考:UIViewControllerまとめ
http://qiita.com/edo_m18/items/189acd18f1ecc368b5b0

CueZy Webサイト
日本語 http://www.cuezy.net/jp
English http://www.cuezy.net

CueZy - App Store のページ
日本語
http://itunes.apple.com/jp/app/cuezy-9-pad-sampler/id534202024?mt=8
English
http://itunes.apple.com/us/app/cuezy-9-pad-sampler/id534202024?mt=8