iPhoneアプリ開発のメモリ管理

昨日はメモリリークと戦っていて,自分が色々勘違いをしていたことに気がついた.
retain,autorelease,release難しいよ..

前提

そもそも,obj-cでは変数は参照であり指しているものはオブジェクトと考える.
オブジェクトを生成するたびに,cで言うところのmallocで動的にメモリを確保してそこに変数を割り当てている状態になるのでユーザーがきちんと管理,解放を行わないとすぐにメモリリークする.
このメモリ管理がくせ者だ.

retainCountを意識する

オブジェクトはそれぞれretainCountという変数をもっている.

obj = [[Class alloc] init];

てな感じにオブジェクトを生成したときに,カウントが1増える.
このカウントが0になったとき,オブジェクトは解放される.
(解放されるとき,objがもっているdeallocメソッドが呼ばれる.)
上記のように生成した場合,retainCountは勝手に減らないのでそのままだといつまでたってもオブジェクトが解放されない.
では,カウントはどうやって増減するのかというと,retain,releaseによって増減する.

release

[obj retain];  //retainCountを上げる
[obj release]; //retainCountを下げる

つまり,

obj = [[Class alloc] init];
[obj release]; //retainCountを下げる

ってなことをすると即解放する.
関数などで一時的なオブジェクトを作るときは,使い終わったらreleaseして解放してあげる.
大体,alloc]init]が出てきたらreleaseはセットになる.
また,

NSString * str = [[NSString alloc] initWithString:@"hoge"];

みたいなinitなんたらかんたらというメソッドで生成したオブジェクトもreleaseする必要がある.
逆に,

NSString * str = [[NSString alloc] stringWithString:@"hoge"];

のようにinitが頭についてないメソッドで生成したオブジェクトは後述するautoreleaseが適用されているらしいのでreleaseする必要は無い.
これが悩む.
releaseしなきゃいけないのか,する必要無いのか判別は名前だけなのかと.
試行錯誤の結果,initが頭についていないメソッドは基本的にはreleaseしなくて良さそうです.
もし心配なら

  • とりあえずreleaseして落ちる.
  • 次にrelease文をコメントアウトして落ちない.

なら大丈夫じゃないでしょうか.責任は持てませんが.

retain

じゃあ,retainはどこで使うかというと,これが難しい.
例えば,生成したobj_aを他のオブジェクトobj_bにもたせたいとき.
生成元でobj_aをreleaseして解放されると,obj_bにもたせたobj_aの参照先も解放されているため,obj_bが内部でobj_aを触ると多分落ちる.
これを回避するために,obj_bが[obj_a retain]をして参照カウントを上げておく.
すると,生成元でobj_aをreleaseしてもobj_aは解放されない.
ただし,今度はobj_bがきちんとreleaseをして解放してあげる必要がある.
retainは間違った使い方をするとretainCountが0にならなくなってしまいメモリリークにつながるので注意が必要.
retainしたらreleaseもどこかで必要になるのが基本.

autorelease

手前でいちいちreleaseするのは面倒なので,autoreleaseという機構が備わっている.

obj = [[[Class alloc] init] autorelease];

こうしておくことで,objが生成されたメソッドを抜けて適当なタイミングでreleaseが勝手に走る.
そこでretainCountが0になればオブジェクトは解放される.
便利.
ちなみにアップルさんはiPhoneの開発ではあまりautoreleaseを使うことを推奨していないみたいです.

メモリ管理の注意

autoreleaseが走るタイミングを色々調べたら,イベントループが一回りしたときに走るよ!とか色々情報が落ちているのだが,基本的にはオブジェクトにautoreleaseを適用したメソッドを抜けたらいつ発生しても良いようにしておく必要があると思う.
自分の場合は,UITableViewControllerの最初に作ったオブジェクトにautoreleaseを適用したら,その後に実行されるnumberOfRowsInSectionまではcellForRowAtIndexPathが実行される前にautoreleaseが走っていた.


また,グローバルに触れるクラス変数にオブジェクトを入れて,複数メソッドで使い回す場合,retainCountを把握していないと

  • いつの間にかオブジェクトが解放されていて落ちる
  • オブジェクトが解放されていないのに新しいオブジェクトを作ってしまいメモリリークする

の2つにすぐに陥ります.

色々なテクニック

テクニックというのもおこがましいのですが,頭を沸騰させて試行錯誤をしているときに使った方法です.

NSZombieEnabled

retainCountが0になって解放されたオブジェクトに触ると,EXC_BAD_ACCESSが発行されて落ちる.
EXC_BAD_ACCESSとか言われてもどこでなにが起きたのかさっぱりわからない.
そこで,NSZombieEnabledをつかうとよい.
これは,こちらのサイトに詳しく書いてありますが,解放されたオブジェクトにNSZombieという物騒な名前のオブジェクトが入ります.
このゾンビに触ると,ダイイングメッセージを残して死ぬことができます.

retainCount

retainCountをNSLogで表示することで,どこでautoreleaseが走るか調べる.
ちなみに,解放されたオブジェクトのretainCountを調べようとすると落ちるので注意.

Leaks

XcodeのツールでLeaksってのを使うとメモリリークのチェックができます.
実行=>パフォーマンスツールを使って実行=>Leaksで起動します.
動かしていてLeaks Discoverdに赤いバーが立ったらメモリリークしているみたいです.
とりあえずこのバーが立たないようにがんばってメモリリークをつぶす.


その他,ブレークポイントを仕掛けて,常に参照が指しているオブジェクトの中身を気にするとか,色々あるけど,基本は作ったものは必ず解放するところまで面倒を見る.
間違っているところがあるかもしれませんが,今のところこんな感じに頭を整理しています.
個人的に,Cのmalloc/freeではあまりはまらなかったので,今回このような頭が沸騰する機会を頂くことができ非常にうれしく思っています.
どうしても理解できない人は,3日くらいあの手この手を使って頭を沸騰させると体が感じるようになってくる.
Don't think, feel.ですね.
メモリこわい.

[追記]
Build and Analyze を教えてもらいました.
これはすごいです.
記事