3月2010

Objective-Cのメモリ管理

Objective-C のメモリ管理の話をします。Objective-C ではどのようなルールに基づいてコーディングすればよいかを説明します。

  1. alloc, copy, new を送信したオブジェクトは、それによって生成されたオブジェクトを所有します。また、retain を送信したオブジェクトは、その受信側のオブジェクトを所有します。
  2. 所有しているオブジェクトが不要になったら、そのオブジェクトに release メッセージを送信して、所有を放棄しなければなりません。
  3. 所有してないオブジェクトに release メッセージを送信して、そのオブジェクトを放棄しようとしてはいけません。

補足して、次のようなことも頭に入れておきましょう。

  • 誰もオブジェクトを所有しなくなったとき、そのオブジェクトには dealloc メッセージが送信され、そのあとメモリから解放されます。
  • あるオブジェクトを複数のオブジェクトが所有することもあります。
  • あるオブジェクトが特定のオブジェクトを重複して所有することもあります。その場合、重複した所有だけ release メッセージを送信し、所有を放棄する必要があります。
  • 所有してないオブジェクトはどこかで破棄されるので、そのオブジェクトが必要なときは、そのオブジェクトに retain メッセージを送信するか、 copy メソッドを用いて複製し、明示的に所有しなければなりません。

文章ではわかりにくいので、実例をあげて解説を試みます。

例.1 オブジェクトの作成

NSString *string = [[NSString alloc] initWithString:@"abc"];

このメッセージを送信したオブジェクトは、すなわち self は、この string が不要になったら、 string に release メッセージを送信して、オブジェクトを放棄しなければなりません。重要なことは、 self が string を放棄しなければならないことです。これは、最も遅くても self が dealloc されるときに string に release メッセージを送信しなければならないことを意味します。

例.2 簡易コンストラクタの使用

NSString *string = [NSString stringWithString:@"abc"];

このメッセージを送信して作られた string は、alloc, retain, copy, new どれにも当てはまらないので、self は string オブジェクトを所有していません。したがって、 release メッセージを送信して、放棄しようとしてはいけません。逆に考えると、この string は所有していないので、不要でない場合は、明示的に retain して所有しなければなりません。

例.3 コレクションに入れる

NSString *string = [[NSString alloc] init];
NSArray *array = [[NSArray alloc] initWithObject:string];
[string release];

NSArray などのコレクションクラスは、格納されるオブジェクトを明示的に所有します。したがって2行目の時点では、 string は self と array 両者に所有され、array は self に所有されています。string は array だけが所有すればいいので、 self は release メッセージを string に送信して所有を放棄すべきです。このコードの最後の状態は、 self が array を所有し、 array が string を所有している状態になります。

例.4 各種メソッドでオブジェクトを得た

NSURL *URL = [NSURL URLWithString:@"http://www.google.com/"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSString *html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

まず、登場するオブジェクトを挙げてみましょう。リテラルである @”http://www.google.com/” の NSString オブジェクトを除くと、URL, request, response, error, data, html があります。しかし、最後の html 以外のオブジェクトについて、 self は所有していないので、所有を放棄する必要はありません。したがって、この場合 self が所有しているのは html のみとなり、これが不要になったら所有を放棄する必要があります。

例.5 メソッドの戻り値

- (NSString *)htmlFromURL:(NSURL *)URL {
	NSURL *URL = [NSURL URLWithString:@"http://www.google.com/"];
	NSURLRequest *request = [NSURLRequest requestWithURL:URL];
	NSURLResponse *response;
	NSError *error;
	NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
	NSString *html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
	return 1;
}

例えば、このようなメソッドを考えます。 URL を引数にとり、戻り値にその URL のページを NSString で返します。先の 例.4 をメソッドにしました。ここで問題となるのは、 self が所有してしまっている html を、どうやって放棄すべきかです。 例2. のような、NSString に NSData から文字列にする簡易コンストラクタがあればいいのですが、そのようなメソッドはありません。また、関数内で release してしまうと、html がメモリから破棄される可能性があります。

このような場合の対処法として、 autorelease を使用します。 autorelease については、また別の記事を参照願いたいのですが、 autorelease メッセージを送られたオブジェクトは、あるタイミングで release メッセージが送られます。先にオブジェクトが解放されることなく、所有を放棄することができます。

例6. 簡易コンストラクタの作成

+ (id)classname:(id)anObject {
	return [[[self alloc] init] autorelease];
}

NSArray の array メソッドや、 NSString の stringWithFormat メソッドのように、簡易コンストラクタを自分のクラスでも実装したい場合があります。そのときは、このように alloc 、任意の init メッセージを送信したあとに、 autorelease メッセージも送信することで、所有の放棄されたオブジェクトを返すことができます。

例7. setter の作成

- (void)setObject:(id)anObject {
	if ( anObject != object ) {
		[object release];
		anObject = [object retain];
	}
}

いまやプロパティ構文により必要ない話ですが、setter の中身を知っておくことで、より理解が深まるでしょう。マルチスレッドを気にしなければ、このようなコードになります。なお、インスタンス変数 object にセットする setter です。また、retain で変数を所有していますが、 copy も使う場合もあります。

注意1. retainCount を用いてはならない

参照カウンタ方式のメモリ管理なので、そのオブジェクトのカウント数がいくつなのか気になり、retainCount を見ることがあるかもしれません。しかし、Objective-C において、retainCount は目安でしかありません。したがって retainCount を用いたプログラムを組んではいけません。retainCount は、デバック目的で使用されるべきではありますが、値は参考程度にしましょう。たとえば、次のコードは、どちらも retainCount が 1 ではありません。

#import <Foundation/Foundation.h>

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

	NSNumber *num = [[NSNumber alloc] initWithInt:1];
	NSString *s = [[NSString alloc] initWithString:@"ABC"];

	NSLog( @"count of num is %d", [num retainCount] );
	NSLog( @"count of s is %d", [s retainCount] );

	[num release];
	[s release];

    [pool drain];
    return 0;
}

注意2. delegate は所有してはいけない

@property (assgin) id delegate;

もし、delegate となるオブジェクトが必要な場合でも、delegate オブジェクトは所有されるべきではありません。なぜなら、delegate オブジェクトがすでに self を所有していることが多く、循環参照に陥るからです。これは参照カウンタで起きるメモリリークです。


参考資料:
詳解 Objective-C 2.0
Objective-C プログラミング言語 : メモリ管理


関連エントリ:
NSAutoreleasePool
retain と copy の使い分け方

Objective-C逆引きハンドブック

たまたま時間が余ったため、ふらっと書店に立ち寄ると、こんな書籍を見つけました。今まで Windows で開発していたデベロッパーが、サクッと iPhone アプリを開発したいときに、手元にあると非常に役に立ちそうな内容となっています。また、あれをやりたい、これをやりたい、だけど Objective-C ではどうするんだろう? という疑問をすぐに解決してくれるでしょう。

Objective-C逆引きハンドブック Objective-C逆引きハンドブック

シーアンドアール研究所 2010-02-26
売り上げランキング : 5113

Amazonで詳しく見る by G-Tools

おまけとして、こちらの書籍も紹介しておきます。

基本的には、ペンタブレットの開発話や、操作方法などの説明、ペンタブレットを活用できるソフトウェアを紹介している本となります。しかし、一番最後の章に、ペンタブレットを使ったソフトウェア開発の方法が書かれています。Windows では、Win32API, .NET の開発方法、 Mac OS X では、 Cocoa を使った開発方法が書かれています。ペンタブレットを活用したアプリケーションを開発したいデベロッパで、サンプルコードに困っているならば、この本を手に取ってみるのも良いかと思われます。

Intuos HACKS―プロフェッショナル・ペンタブレットの使いこなし術 Intuos HACKS―プロフェッショナル・ペンタブレットの使いこなし術

秀和システム 2010-03-01
売り上げランキング : 79808

Amazonで詳しく見る by G-Tools

ちなみに、 Cocoa の開発方法では、 ペンタブレットの入力は普通に mouseDown: で取得され、 NSEvent から筆圧や傾きの情報を取得できるようです。