落ちないiPhoneアプリが作りたい自分のための、押さえておくべきポイントたくさん。

iPhoneアプリは良く落ちる。

ランキングで上位に入ったり、話題になったりしているアプリでも落ちる時は落ちる。なぜ落ちるか?
原因のほとんどは、

解放済みのオブジェクトにメッセージを送ってしまうこと

だと思います。

メモリ不足で落ちる?

「メモリ不足で落ちる」という言葉も良く使いますが、本質的には

  1. メモリが不足する
  2. 自動的にオブジェクトが解放される
  3. 解放されたオブジェクトにメッセージが送られる
  4. エラー

という流れで生じるものだと思います。
だから、この問題の解決策は、ふたつ。

  1. メモリを無駄に使わない(メモリリークを無くす)
  2. メモリ不足などによって解放されたオブジェクトにメッセージが送られないようにする

そのために気をつけるべきことなどを以下に列挙します。

alloc initしたら参照カウントが増えるので、かならずreleaseする。

非常に基本的なことですが、確保したものは解放します。

NSString *string = [[NSString alloc] initWithString:@"test"];
// ...処理...
[string release];

クラスメソッドで初期化した場合はreleaseしない。

次の例のようにクラスメソッドで初期化する場合があります。この場合、autoreleaseの状態、つまり後ほど自動でreleaseが送られることになっているため、自分ではreleaseしません。

NSString *string = [NSString stringWithString:@"test"];
// ...処理...

CF〜Create()で作られたオブジェクトはCFRelease()で開放する。

CF〜Create()という関数で作られたオブジェクトも必ず解放する必要があります。

CFDataRef dataref = CFDataCreate(NULL, buffer, CFDataGetLength(data));
// ...処理...
CFRelease(dataref);

viewDidUnloadはいつ呼ばれる?

viewDidUnloadというメソッド名を見ると、まるでビューが閉じる時に呼ばれそうなものですが、これは勘違い。一度も呼ばれることなくビューが開いて閉じることも多々あります。
ではいつ呼ばれるかといえば、低メモリ状態になった時です。低メモリ時、最前面で表示されているもの以外のビューコントローラにはviewDidUnloadが呼ばれます。

viewDidLoadで確保した変数はviewDidUnloadでreleaseする。

viewDidUnloadされたビューコントローラは、画面が空っぽになってしまいます。
まだ使っている最中のビューコントローラが空になっても、問題がないのは、次に表示されるときに再びviewDidLoadが呼ばれて画面を作り直すためです。もし、1回目のviewDidLoadで確保したオブジェクトをviewDidUnloadで解放していないと、2回目のviewDidLoadをするときにメモリリークを起こします。
だから、viewDidLoadで確保したオブジェクトはviewDidUnloadで解放します。

initとviewDidLoadで確保したメモリはdeallocでreleaseする。

上でも書いたように処理が終わってビューコントローラが閉じる時には、viewDidLoadは呼ばれません。deallocだけが呼ばれます。
したがって、initで確保したオブジェクトはもちろん、viewDidLoadで確保したものも解放する必要があります。

didReceiveMemoryWarningでオブジェクトを解放する時はnilを代入する。

低メモリ状態になると、確保されている全てのビューコントローラにdidRecieveMemoryWarningが呼ばれます。viewDidUnloadは、ビューコントローラが閉じた状態になると、もう呼ばれません。
しかし、didReceiveMemoryWarningはメモリが足りなくなるたびに何度でも呼ばれます。
ここで注意しなければならないことは、1回目のdidReceiveMemoryWarningで解放されたオブジェクトに、2回目のdidReceiveMemoryWarningで再びreleaseが送られるとクラッシュします。
そこで、didReceiveMemoryWarning内で解放したオブジェクトにはnilを代入して、二度目の解放処理が行われないようにします。(nilはreleaseを受け取ってもクラッシュしないためです)

というかオブジェクトを解放する時はnilを代入する。

解放後にメッセージが送られる可能性があるオブジェクトを解放する時は、releaseだけでなくnilの代入が必要です。そうでない場合でも、nilを代入して悪いことはないです。

[obj release];
obj = nil;

プロパティとして定義されているものは、nilを代入して解放する。

プロパティとして定義されている変数はセッターにreleaseの処理が含まれているので、nilを代入するだけで済みます。

self.obj = nil;

この解放手段は、プロパティがassignかretainかcopyか考える必要がなくて一石二鳥です。
http://iphone-dev.g.hatena.ne.jp/tokorom/20100314/1268591111
ただし、次のように書いてしまうとセッターが働かず解放できないので注意。

obj = nil

マルチスレッドの場合は、全てのスレッドでNSAutoreleasePoolを作成する。

NSAutoreleasePoolはスレッド内でのみ有効なので、新たにスレッドを立てる場合はNSAutoreleasePoolを作成する必要があります。

ループ内など大量のオブジェクトを確保するところでは、局所的にNSAutoreleasePoolを作成する。

ループ内でオブジェクトを確保するような処理では、大量のメモリを消費するので、NSAutoreleasePoolを局所的に作成するなどして、ループが1周するたびにメモリを解放するようにしてやります。

UIScrollViewでaddSubviewをする場合は、画面外に出たviewをremoveFromSuperviewする。

UITableViewでは画面外に出たセルを自動的に再利用する機構がありますが、UIScrollViewにはありません。
したがって、そのような機構を自作するなどして画面外に出たviewを閉じることでメモリ使用量を削減します。

メモリ使用量の取得方法

プロファイラなどを使えば見えるが、プログラム中で値として取得したい場合
http://d.hatena.ne.jp/kimada/20090405/1238914098#20090405f1

メモリリークを調べる。

Xcode4をRunではなくAnalyzeで実行することで、本質的にメモリリークになる部分を全て指摘してくれます。便利。
これを全てつぶしても、動的に生じるメモリリークは残るので、更にProfileで実行してメモリリークをプロファイルします。これでひとつずつ確認して原因を潰していく作業が待っています。

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

iOSシミュレータのメニューから、メモリ警告シミュレーションを実行できる。
これでメモリ不足の警告を出し、つまり背面のビューコントローラをUnloadしたりして、メモリリークが起きたり、解放されたオブジェクトにメッセージが送られるようなプログラムになっていないかを確認します。

おわりに

以上で終わりです。
勉強しながらまとめたものですので、もし誤っているところがありましたら、お知らせください。

参考URL

http://konton.ninpou.jp/program/cocoa/memory.html
http://iphone-dev.g.hatena.ne.jp/tokorom/20100314/1268591111
http://hamasyou.com/blog/archives/000384
http://labs.torques.jp/2010/11/07/1495/
http://safx-dev.blogspot.com/2010/10/viewcontroller.html
http://d.hatena.ne.jp/sakusan_net/20110226/1298727973
http://journal.mycom.co.jp/column/objc/104/index.html
http://hamasyou.com/blog/archives/000383
http://d.hatena.ne.jp/glass-_-onion/20110218/1297963007
http://cocoadays.blogspot.com/2010/10/nsautoreleasepool.html

追記(2011/11/19)

本気で強制終了される場合もあるようなので、それについても書きました。
落ちないiPhoneアプリが作りたい自分のための、メモリ不足でアプリが強制終了する話。

コメント

タイトルとURLをコピーしました