俺もメモリ管理を間違えて頻繁にアプリを落としてしまうゆとりプログラマなのであんまり偉そうなことも言えないのだけど、「releaseの使いどころ。メモリの辺りがどうしてもわからない。」を読んでいくつか思ったところがあるので書いときます。
「何度もinit」はしない
既に確保されているハズの変数を二度initするのはメモリリークだよね?
解放されている変数は、
if (obj == nil) では判別できないのだろうか。
ボタン押下時などに、同じロジックを走ることが多くて、何度もinitをしてしまうことが多いのだが。。。。
具体的なコードを見てないので間違ってるかもだけど、多分これ「何度もinitしちゃう」じゃなくて「開放してないオブジェクトを参照してる変数に、何度も新しいオブジェクトをつっこんじゃう」ってことだと思って話を進める(そうすると、どこからも参照が辿れないのにメモリが開放されてないオブジェクトができちゃうからメモリリーク)。ちなみにもし本当にinitメッセージを何度も送ってるんだとしたら、
- allocとinitを必ずワンセットで使う
- initはオブジェクトが生成された後絶対に一度しか呼ばない
ってのを守るだけでいい。[[Class alloc] init]はイディオムなのでinitだけ分けて後から呼ぶことはそうそうないし、すべきじゃない。
前者の、例えば「そのメソッドを通るときにインスタンス変数に新しいオブジェクトをつっこむ処理がある」とかだったら。仮にこういうクラスだったとして、
#import@interface Hoge : NSObject { NSArray *array; } - (void)foo; - (void)bar; @end
(こんなのは流石にやらんけど、例えば)その実装がこういう風だとメモリリークが起きる。
- (void)foo { // fooの前に必ずbarが呼ばれてればいいけど、 // そうでないなら元々arrayに入ってたオブジェクトがリーク。 array = [[NSArray alloc] init]; /* * [array release]を含まないなんかの処理 */ } - (void)bar { // arrayのretainCountが1だったらこの時点でarrayはnilになるけど、 // どっか他でretainされてたらnilにはならない [array release]; }
基本的にはそこを通る前に必ずreleaseしてあるようにしてなきゃ駄目だけど、「念のためそのインスタンス変数に代入する前に必ずreleaseを送る」とかするのもいいと思う。そうすれば代入する時点では必ず開放されてるのは間違いないし、仮に既に開放済みで変数の中身がnilだったとしても、nilにどんなメッセージを送っても「何も起こらない」ので特に問題ない。
- (void)foo { [array release]; // もしarrayがnilならこの行は何もしてないのと同じ array = [[NSArray alloc] init]; /* * [array release]を含まないなんかの処理 */ }
もちろんif (obj == nil)で判別できるので「objがnilの時だけ代入する」でも問題無いはずだけど。どっか他でretainしてるとかでその変数がnilになってない可能性があるなら、release時点で明示的にnilを入れておけばいい。その場合はそのオブジェクトは消えないままその変数がnilになるので、当然そのオブジェクトをretainしてる別なオブジェクトが責任持ってreleaseしなきゃ駄目だけどね。
- (void)foo { if (array == nil) { array = [[NSArray alloc] init]; } /* * [array release]を含まないなんかの処理 */ } - (void)bar { [array release]; array = nil; // barが実行されたら、必ずarrayはnilになる }
initで確保してないNSStringは解放しない
これは「しなくていい」じゃなくて「しちゃいけない」。stringWithなんたらとかいう類のstringを生成して返すメソッドはNSStringのオブジェクトを先にautoreleaseしてから渡してくれるんだけど、autoreleaseされてるオブジェクトを明示的にreleaseするとエラー出てアプリごと落ちるので気を付けた方がいい。NSArrayのarrayとかNSDictionaryのdictionaryとか、クラスメソッドでオブジェクトの初期化までやってくれるやつも基本的にどれも「allocしてinitしてautorelease」相当の処理をやってるので同様。多分「変なとこでreleaseしたせいで落ちた」ってのの原因のかなりのとこがこれじゃないかと思うので、autoreleaseされてるオブジェクトにrelease送ってないか見てみるといいと思う。というか、俺がそれを頻繁にやらかしてる人のなので…。ついやっちゃう。
int main(int argc, char **argv) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSString *hoge = [NSString string]; // ちなみにこれはautoreleaseされてる NSString *fuga = [[NSString alloc] init]; NSString *piyo = [[[NSString alloc] init] autolelease]; // [hoge release]; [fuga release]; // これは問題ない、というかやんないとダメ // [piyo release]; // poolを開放するときに // autoreleaseされてるhogeとpiyoを開放しようとするけど、 // 明示的にreleaseを呼んでしまっているとエラーが出る。 // 試しに↑のコメントアウトを外すと落ちる。 [pool release]; return 0; }
こんな感じ。あと、
また更に外部のobjectに渡すと、構わずreleaseするとアプリが落ちたりするので解放できなくなったりして、
解放するタイミングを失うのだが、そんなもん?!
ってあるけど、これもCocoaのクラスを参考に、自分で作るときもautorelease済みのオブジェクトを渡すようにしてやるといいと思う。autoreleaseしてしまえば生成した側も使う側もメモリの開放に責任持たなくて良くなるので、変なとこで開放しちゃったとか開放し忘れたとか起きにくい。もちろん勝手に消えるけど。
そうじゃなくて永続的に持ちたい場合は、使う側のオブジェクトがcopyするかretainするかして、そのオブジェクトの開放に責任を持つようにすること。「こっちで作ったオブジェクトをあっちで使ってるから安易に開放できなくて、でもこっちがあっちで」みたいなことが頻繁に起きて煩雑になるようなら、あるいはそれも設計を見直した方がいいかも。あんまり別なクラスのメソッドで生成されたオブジェクトを参照で受けてそれを共有して…ってのは良い設計じゃなさそうな臭いがする。
まとめ
- 大前提として、これをできるだけ守る
- 処理の中で生成したオブジェクトはなるべく外に出さずに処理の中でreleaseする
- initしたやつはそれをreleaseする責任を持つ
- retainしたやつはそれをreleaseする責任を持つ
- allocとinitは必ずワンセット
- autoreleaseされたオブジェクトにreleaseを送ってはいけない
- オブジェクトの参照を受けとってそれを保持しておくような処理をするときは、受けとった側がcopyするかretainする
- もちろん必ず受けとった側でも要らなくなったときにreleaseする
- 「処理をする前に一度release」と「releaseしたあとnil代入」は結構使える
要は「initやretainはretainCountを1増やす、releaseはretainCountを1減らす」なのでその対応関係をしっかり保とうってのと、autorelease済みのオブジェクトにrelease送ってないか気を付けとこう、って話でした。
とか記事書いてたら
コメント付いてた。「たのしいCocoaプログラミング」は良い本です。さしあたりそれと「詳解Objective-C2.0」は持ってて損しないです。
余談
考えると当たり前なんだけど、autorelease多用してるとその分パフォーマンスは落ちるしメモリも食うと聞いた。のでiPhoneアプリではなるべくautorelease使わないようにしてるけど、どうなんだろう。正直大して違わなかったりするのかな。あとこれは単に俺があたまよわいだけかもしれないけど、予期しないタイミングでオブジェクトが消えてたり、上に書いた「うっかりrelease送ってしまって全然違うところでエラーが出る」とかも起こりやすい気がするんだよなー。どっちかというとinit/retainとreleaseの方が好き。