メモリ管理
iPhone開発で、メモリ管理の基礎を社員に伝えることが増えてきたので、エントリとして書こう。
Objective-C基礎
メモリ管理の前にObjCの基礎として、メソッド呼び出しの話。
クラスのインスタンスaがmethodAをコールするときは、
[a methodA]
と書く。このとき、aがnilだったときは、エラーではなく、コールされない。methodAに戻り値があるときは、それは、0やnilやNOが返る。ObjCでは、
void dealloc { if(a!=nil){ [a release]; } [super dealloc]; }
は、気持ち悪いので、nilチェックはやめましょう。
なお、ObjCでは、動的にメソッドを差し替えることができ、コールの度にメソッドが存在しているかも確認しています。そのため、LL言語(ライトウェイト言語、スクリプト)のように柔軟な記述が可能です。そして、そのため、事前に宣言されていないメソッド呼び出しはエラーとならず警告になります。
メモリ管理基礎
いろいろな言語がありますが、C言語では、全部自力でメモリ管理しますが、JavaやC#やLL言語ではガーベージコレクションで解放します。ObjCは、参照カウンタで管理します(ObjC2.0のガーベージコレクタは使ったことがない)。参照カウンタでの管理は、全部自力とガーベージコレクタの間ぐらいの管理で、全部自力よりは楽と言ったところです。WindowsのCOMもリファレンスカウンタですね。
参照カウンタのインクリメントが、[a retain];、デクリメントが[a release];、現在のカウントを得るのが[a retainCount];です。これらはNSObjectのメンバです。あと、NSAutorelasePoolがあって、これはスレッドごとに最低1つはインスタンスがあります。
[a autorelease];をコールすると、カレントのオートリリースプールにこのインスタンスを後でrelease(参照カウントのデクリメント)するように依頼します。メインスレッドのコードを書くときは、イベントの最初にオートリリースプールが作成され、イベント処理が終わるとautorelease依頼されたものをreleaseします。このオートリリースプールは、メインスレッドのイベント経由の処理ではシステム側がやってくれるので、アプリは気にする必要がありません。バックグラウンドスレッドを作成するときは、スレッドの最初でオートリリースプールを作成して、スレッドの終わりで解放する必要があります。参照カウンタが0になると、dealloc(デストラクタ)が呼び出されます。
retain, releaseのルール
で、いつretainして、いつreleaseして、いつautoreleaseを使って、いつ自分の書いたコードでないところでカウントアップされるの?というところですが、
- メンバ変数の代入、グローバル変数の代入、static変数の代入など、1イベント処理以降も参照し続ける場合は、retainする。グローバル変数やstatic変数はほとんど使わないだろうから実質メンバ変数への代入でretainと覚えておけば良いでしょう
- ローカル変数(auto変数)では、retain、releaseしない
- allocやnewで始まるメソッドやCopyが含まれるメソッドは、受け取り側がretainするのでなく、そのメソッド内でretainされている。(alloc以外はあんまり使わないので、ここではallocだけ覚えておけば良いでしょう)
というのが基本です。
生成時
他のクラスの生成時ですが、init系のメソッドとクラス名で始まる生成系のメソッドがあります。例えば、NSDataクラスには、「initWithBytes:length:」と「dataWithBytes:length:」とと非常に名前の似たメソッドがあります(init系はあるけど、クラス名で始まる系がないクラスもあります)。init系は、
NSData* data = [[[NSData alloc] initWithBytes:"ABC" length:3] autorelease];
の、ように、クラス名で始まる系は、
NSData* data = [NSData dataWithBytes:"ABC" length:3];
のように書きます。どちらで書いても同じ結果で、クラス名で始まる生成系の方が記述が少なく済みます。で、init系は、alloc時点で参照カウンタが1で、生成されます。そして、上記例だとautoreleseがついているので、このイベントが終わったらreleaseされます。このautoreleaseがついているインスタンスは、今retainCountを見ると1ではあるもののオートリリースプールに登録されているので、このイベントが終わったときには0になるので、イベントが終わったときベースで考えると、参照カウントが0と見なすことができます(実質0)。また、dataWithBytesの方も中身はalloc init autorelaseをやっているものと思われます。単に記述が短くなるように用意されているだけです。
上記例は、ローカル変数への代入であるためretainする必要がありません、今のイベント処理が終わったら全部解放されて欲しいためです。つまり、代入演算子の左辺が一時変数(つまり参照を保持し続けない)であるため、右辺もalloc init autoreleaseで参照カウントを実質0にして、左辺と右辺をバランスさせるということです。
わずかばかりでもメモリの消費が押さえたい場合は、
NSData* data = [[NSData alloc] initWithBytes:"ABC" length:3]; //dataを使った処理 [data release];
と記述することも可能です。この場合、[data release]で参照カウントが0になるなら即メモリが解放されます(「dataを使った処理」で、他のインスタンスにdataを渡して、そのインスタンスが長期保持するならretainされるであろうから解放されない)。ただし、生成行とreleaseが離れると解放忘れをするし、開発チームの他のメンバーがこのソースを見ると、「ちょっと危険なコード」として、ちゃんと解放しているかチェックしてしまうので、お勧めしません。
メンバ変数への代入
メンバ変数など、1つのイベント処理を超えて参照を保持する場合は、retainする必要があります。dataMemberがメンバ変数として、生成のときは、
[dataMember release]; dataMember=[[NSData alloc] initWithBytes:"ABC" length:3]; //alloc内部でretainされる
と書きます。dataMemberがnilと分かっているときは、[dataMember release]は不要ですが、そうでないときは書く必要があります。前述の通り、dataMemberのnilチェックは不要です。
引数でもらった、aDataをメンバ変数に代入するときは、
[aData retain]; [dataMember release]; dataMember=aData;
と書きます。nilを代入したいときは、
[dataMember release]; dataMember=nil;
と書きます。nilの代入を忘れると2重解放の原因になるのでdealloc以外ではnil代入を忘れないようにすること。
なお、誰かの作ったメソッドであるクラスの参照が得られるとき、そのメソッド側で参照カウンタが勝手にあがることはありません(allocや、その他newやcopyがメソッド名につくものなど以外)。例えば、自分のビューの親ビューを得るのに
UIView* parent=[self superview];
と呼び出しても勝手には参照カウントは増えません。参照カウントを増やす責任があるのは、代入を書く人です。逆に自分でメンバ変数を返すコードを書くときもretainして返してはいけません。
-(NSData*)getDataMember { [dataMember retain];//NG! return dataMember; }
と書いてはいけません。何か生成して返すメソッドを書くときもautoreleaseをつけて実質0(今のイベント処理後で0)で返すべきです。
-(NSData*)getData { return [[[NSData alloc] initWithBytes:"ABC" length:3] autorelease]; }
上記は、alloc内部で参照カウントが1で生成されて、autoreleaseで実質0となります。
プロパティをガンガン使おう
さて、何度かretainとreleaseを書く例がでていますが、retain数に比べてreleaseが少ないとメモリリークになるし、retain数に比べてreleaseが多いとrelease時に
malloc: *** error for object 0x4c40740: pointer being freed was not allocated
とコンソールにエラーを吐いてクラッシュします。initしてautoreleaseをコールして、releaseをした場合は、オートリリースプールの解放処理でクラッシュします。当たり前ですが、retainのコールされる数とreleaseのコールされる数は厳密にマッチしていなければならず、プログラマが慎重に管理しなければなりません。そこで、retainとreleaseを極力書かないで済むようにすることをお勧めします。
その為にプロパティを使います。プロパティは、ヘッダファイルに
@interface Person { NSString* name; } @property (nonatomic, retain) NSString* name; @end
と書いて、実装ファイル(*.mや*.mm)に
@implementation Person @synthesize name; //中身 @end
と書きます。@propertyのnonatomic属性は、スレッドセーフでないが高速という属性です。マルチスレッドで複数のスレッドからアクセスされないときは、nonaotmicでよいでしょう。retain属性は、自動でretainやreleaseをしてくれるものです。なお、nonatomicやretainを書くのは、クラスのポインタ型のときであり、プリミティブ型のときは、@property int type;のように、属性は不要です。
実装ファイルに書く@synthesizeは、getterとsetterを自動で書いてくれるものです。
で、そのsetterですが、自動で、
-(void)setName:(NSString*)a { [a retain]; [name release]; name = a; }
というものを自動で作ってくれます(見えないけど、上記と同等のコードに展開されると思われる)。
で、setterの呼び出しは、
self.name = aName;
みたいに書くとsetterが呼び出されます。プロパティとして宣言しても
name = aName;
と書くと直接メンバ変数への代入となり、setterが呼ばれません。
- self.nameとnameは全然違う!
ということです。そして全てのポインタ型のメンバ変数をプロパティにすることで、ほとんど全てのretainやreleaseを書かずに済みます。上に挙げた例を書き直すと、生成してメンバ変数に代入なら
self.dataMember = [NSData dataWithBytes:"ABC" length:3]; //代入前の参照に対してreleaseしてくれる
[dataMember release]をせずに代入でOKになる。クラス名で始まるメソッドがないときは、
self.dataMember = [[[NSData initWithBytes:"ABC" length:3] autorelease];
[dataMember release]をせずに代入でOKになる。引数で来たaDataをメンバ変数に代入なら、
self.dataMember = aData; //代入前の参照に対してreleaseがされ、代入後の参照に対してretainされる
releaseもretainも不要。nilの代入も
self.dataMember = nil;//releaseされる
でOKです。これでほぼすべての場所でretainとreleaseを使わずに済みます。ただ慣習として、deallocは、nil代入でなくreleaseを呼ぶことが多いようです。しかし、プロパティに対してnil代入というコーディングルールにすればreleaseなしにできるでしょう。
- ポインタ型のメンバ変数をすべてプロパティにすればretainとreleaseは、原則呼ばなくてよくなる(例外はあるけど、setterの中とか、マルチスレッドとかね)
生成といっしょに他人にセット
どのメソッド呼び出しで参照カウントがアップされて、いつ解放されるか気になる人がいますが、「メンバ変数(など長期に保持する変数)に代入する人がカウントアップするというのが原則なので、普通、呼び出し側は気にする必要がありません(deallocが呼ばれるタイミングが重要とかマルチスレッドでない限り)。
view.backgroundColor=[[UIColor alloc] initWithRed:0.5f green:0.3f blue:0.1f alpha:1.0f];
これは間違えです。なぜならこれは、
UIColor* color = [[UIColor alloc] initWithRed:0.5f green:0.3f blue:0.1f alpha:1.0f]; view.backgroundColor=color;
と同じだからです。ローカル変数の代入は、alloc init autoreleaseをセットで使うか、クラス名で始まるコンストラクタ系を使うかです(別途、releaseを明示的に呼ぶのも可ですが、ここではretainとreleaseをできるだけ使わないようにしようという趣旨なので)。正しくは、
view.backgroundColor=[UIColor colorWithRed:0.5f green:0.3f blue:0.1f alpha:1.0f];
となります。
この生成されたカラーは、viewからしか参照がなければ(普通そう)、viewの解放処理とともに解放されます。
privateなプロパティ
メンバ変数をプロパティにすると、他のクラスから書き換え、参照し放題になりpublicになってしまいます。retain, release最小化のためにプロパティにしたいが、公開したくない場合は、クラスの拡張を使って、ヘッダファイルに書かずに実装ファイルに書きます。先ほどのPersonクラスのnameメンバ変数の例で行くと、
//Person.h @interface Person { NSString* name; } @end
ヘッダファイルには、@propertyを書きません。そして実装ファイルの上の方に
@interface Person () //クラス拡張 @property (nonatomic, retain) NSString* name; -(void)somePrivateMethod; @end @implementation @synthesize name; //内容 @end
のように、Personを拡張することで、他のクラスから見えないようにします。この拡張を使うことで、privateメソッドも作れます。(ただ、ビルド時に見えないだけで、実行時に無理矢理呼び出せば呼び出せるでしょう)
自分でNSAutoreleasePool
たまに自分でオートリリースプールの作成をすることがあります。1つは前述した別スレッドを使うときです。スレッドメインの最初に
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
を書いてそのスレッドが終わるときに
[pool release];
を書くだけです。その他に、メインスレッドでもサブスレッドでも大量にオートリリースプールに登録するような場合です。
for(int i=0;i<10000;i++){ NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; NSString* a=[NSString stringWithFormat:@"%@=%d", b, c]; //適当な長い処理 [pool release]; }
この場合、stringWithFormatのコールでガンガンメモリが作られるのですが、自分でオートリリースプールを作らないと、ループ中に解放されないのでメモリをどんどん消費します。ループの中にオートリリースプールを作ることで、ループの1回ごとに解放されてメモリ使用量を抑えることができます。
メモリリークの試験
Xcodeでは、簡単にメモリリークの試験ができます。まず、iOS4からホームボタンをおしてもバックグラウンドでサスペンドされたままにすることができるようになったため、これがONだと試験にならないので、OFFにします。Info.plistで、「Application does not run in background」をONにします。そしてデバッグビルドします。
メニュー>実行>パフォーマンスツールを使って実行>Leaks
で実行します。Instrumentsが起動されるので、その状態で適当に操作してホームボタンを押します。リークしている場合は、リークブロックが出ます。
上のペインにAllocationsとLeaksの2つのグラフが出るのでLeaksをクリックしてカレントにします。上ペインと下ペインの間に「Leaked Blocks」というプルダウンがあるので、そこでCall Treeを選択します。そこのツリーを展開していくと自分のコードでinitやnew、mallocした行が出ます。ソースコードも表示され、newした行がハイライトされます。メモリを確保した位置が分かれば、大抵すぐに修正できるでしょう。
まとめ:かみやん式 retain,releaseしないコーディングルール
ルールをまとめると
- ルール1:クラスのポインタ系のメンバ変数は、すべてretain属性付きのプロパティにする。必要ならばクラス拡張を使ってprivateに。
- ルール2:クラスのポインタ系のメンバ変数への代入は、必ずself.をつけてプロパティ経由で代入する。
- ルール3:インスタンスの生成は、必ずalloc init autoreleaseまたは、クラス名で始まるファクトリメソッドを使う。左辺がメンバ変数のときは、self.をつけてプロパティへの代入にする。
- ルール4:retainは使わない。releaseは、dealloc内とNSAutoreleasePoolのrelease以外は使わない。それ以外の場所で参照カウントを減らしたいときは、self.プロパティ=nil;とする。
という感じ。マルチスレッドとかそれ以外の特殊な場合(循環参照の問題とか、微妙なタイミングの問題とか)でなければretain, releaseはほぼなしにできます。
プロパティを使って retain、releaseを最小にしましょう!
#2011/2/20 18:14 C++のdelete前のNULLチェックの文章をカットしました、指摘ありがとうございます。
#2011/2/20 18:30 NSAutoreleasePoolの章を追記しました
#2011/2/20 19:16 カテゴリを使わず拡張としました
#2011/2/20 21:03 生成といっしょに他人にセット節を追加
#2011/2/20 22:22 指摘ありがとうございます。C++をカット
#2011/2/20 22:41 指摘ありがとうございます。retain->release->代入の順番に修正しました
#2011/3/27 まとめ節を追加しました。
ーー
URL:ibisMailのダウンロード、レビューはこちら
Twitter: @kamiyan