Objective-C2.0メモ メモリ管理

Objective-Cのメモリ管理について

C言語でメモリ管理をするにはmalloc, freeを使いますが、Objective-Cでは、オブジェクトの参照数を考慮したメモリ管理の仕組みが用意されています。現在いくつ参照されているのかを把握し続けるために、全てのオブジェクトは参照カウンタという値をメンバに持っています。参照カウンタの増減だけ指示すれば、参照がなくなった時点で自動的にメモリを解放してくれる仕組みになっていますので、より手軽にメモリを管理することができます。ただし参照カウンタの指示が適切になされていないとメモリリークが発生しうるので注意が必要です。カウンタの値は、NSObjectクラスのretainConterというメソッドで取得することができます。

※ちなみに、Objective-Cでallocとdeallocを使うとオブジェクトの生成と破棄のタイミングを自由にコントロールできますが、上で述べた仕組みがありますのであまり推奨されているわけではなさそうです。使うときはリークやアクセス違反が起こりやすいので注意する必要があります。

    Sample *obj = [[Sample alloc]init];
    [obj dealloc];

メモリ管理の仕組みを理解するには以下の3点に着目するとよいと思います。
・オブジェクトが生成されるタイミング
・オブジェクトが破棄されるタイミング
・ガーベージコレクションの規則とオブジェクトの参照数の変化

retainとrelease

オブジェクトの生成

allocメソッドによってオブジェクトが生成され、参照カウンタが1になります。

オブジェクトの破棄

参照カウンタが1の状態で、releaseを呼ぶとオブジェクトが破棄されます。

参照カウンタの変化

retainメソッドで参照カウンタが+1, releaseメソッドで-1されます。
以下、参照カウンタをインクリメント・デクリメントしたサンプルです。
main.m

#import <Foundation/Foundation.h>
// sample class
@interface Sample : NSObject
@end
@implementation Sample
@end

int main (int argc, const char * argv[]){
    Sample *sample = [[Sample alloc]init];             // count 1
    NSLog(@"retain counter = %lu", [sample retainCount]);
    [sample retain];                                   // count 2
    NSLog(@"retain counter = %lu", [sample retainCount]);
    [sample release];                                  // count 1
    NSLog(@"retain counter = %lu", [sample retainCount]);
    [sample release];                                  // count 0
    return 0;
}

モリープールとガーベージコレクション

モリープールとautorelease

生成したオブジェクトを自動的に破棄してくれる仕組みをガーベージコレクションと呼びますが、Objective-Cでも同様の仕組みを利用することができます。オブジェクト領域をメモリに確保したあと、AutoReleasePool(自動的に解放してくれる機能を持つメモリプール)に登録して、最後にプール自体を解放(drain)してしまうという手があります。こうしておくと、解放するタイミングを意識することなく手軽にオブジェクトを生成することができます。一方で、解放されるタイミングを任せてしまうので使用メモリ量をこと細かに制御できないという欠点もあると考えられます。

void checkMemoryLeak(){
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Sample *obj = [[Sample alloc]init];
    [obj autorelease];
    [pool drain];
}

autoreleaseが呼び出されると以前に作成したメモリプールを探しにいきます。ですのでスコープ外でプールを作成しておけば、そちらのメモリプールに登録することができます。main関数の始めにプールを準備し、終わりでプールを片付ければその間自由にautoreleaseを使うことができます。以下の例ではmain最後で破棄しているのでスリープした後にdeallocが呼び出されています。

#import <Foundation/Foundation.h>
@interface Sample : NSObject
@end
@implementation Sample
-(void) dealloc {
    NSLog(@"dealoc was called" );
    [super dealloc];
}
@end

static void checkMemoryLeak();
int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    checkMemoryLeak();
    NSLog(@"sleep");
    [NSThread sleepForTimeInterval:3]; // sleep 3 seconds
    [pool drain]; // ここでdealloc呼び出し
    return 0;
}
void checkMemoryLeak(){
    //NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Sample *obj = [[Sample alloc]init];
    [obj autorelease];
    //[pool drain];
}

実行結果

sleep
dealoc was called

オブジェクトが解放されるタイミングを早めるためには、より近いスコープ内にAutoReleasePoolを作成してスコープを出るときにプールを片付けるのが効果的です。こちらの例では、スリープよりも先にdeallocが呼び出されています。

int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    checkMemoryLeak();
    NSLog(@"sleep");
    [NSThread sleepForTimeInterval:3]; // sleep 3 seconds
    [pool drain];
    return 0;
}
void checkMemoryLeak(){
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    Sample *obj = [[Sample alloc]init];
    [obj autorelease];
    [pool drain]; // ここでdealloc呼び出し
}

実行結果

dealoc was called
sleep

ガーベージコレクションが有効な環境か無効なのかによって若干drainの動作が異なります。ガーベージコレクションが有効な環境ではdrainを実行すると、そのガーベージコレクションを起動して、適切なタイミングで破棄します。無効な環境では、登録されている全てのオブジェクトをreleaseします。

オブジェクトの生成

allocメソッドによってオブジェクトが生成され、参照カウンタが1になります。

オブジェクトの破棄

autoreleaseを実行すると有効なNSAutoreleasePoolを探して参照を自動的に登録します。
ガーベージコレクション有効→
NSAutoreleasePoolのdrainが呼び出された後、適切なタイミングでオブジェクトが破棄されます。
ガーベージコレクション無効→
NSAutoreleasePoolのdrainが呼び出されたとき、参照カウンタが1ならオブジェクトが破棄されます。

参照カウンタの変化

ガーベージコレクション無効→
同様に、NSAutoreleasePoolのdrainが呼び出されたときにカウンタが-1されます。

メモリ管理の例

いくつか具体例を追記しておきます。

クラス生成(releaseとautorelease)
#import <Foundation/Foundation.h>
@interface Sample : NSObject
@end
@implementation Sample
-(void) dealloc {
    NSLog(@"dealoc was called" );
    [super dealloc];
}
@end

int main (int argc, const char * argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    Sample *obj = [[Sample alloc]init];  // 非auto
    Sample *obj_auto = [[[Sample alloc]init]autorelease];  //  auto
    [obj release];  // obj破棄

    [pool drain]; // obj_auto破棄
    return 0;
}

allocで生成したらそれに対応するreleaseを呼び出しているところがポイントです。

クラスが別のオブジェクトを持つ場合

ParentクラスがChildクラスをメンバに持っており、オブジェクトを引数とするメソッドを持っている例です。setterの実装では古いオブジェクトの参照カウントを減らして、逆に新しいオブジェクトをカウントアップしています。
Parentクラス

#import <Foundation/Foundation.h>
#import "Child.h"
@interface Parent : NSObject
{
    Child* child;
}
-(id) initWithChild:(Child*) aChild;
-(void) setChild:(Child*) aChild;
@end

@implementation Parent
- (id)initWithChild:(Child*) aChild
{
    self = [super init];
    if (self) {
        [self setChild:aChild];
    }    
    return self;
}
//  オブジェクトを引数とするメソッド
-(void) setChild:(Child*) aChild
{
    [child release];  //  古い値なのでリリースしておく
    //[child autorelease]; // 必要に応じてautoreleaseと使い分けてもよい
    [aChild retain];  //  新しい値を参照するのでカウントアップ
    child = aChild;
}
-(void) dealloc {
    NSLog(@"Parent dealoc was called" );
    NSLog(@"retain counter = %lu",[child retainCount]);
    [child release];  //  最後に解放しておく
    [super dealloc];
}
@end

Childクラス

#import <Foundation/Foundation.h>
@interface Child : NSObject
@end

@implementation Child
- (id)init
{
    return [super init];
}
-(void) dealloc {
    NSLog(@"Child dealoc was called" );
    [super dealloc];
}
@end

main

int main (int argc, const char * argv[])
{
    Child *child = [[Child alloc]init];
    Parent *parent = [[Parent alloc]initWithChild:child];
    [parent setChild:child];
    [child release]; // alloc対応
    [parent release]; // alloc対応
    return 0;
}