☆メモリ管理
これは、
Memory Management Programming Guideの、
iOS4.0段階の内容を翻訳(意訳)・手入れしたものである。
すなわち、iOS5でARCなる物が導入される古い内容であるが、私はARCを使ってないので構わない。
というか、Cでメモリー管理を覚えた身にとって、AutoreleasePoolでもそうだったけど、
「自動で管理」なんて言うのは「実動作が見えない」ので安心出来ないわけである。
ちなみに、すでにアップル純正の日本語版
「Cocoaメモリ管理プログラミングガイド」
もあったりする。これはARC対応に改められている。よって、最新の情報を知りたければそれを読むべきである。
でも一応、翻訳するだけでなくわかりにくいやおかしいところ所は書き換えたので、
これはこれで役に立つんじゃないかと。もちろん保証はしないけど。
ここでは、Objective-Cにおけるメモリ管理の規則の要約している。
それは以下の通りである。
あなたは、自身が所有しているオブジェクトのみreleaseまたはautoreleaseする
- あなたは以下のオブジェクトの所有権を持っている
- 名前が"alloc" "new"で始まってる、または"copy"を含んでいるメソッドで生成したオブジェクト
- retainメッセージを送ったオブジェクト
- オブジェクトの所有権を放棄するためには、releaseまたはautoreleaseを使う
autoreleaseは「AutoreleasePoolが解放される時点で、そこに含まれるオブジェクトにreleaseを発行する」という意味である
(この意味は「自動開放プール」で理解できる)
以下の規則は基本的な規則に由来するか、端的な場合に対処する。
- 基本的な規則の当然の結果として、インスタンス変数のプロパティーとして容認されているオブジェクトを保存する必要があるならば、
あなたはそれを保持しなければならないか、コピーしなければならない。
このような場合、通常、アクセッサ・メソッドに応答をゆだねる。
-
受信したオブジェクトは通常、受信したメソッド内に値を保持することが保証されている。
そしてメソッドは、その呼び出し側に安全に値を返す。
しかし、iOSはマルチスレッドなので、メソッドを呼び出した後もリターンを待たずに実行が進む場合がある。
こういう場合、その進んだ先でオブジェクトの内容を更新してしまうと不具合が発生する可能性もあるので、
通常、受信側でretainもしくはcopyしてオブジェクトを保持する。
(「共有オブジェクトの有効性」も参照のこと)。
これらの規則の理由については、次の
「オブジェクトの所有権と処分」で述べる。
プログラムは、できるだけ少ない(適切な量の)メモリを使うようにするべきである。
環境は、あなたに対してプログラムメモリを管理するために、機構と方針を定義する。
Objective-Cプログラムでは、オブジェクトは作られて、破棄される。
アプリケーションが必要以上のメモリを使わないことを確実とするためには、
それらが必要でなくなったとき、オブジェクトは破棄されなければならない。
しかし、それらがまだ必要であるならば、オブジェクトは破棄されてはならない。
これらの要求をサポートするために、Cocoaはオブジェクト所有権の機構を定める。
それによって、あなたがいつオブジェクトを必要とするか、いつ不要になったかを指定することができる。
どんなオブジェクトでも、有効ならば一人以上の所有者を持っている。
オブジェクトが少なくとも1人の所有者を持っている限り、それは存在し続ける。
オブジェクトが所有者を持っていないならば、ランタイムは自動的にそれを破棄する
(
「オブジェクトの割当てを解除する」参照。
ここで言うランタイムとはiOSのことではなく、Objective-Cの管理システムである)。
あなたがいつオブジェクトを所有し、いつ所有権を放棄するかを明らかにするために、
Cocoaは次の方針を設定する。
- あなたは、生成したどんなオブジェクトも所有する。
あなたは、名前が「alloc」か「new」で始まるか、「copy」を含むメソッド
(たとえば、alloc、newObjectまたはmutableCopy)を使用してオブジェクトを生成(create)する。
- あなたは、retainを使うことで、オブジェクトの所有権を持つことができる。
オブジェクトは二人以上の所有者を持っているかもしれない。
オブジェクトの所有権を持つことは、それを保存し続ける(存在を保証する)ために必要なやり方である。
(これについては「アクセサメソッド」で説明する。)
- 使い終わったら、所有していたオブジェクトの所有権を放棄しなければならない。
releaseメッセージまたはautoreleaseメッセージを送ることによって、オブジェクトの所有を放棄する
(autoreleaseについては「自動開放」で述べる)。
Cocoa用語では、オブジェクトの所有権を放棄することを、「オブジェクトを解放する」と呼ばれる。
- あなたが所有しないオブジェクトの所有権を放棄してはならない。
以下のコードで考えてみる。
{
MyClass *myClass = [[MyClass alloc]init];
// ...
NSArray *sprockets = [myClass sprockets]; // メッセージsprocketsについては後述
// ...
[myClass release];
}
この例は、きちんと方針に従っている。
allocメソッドを使用してMyClassオブジェクトを生成するので、終了時にreleaseメッセージを送る。
あなたがMyClassオブジェクトからsprockets配列を得るとき、
あなたは配列を「creatしない=生成しない」ので、それにはreleaseメッセージを送らない。
所有権方針は、保持回数をによって実現される。
すなわち、それぞれのオブジェクトは保持回数を持つ。
これはNSObjectのretainCountメソッドで参照が可能である。
- オブジェクトを生成するとき、保持回数=1になる
- オブジェクトにretainメッセージを送ると、保持回数+1になる
- オブジェクトにreleaseメッセージを送ると、保持回数-1になる
- オブジェクトにautoreleaseメッセージを送ると、保持回数は将来のいくつかの場面で-1される
- オブジェクトの保持回数が0になったら、解放される(「オブジェクトの割当てを解除する」参照)
重要:
一般的に、オブジェクトの保持回数を知る必要は無い。
どんなフレームワークオブジェクトが、あなたが興味を持ったオブジェクトを保持するかわからないからである。
メモリ管理問題をデバッグする際には、
あなたはコードが所有規則を厳守することを確実とすることだけに関心を持たなければならない。
・・・とは書かれているが、やはりメモリリークの場所を追及するのにretainCountを調べるのは有効な方法である。
|
NSObjectによって定義されるautoreleaseメソッドは、レシーバーに後でreleaseする印を付ける。
オブジェクトにautoreleaseメッセージを送ることによって、
あなたはメッセージを送ったスコープを超えてオブジェクトを所有したくないと宣言する。
そのスコープは、現在の自動開放プールによって定義される(
「自動開放プール」参照のこと)。
あなたは、上記のsprocketsメソッドを次ように実装するとする:
- (NSArray *)sprockets
{
NSArray *array = [[NSArray alloc]initWithObjects:mainSprocket,auxiliarySprocket, nil];
return [array autorelease];
}
あなたは、allocを使って配列を生成する。
したがってあなたは、配列を所有して、後で所有権を放棄する役割を果たさなければならない。
そのため、autoreleaseを使っている。
他のメソッドがSprocketオブジェクトの配列を得るとき、
そのメソッドがそれを必要でなくなったとき、配列が破棄されると仮定するが、
実は、スコープ中どこでも、まだ配列は有効である(
「共有オブジェクトの有効性」参照)。
autoreleaseメソッドは、メソッドからオブジェクトを返してまだ、所有権方針に従うことを簡単にする。
実例を示すために、sprocketsメソッドの2つの誤った実装を示す。
- 次の記述は所有権方針に従っていない。場合によってはメモリーリークをもたらす。
- (NSArray *)sprockets
{
NSArray *array = [[NSArray alloc]initWithObjects:mainSprocket,auxiliarySprocket, nil];
return array;
}
オブジェクトのアドレスを示すarrayは、このメソッドの中でのみ有効なローカル変数である。
従って、基本的にはこのメソッドからリターンした時点で破棄されていると思わなければならない。
alloc自体はスタックとは違うグローバルな領域に対して行われているので、
メソッドを抜けても存在する。
だが、この場合arrayはリターン値に指定されているので、その値はスタック上に積まれ呼び出し側に戻る。
従って、呼び出し側でそのリターン値を持ってreleaseすれば問題なく動作する(メモリリークは起こらない)。
にもかかわらず問題とされるのは「所有権の方針」に従っていないからである。
呼び出し側=メソッド外で解放することは、確保者;この場合このメソッド自身が解放指示していないからである。
実際、XcodeのAnalyzerで調べると、こういう記述に対しては警告が出る。
が、くどいようだが、呼び出し側でちゃんと解放していれば問題なく動作する。
- 次の記述も間違っている。
オブジェクトはちゃんと新しい配列の所有権を放棄しているが、
配列はreleaseメッセージが送られたあと、システムによってすぐに解放される(他に保持者がいないから)。
ローカル変数arrayの示すアドレス自体は正しいのに、その指し示す内容=実体が存在しない。
したがって、メソッドは解放されて無効なアドレスを返すことになる。
- (NSArray *)sprockets
{
NSArray *array = [[NSArray alloc] initWithObjects:mainSprocket,auxiliarySprocket, nil];
[array release]; // ここで即時解放されてしまう
return array; // array 実体はすでに存在しない
}
別の方法として、クラスメソッドを使った記述方法もある。
- (NSArray *)sprockets
{
NSArray *array = [NSArray arrayWithObjects:mainSprocket,auxiliarySprocket, nil];
return array;
}
arrayWithObjects:による確保は、(その実質はともかく)所有権方針によると、配列を所有しているとはみなさない。
したがって、所有権を放棄する必要もない。
実際には、クラスメソッドによる配列確保は、AutoreleasePoolに対して行われている。
そのため、sprocketsメソッドから安全に返すことができる。
重要:
「これを理解するには、arrayWithObjects:メソッド自身がautoreleaseを使って実装されているかのように思わせる。
この場合は正しいけれども、それは厳密には詳細実装次第である。」
ということらしい。iOSでの実装はまさしくAutoreleasePoolを使っている。
「オブジェクトの実際の保持回数に関心を持つべきではないし、
あなたは返されるオブジェクトが自動開放されるかどうかを、心配するべきでない。
唯一の関心を持つべきは、あなたがそれを所有するか否かである。」だそうである。
|
Cocoaの所有権方針は、受け取ったオブジェクトが、一般的に、メソッド呼び出しのスコープ中全てで、
有効なままでなければならないことを示している。
それが解放されたかどうかの心配なしに、現在のスコープからオブジェクトを返すことも可能でなければならない。
あなたのアプリケーションにとって、
オブジェクトのgetterメソッドがたくわえられたインスタンス変数または計算された値を返すことが
重要なのではなく、あなたがそれを必要とする時にオブジェクトが有効なままであるが重要なのである。
この規則にはたまに例外がある。
- オブジェクトが集合クラスの1つから取り除かれるとき。
heisenObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// heisenObject はここでは無効である
オブジェクトが集合クラスのうちの1つから取り除かれるとき、それはreleaseメッセージを送る。
集合が、取り除かれたオブジェクトのただ一人の所有者であるならば、
取り除かれたオブジェクト;例ではheisenObjectはそれからすぐに割当てを解除される。
- 「親オブジェクト」が割り当てを解除されたとき。
id parent = <#親オブジェクトの生成#>;
// ...
heisenObject = [parent child];
[parent release]; // またはたとえば self.parent = nil;
// heisenObject はここで無効である
いくつかの状況では、あなたは他のオブジェクトからオブジェクトを取り戻す。
そして、直接的もしくは間接的に親オブジェクトを解放する。
親を解放することが割当てを解除される原因になるならば、
そして、親が子のただ一人の所有者だったなら、
子;例ではheisenObjectは、それが親のdeallocメソッド内でreleaseを送られる場合、
同時に割当てを解除される。
これらの状況から保護するためには、heisenObjectを、それを受け取る前に保持(retain)し、終わったときに解放(release)する。
例は以下の通り。
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// heisenObjectを使う
[heisenObject release];
あなたのクラスがオブジェクトのインスタンス変数を持つならば、
インスタンス変数のための値を設定しているオブジェクトが、利用中に解放されないことを確認しなさい。
したがって、それを設定するとき、オブジェクトの所有権を主張しなければならない。
また、現在持っているどんな値の所有権も放棄することを確認しなければならない。
たとえば、あなたのオブジェクトがmainSprocketを設定することを許すならば、
あなたはsetMainSprocket:を、このように実装するかもしれない:
- (void)setMainSprocket:(Sprocket *)newSprocket
{
[mainSprocket autorelease];
mainSprocket = [newSprocket retain]; // 新しいSprocketを設定する
return;
}
setMainSprocket:は、呼び出し者がその辺りで持っているつもりであるSprocketで呼び出したかもしれない。
それはオブジェクトがSprocketを他のオブジェクトと共有していることを意味する。
そのオブジェクトがSprocketを変えるならば、オブジェクトのmainSprocketは変わる。
あなたはそれを望むかもしれないが、MyClassがそれ自身のSprocketを持つ必要があるならば、
メソッドはプライベートなコピーを製作しなければならない。
- (void)setMainSprocket:(Sprocket *)newSprocket
{
[mainSprocket autorelease];
mainSprocket = [newSprocket copy]; // プライベートなコピーを作る
return;
}
このどちらの実装も、元のmainのsprocketをautoreleaseする。
これはnewSprocketとmainSprocketが同じオブジェクトであった場合の問題を回避する。
MyClassがそれを所有する唯一のオブジェクトであったとき、
sprocketが解放されると、それはすぐに割当てを解除される。
そして、それがretainされるかcopyされるとすぐに、エラーを引き起こす。
以下の実装も、その問題を解決する。
- (void)setMainSprocket:(Sprocket *)newSprocket
{
if (mainSprocket != newSprocket) {
[mainSprocket release];
mainSprocket = [newSprocket retain]; // または適切なら copy
}
}
しかし、これらのケースの全てにおいて、あなたがその所有を放棄しないので、
あなたのオブジェクトに設定される最終的なmainSprocketはメモリリークするかもしれない。
これはdeallocメソッドによって面倒を見られる(
「オブジェクトの割当てを解除する」参照)。
アクセサメソッドとその実装についての詳細は、
「アクセサメソッド」の項で更に詳細に記述する。
保持回数が0になったら、オブジェクトのメモリは解放され、システムに回収される。
Cocoa用語では、これを「解放(freed)」か、「割当てを解除する(deallocated)」という。
オブジェクトが割当てを解除されるとき、deallocメソッドが自動的に呼び出される。
deallocメソッドの役割は、オブジェクト自身のメモリを解放し、
インスタンス変数の所有権も含む、それが持つどんなリソースも処分する。
クラスがオブジェクト・インスタンス変数を持つならば、
それを解放するためにdeallocメソッドを実装しなければならず、また親のdealloc実装も呼び出さなければならない。
たとえば、MyClassクラスがmainSprocketとauxiliarySprocketのインスタンス変数を持つならば、
以下の通りにそのdeallocメソッドを実装する。
- (void)dealloc
{
[mainSprocket release];
[auxiliarySprocket release];
[super dealloc]; // 親のdeallocを呼び出す
}
重要:
あなたは、他のオブジェクトのdeallocメソッドを、決して直接呼び出してはいけない。
また、システムリソースの管理とオブジェクトの有効期間を結ぶべきでない。
詳しくは「リソース管理」を見なさい。
|
Cocoaのいくつかのメソッドは、オブジェクトがアドレス参照;つまり、ClassName **またはid *によって返される。
例えば、発生したエラーの情報を持つNSErrorオブジェクトを使ったいくつかの例がある。
NSData | initWithContentsOfURL:options:error: |
NSString | initWithContentsOfFile:encoding:error: |
これらの場合、すでに記述されたように、同じ規則があてはまる。
あなたがこれらのメソッドのいずれかを呼び出すとき、
NSErrorオブジェクトを生成しないので、あなたはそれを所有しない。
従って、解放する必要もない。
NSString *fileName = @"ファイル名";
NSError *error = nil;
NSString *string = [[NSString alloc]initWithContentsOfFile:fileName encoding:NSUTF8StringEncoding error:&error];
if (string == nil) {
// deal with error ...
}
~
[string release];
いずれにせよ、返されたオブジェクトの所有権が基本的な規則に従わないならば、
これは、メソッドのドキュメントではっきりと述べられる。
(たとえば、dataFromPropertyList:format:errorDescription:を見なさい。)
若干の状況では、2つのオブジェクトは、循環参照をするかもしれない。
つまり、各々のオブジェクトが、他のオブジェクトを参照するインスタンス変数を含む。
たとえば、
図1はオブジェクトの結びつきを保つテキストのプログラムについて考えている。
Documentオブジェクトは、文書における各々のページのために、Pageオブジェクトを生成する。
各々のPageオブジェクトは、それがどのドキュメントの中にあるかについて追跡し続けるインスタンス変数を持つ。
DocumentオブジェクトがPageオブジェクトを保持し、そしてPageオブジェクトがDocumentオブジェクトを保持するならば、
どちらのオブジェクトも決して解放されないだろう。
Documentの参照回数はPageオブジェクトが解放されるまで0になることは出来ず、
そしてPageオブジェクトはDocumentオブジェクトの割り当てが解除されるまで解放されない。
図1 循環保持
循環保持の問題の解決は、「親」オブジェクトはその「子供たち」を保持しなければならないが、
子供たちは、彼らの親を保持してはならない、ということで解決する。
図1の中で、documentオブジェクトはそのpageオブジェクトを保持する。
しかし、pageオブジェクトはdocumentオブジェクトを保持しない。
親への子供の参照は、弱い参照の例である。
それについては次章で述べる。
オブジェクトを保持することは、そのオブジェクトの「強い」参照を生成する。
その強い参照の全てが解放されるまで、オブジェクトは割当てを解除するができない。
あるオブジェクトの有効期間は、その所有者の強い参照のによって決定される。
場合によっては、この動作は、期待しているものと違うかもしれない。
オブジェクトがそれ自体の割当てを解除するのを防ぐことなく、オブジェクトへの参照を望むかもしれない。
これらの場合のために、あなたは「弱い」参照を得ることができる。
弱い参照は、オブジェクトを保持することなく、オブジェクトへのポインターを格納することによって生成される。
それ以外の方法では、循環参照が出来てしまう場合、弱い参照は欠かせない。
たとえば、Object AとObject Bが互いと通信するならば、各々は他方への参照を必要とする。
各々が他を保持するならば、接続が断たれるまで、どちらのオブジェクトも決して割当てを解除されない。
しかし、オブジェクトのうちの1つが割当てを解除されるまで、接続は断たれない。
八方ふさがりの状態である。
循環を壊すために、1つのオブジェクトが従属の役割を引き受け、他への弱い参照を得る。
具体的な例としては、ビュー階層において、親ビューはその子ビューを所有して、それゆえに保持する。
しかし、子ビューは、その親を所有しない。
子供はまだ、その親が誰であるかわかっている必要があるので、それは親への弱い参照を保持する。
Cocoaにおいては、弱い参照の場合が他にもある。
テーブルデータ源、アウトラインビューのアイテム、通知オブザーバー、ターゲットとデリゲート等である。
重要:
Cocoaでは、テーブルデータ源、アウトラインビューのアイテム、通知オブザーバー、デリゲートへの参照は全て弱いと考える。
(たとえば、UITableViewオブジェクトはそのデータ源を保持しない、そして、UIApplicationオブジェクトはそのデリゲートを保持しない)。
ドキュメントはこの慣例に例外を述べるだけである。
|
弱い参照をだけを持つオブジェクトにメッセージを送る時には、要注意である。
割当てを解除された後にオブジェクトにメッセージを送れば、アプリケーションはクラッシュする。
オブジェクトが有効である時のはっきりした条件を持たなければならない。
ほとんどの場合、弱く参照されたオブジェクトは、循環参照の場合のように、それへの他のオブジェクトの弱い参照に気づいている。
そして、それが割当てを解除するとき、他のオブジェクトに通知しなければならない。
たとえば、あなたが通知センター(notification center)でオブジェクトを登録するとき、
適当な通知が送られるとき、通知センターにはオブジェクトへの弱い参照が入って、それにメッセージを送る。
オブジェクトが割当てを解除されるとき、あなたは通知センターが、もはや存在しないオブジェクトにメッセージを送るのを防ぐため、
通知センターでそれを非登録にする必要がある。
同様に、デリゲート・オブジェクトが割当てを解除される時、
あなたは、他のオブジェクトに、setDelegate:メッセージにnil引数で送ることによって、
デリゲート・リンクを取り除く必要がある。
これらのメッセージは通常、オブジェクトのdeallocメソッドから送られる。
あなたは、一般的にまれなリソース、例えばファイル記述子、ネットワーク接続、バッファ/キャッシュをdeallocメソッドで管理するべきでない。
特に、それが呼び出される時、後にdeallocが呼び出されるだろうと仮定して、クラスを設計するべきでない。
バグのため、または、アプリケーション取り外しのため、deallocの呼び出しは遅れるかもしれないか、回避されるかもしれない。
その代わりに、インスタンスがまれなリソースを管理するクラスを持つなら、
あなたはアプリケーションを、リソースが必要でなくなったときが判るように設計しなければならない。
それから、「掃除をする」(Clean-up)ようにインスタンスに告げることができる。
典型的に、あなたはそれからインスタンスを解放するだろう、そして、deallocが後に続く。
問題が発生する場合、deallocの先頭で相乗りのリソース管理を行っているなら、以下を見直すべきである。
- オブジェクト・グラフ取り外しへの依存を命じなさい
オブジェクト・グラフ取り外し機構は、本質的に命令されない。
あなたが一般的に予想し、得るかも知れない、特定の順序は脆弱性をもたらす。
オブジェクトが予想外に自動開放プールに落ちるならば取り外し命令は変わるかもしれない。
そして、それは予想外の結果に至るかもしれない。
- まれなリソースの非再生
メモリリークはもちろん、修正されなければならないバグである、しかし、彼らは通常、すぐに致命的でない。
しかしながら、あなたが解放されると思っているときに十分なリソースが解放されないならば、
これはずっと深刻な問題に至る。
たとえば、アプリケーションでファイル記述子が尽きるならば、ユーザーはデータを保存することができない場合がある。
- 間違ったスレッド上で実行されているClean-upロジック
オブジェクトが予想外時に自動開放プールに落ちるならば、
それが偶然あったどんなスレッドのプールでも割り当てが解除されてしまう。
これは、1つのスレッドからのみ接触されるリソースにとって致命傷になる。
補足:
「オブジェクト・グラフ(Object graph)」
オブジェクト指向プログラムにおいては、オブジェクトのグループは、互いの結びつき~他のオブジェクトとの直接参照を通して、
または、中間の参照の連鎖を通して、ネットワークを作る。
オブジェクトのこれらのグループは、「オブジェクト・グラフ」と呼ばれる。
オブジェクト・グラフは小さい場合も大きい場合もあるし、単純な場合も複雑な場合もある。
一つの文字列オブジェクトを含む配列オブジェクトは、小さな、単純なオブジェクト・グラフを表す。
アプリケーションオブジェクト、ウインドウへの参照、メニューとそのビュー、そして他のサポートされているオブジェクト
を含むオブジェクトのグループは、大きく、複雑なオブジェクト・グラフかも知れない。
いくつかのオブジェクト・グラフは不完全かもしれない。
これらはしばしば、部分的なオブジェクト・グラフと呼ばれる。
部分的なオブジェクト・グラフは、グラフの境界を表す。
そして、後の段階で記入されるかもしれないプレースホールダー・オブジェクトを持つ。
一例は、ファイル所有者へのプレースホールダーを含むnibファイルである。
|
ここでは、メモリ管理についての実際的な面を説明する。
それは、
「オブジェクトの所有権と処分」で記述される基本的な概念をカバーする。
しかし、よりコード志向の観点から考える。
いくつかの単純な規則に従うことは、メモリ管理を簡単にする。
規則を厳守することに失敗すると、ほとんどの場合確実に複数のポイントでメモリーリークを起こす。
または、オブジェクトの解放のためのメッセージ送信時に実行時例外を起こす。
アプリケーションでメモリ消費をできるだけ少なくしておくために、
使われていないオブジェクトをなんとかしなければならない。
しかし、使われているオブジェクトを除去しないことを確認する必要がある。
したがってあなたは、オブジェクトがまだ役に立つことであることを示すことができる機構を必要とする。
多くの点で、メモリ管理はこのように「オブジェクト所有権」に関して最適の解釈である。
- オブジェクトは1つ以上の所有者を持っている
- オブジェクトが所有者を失ったとき、それは破棄される
- あなたが興味があるオブジェクトを破棄されたくないなら、所有者にならなければならない
- オブジェクトにもはや興味がなく破棄されても良いなら、所有権を放棄する
このモデルをサポートするために、Cocoaは「参照カウント」(reference counting)
または「保持カウント」(retain counting)を呼ばれている機構を提供する。
あらゆるオブジェクトは保持回数(reference count)を持つ。
あるオブジェクトが生成されると保持回数が1になる。
保持回数が0になると、オブジェクトは割り当てを解除される(破棄)。
あなたは保持回数をいろいろなメソッドで操作する(所有権の確保と放棄)。
alloc |
オブジェクトをメモリに割り付け、保持回数を1にする。
allocまたはnewで始まるメソッドでオブジェクトを生成する。
|
copy |
オブジェクトのコピーを作り、保持回数を1にする。
オブジェクトをコピーすると、オブジェクトを所有する。
これは"copy"を含むメソッドが返すオブジェクトに当てはまる。
|
retain |
保持回数を+1する。
オブジェクトの所有権を取得する。
|
release |
保持回数を-1する。
オブジェクトの所有権を放棄する。
0になると実体を解放する。
|
autorelease |
将来的のある場面で参照回数を-1する。
また、オブジェクトの所有権を放棄する。
自動開放プールの開放時に、含有する全てのオブジェクトも解放する。
|
実際のメモリ管理は以下の通りである(
「メモリ管理規則」も参照のこと):
- あなたは、"alloc""new"で始まる、または"copy"を含む名前のメソッド(たとえば、alloc,newObject,mutableCopy)を
使い生成した、または、retainメッセージを送ったオブジェクトを所有する。
多くのクラスは"+className..."というクラスメソッドを提供する。
これは「コンビニエンスコンストラクタ」と称され、このメソッドは新しいクラスインスタンスを生成し、初期化し返す。
あなたはこのコンビニエンスコンストラクタ、または他のアクセサメソッドから返ってきたオブジェクトを所有しない。
- 一度オブジェクトの所有を終えたなら、releaseまたはautolereaseで所有権を放棄なければならない。
一般的に、autoreleaseよりreleaseを使うべきである。
オブジェクトの即時の割り当て解除が不適当な時だけ、autoreleaseを使う。
たとえば、メソッドからオブジェクトを返す時である。
- deallocメソッドを、あなたが所有するインスタンス変数の解放のために実装しなさい
- あなたはdeallocを直接呼び出してはならない
(カスタムのdeallocメソッドの中での親のdeallocを呼び出すとき以外)
以下の簡単な例は、
「alloc」「コンビニエンスコンストラクタ」「アクセサメソッド」
の各方法を使って新しいオブジェクトを作る処理の対比を示している。
最初の例は、allocを使って新しい文字列オブジェクトを生成している。
したがってそれは、解放されなければならない。
- (void)printHello
{
NSString *string;
string = [[NSString alloc] initWithString:@"Hello"];
NSLog(string);
[string release];
}
第2の例は、新しい文字列オブジェクトをコンビニエンスコンストラクタから生成している。
ここには解放の処理はない。
- (void)printHello
{
NSString *string;
string = [NSString stringWithFormat:@"Hello"];
NSLog(string);
}
第3の例は、アクセサメソッドにより新しい文字列オブジェクトを生成している。
コンビニエンスコンストラクタと同じで、ここには解放の処理はない。
- (void)printWindowTitle
{
NSString *string;
string = [myWindow title];
NSLog(string);
}
あなたが一貫してアクセサメソッドを使うならば、メモリ管理問題を抱えている可能性はかなり減少する。
あなたがコード全体を通してretainとreleaseをインスタンス変数に使うならば、間違える可能性が高い。
ここで、カウントを設定するCounterオブジェクトを考える。
@interface Counter : NSObject
{
NSNumber *count;
}
カウンタの取得と設定のために、2つのアクセサメソッドを定義する。
取得アクセッサでは、retainまたはreleaseが必要ないように、変数を返すだけである。
-(NSNumber *)count
{
return count;
}
setterでは、
他の皆も同じ規則によって実行しているならば、
あなたは、新しいcountがいつ処分されるか分からないと仮定しなければならない。
なので、あなたはそれがないことを確実とするために、retainメッセージを送ることで、オブジェクトの所有権を取得しなければならない。
あなたは、それにreleaseメッセージを送ることによって、古いcountオブジェクトの所有権も放棄しなければならない。
(Objective-Cではnilにメッセージに送ることは許されるで、countがまだ設定されてなかったならば、これはまだ働く。)
このあと、2つが同じオブジェクトである場合に備えて、[newCount retain]を、送らなければならない。
-(void)setCount:(NSNumber *)newCount
{
[newCount retain]; // newCcount==countの時に次の文でnewCountを解放しないように
[count release]; // 確実に前のcount(分)を解放
count = newCount;
}
アクセサメソッドを使ってインスタンス変数を設定すべきでない唯一の場所は、initメソッドとdeallocである。
0を表している数値オブジェクトでカウンターオブジェクトを初期化するために、
あなたは以下の通りにinitメソッドを実装するかもしれない:
- init
{
self = [super init];
if (self) {
count = [[NSNumber alloc] initWithInteger:0];
}
return self;
}
カウンターが0以外のカウントで初期化されるのを許可するために、
あなたは以下のようなinitWithCount:メソッドを実行するかもしれない:
- initWithCount:(NSNumber *)startingCount
{
self = [super init];
if (self) {
count = [startingCount copy];
}
return self;
}
Counterクラスがインスタンス変数のオブジェクトを持つので、あなたはdeallocメソッドも実装しなければならない。
それはreleaseメッセージを送ることによって、どんなインスタンス変数の所有権でも放棄しなければならない。
そして、最後に、それは親のdeallocを呼び出さなくてはならない:
- (void)dealloc
{
[count release];
[super dealloc];
}
今度はカウンターをリセットするメソッドを実装する。
ここには2つの方法がある。
1つ目は、コンビニエンスコンストラクタで新しいNSNumberオブジェクトを生成する。
したがってここでは、retrainやreleaseメッセージは不要である。
両方ともクラスのsetアクセッサ・メソッドを使うことに注意すべきである。
- (void)reset
{
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
}
2番目は、allocでNSNumberのインスタンスを生成する方法で、それはreleaseと組になる。
- (void)reset
{
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
[zero release];
}
以下では、一般的な間違いを示す。
アクセサを使ってない
以下は、単純なケースのために正しくほとんど確かに働く。
しかし、それがアクセサメソッドを避ける場合、若干の場面で確実に間違いに至る
(あなたがretrainまたはreleaseを忘れたとき、またはインスタンス変数のためにメモリ管理回路が変わった時)。
- (void)reset
{
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[count release];
count = zero;
}
インスタンスが漏れている
- (void)reset
{
NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
[self setCount:zero];
}
新しい保持回数は1(allocから)で、メソッドのスコープ内のreleaseと釣り合わない。
新しい番号は決して、解放されない。その結果はメモリリークである。
自分が保持していないインスタンスにreleaseを送る
- (void)reset
{
NSNumber *zero = [NSNumber numberWithInteger:0];
[self setCount:zero];
[zero release];
}
numberWithInteger:はクラスメソッドなのでreleaseしてはいけない。
あなたがオブジェクトを集合(配列/辞書/セット等)に加えるとき、集合はその所有権を取得する。
集合は、オブジェクトが集合から取り除かれる、あるいは、集合自身が解放される時、所有権を放棄する。
このように、たとえば、あなたが数値の配列を生成したいならば、以下のどちらか使う。
NSMutableArray *array;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject:convenienceNumber];
}
この場合、あなたはallocを呼び出さないので、releaseを呼び出す必要もない。
NSMutableArray *array;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger: i];
[array addObject:allocedNumber];
[allocedNumber release];
}
この場合、forループのスコープの中で、allocのバランスを取るために、
allocedNumberにreleaseメッセージを送る必要がある。
配列は、addObject:によって追加された時数値を保持するので、配列の中にいる間、割り当てを解除されない。
あなたがメソッドから局所変数を返すとき、あなたは
- メモリ管理規則を厳守する
- レシーバーは割当てを解除される前に、オブジェクトを使う機会を得る
の両方を確実としなければならない。
あなたが(所有する)新しく作ったオブジェクトを返すとき、
あなたはreleaseよりもむしろautoreleaseを使って所有権を放棄しなければならない。
firstNameとlastNameを連結する単純なfullNameメソッドを考える。
1つの可能な正しい実装は以下の通りである
- (NSString *)fullName
{
NSString *string = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
return string;
}
基本的な規則に従えば、あなたはstringWithFormatから返ってきた文字列を所有していない。
なので、あなたはそれをメソッドから安全に返すことが出来る。
以下の実装も正しい。
- (NSString *)fullName
{
NSString *string = [[[NSString alloc]initWithFormat:@"%@ %@", firstName,lastName]autorelease];
return string;
}
あなたはallocで返された文字列を所有する。しかしそれにautoreleaseメッセージを送ったとき、
それへの参照を失う前に所有権を放棄する、そしてメモリ管理規則が再び当てはまる。
比較のため、以下の実装は間違っている
- (NSString *)fullName
{
NSString *string = [[[NSString alloc]initWithFormat:@"%@ %@", firstName,lastName]release];
return string;
}
純粋にメモリ管理の展望から言えば、これは正しく見える。
allocによって返される文字列を所有して、所有権を放棄するために、それにreleaseメッセージを送る。
しかし、おそらく文字列は確保直後に解放されるので、メソッドの呼び出し側は無効なオブジェクトを受ける。
次は「規則上」間違っている。
- (NSString *)fullName
{
NSString *string = [[NSString alloc]initWithFormat:@"%@ %@", firstName,lastName];
return string;
}
あなたはallocによって返される文字列を所有し、その解放の義務を負うが、スコープ内でそれを果たしていない。
確保された領域は存在し、stringもスタックに乗せて呼び出し側に戻るので
参照は可能であり、呼び出し側で解放すればメモリーリークも起こさず問題ないが、
「規則」には反している。
ここでは、自動開放を使うアプリケーションの微調整仕方を説明する。
自動解放機構を使うための全体的な情報は
「オブジェクトの所有権と処分」を読みなさい。
自動開放プールは、autoreleaseメッセージを受けたオブジェクトを含む
NSAutoreleasePool
のインスタンスである。
自動開放プールが割当てを解除されるとき、含まれる個々のオブジェクトにもreleaseメッセージを送る。
オブジェクトにreleaseの代わりにautoreleaseを送ることは、
少なくともそのオブジェクトの有効期間を、自動開放プール自体が解放されるまで、延期する。
Cocoa Touchは常に、利用できる自動開放プールがあると想定している。
実際にはmain()の中でそれを確保している。
自動開放プールはスタックに準備される。従って入れ子に出来る。
あなたが新しい自動開放プールを生成するとき、それはスタックの先頭に加えられる。
プールが割当てを解除されるとき、それらはスタックから取り除かれる。
あるオブジェクトがautoreleaseメッセージを送られるとき、それは現在のスレッドのために、現在の一番上のプールに加えられる。
自動開放プールが入れ子可能であると言うことは、それをどんな関数またはメソッド内でも設定出来ることを意味する。
たとえば、ある関数は自動開放プールを生成するかもしれず、それはまた他の自動開放プールを生成する関数を呼ぶかもしれない。
または、一つのメソッドは、外側のループのための自動開放プールを、内部のループのための他の自動開放プールを持つかもしれない。
入れ子自動開放プールへ能力は確かな利点である。
リスト1ループのための自動開放プール
-(void)sub:(NSArray *)args
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//
for (NSString *fileName in args) {
NSAutoreleasePool *loopPool = [[NSAutoreleasePool alloc] init];
NSError *error = nil;
NSString *fileContents = [[[NSString alloc]initWithContentsOfFile:fileName encoding:NSUTF8StringEncoding error:&error] autorelease];
/* 文字列が処理され、多くのオブジェクトが生成、自動開放される */
[loopPool drain];
}
[pool drain];
}
forループ処理は、一度に1ファイルを処理する。
NSAutoreleasePoolオブジェクトはこのループの始めに生成されて、終わりに解放される。
したがって、ループ(例えばfileContents)の中でautoreleaseメッセージを送られるどんなオブジェクトでも、
loopPoolに加えられる。
そして、loopPoolがループ終了後に解放されるとき、それらのオブジェクトも解放される。
main()で確保されている自動開放プールは、メインスレッド用の物である。
従って、ユーザーが別のスレッドを作った場合は、その中でそのスレッド用の自動開放プールを作成する必要がある。
リスト1で例示したように、自動開放プールが入れ子であることと言うことは一般的である。
しかし、「一番奥の」自動開放プールがスタックの一番上にあって、
スタック上で入れ子になった自動開放プールについて考えることもできる。
先に述べたように、実はこれは、入れ子になった自動開放プールが実装される方法である。
プログラムの各々のスレッドは、多くの自動開放プールを維持する。
あなたが自動開放プールを生成するとき、それは現在スレッドのスタック先頭に積まれる。
オブジェクトが自動開放されるとき、つまり、オブジェクトがautoreleaseメッセージを送られる時、
またはそれがaddObject:クラスメソッドの引数として渡される時、
それは、常に、スタックの一番上で自動開放プールに置かれる。
したがって、自動開放プールのスコープは、スタック内での位置と、その存在によって定義される。
一番上のプールは、自動開放対象オブジェクトが加えられるプールである。
新しいプールが生成されたなら、それが解放されるまで、現在の一番上のプールはスコープから出る。
あなたがスタックの先頭でない自動開放プールを解放したら、
その中の全てのオブジェクトとともに、スタック上でそれより上のすべての(解放されてなかった)自動開放プールが解放される原因になる。
あなたが推薦されない方法で終了するとき、自動開放プールにreleaseを送り忘れると、
それが入れ子を作る自動開放プールのうち一つが解放される時、一緒に解放される。
この動作には、特別な状況への意味がある。
例外が発生し、スレッドが突然現在の前後関係(context)から遷移するならば、
その前後関係に関連したプールは解放される。
しかし、そのプールがスレッドのスタック上の一番上のプールでないならば、
解放されたプールより上のすべてのプールも解放される(プロセス内のすべてのオブジェクトを解放する)。
スレッドスタックの上の一番上の自動開放プールは、それから以前特別な状態と関連した解放されたプールの下のプールになる。
この動作のため、例外ハンドラーは、autoreleaseを送られたオブジェクトを解放する必要はない。
ハンドラーが例外を再発行しないかぎり、
それは必要でないか、その自動開放プールにreleaseを送る例外ハンドラーにとって望ましくない。
簡単な解放オブジェクトの代わりに自動開放プールを生成することによって、
一時的なオブジェクトの有効期間をそのプールの有効期間まで延ばすことが出来る。
自動開放プールが割当てを解除されたあと、そのプール内容が破棄処理されている間に
内包されるオブジェクトにメッセージを送ってはいけない。
あなたが自動開放プールの前後関係を越えてオブジェクトを使わなければならないならば、
オブジェクトにretainメッセージを送ることによって実現出来る。
それに対しては、別途autoreleaseを発行する。
- (id)findMatchingObject:(id)anObject
{
id match = nil;
while (match == nil) {
NSAutoreleasePool *subPool = [[NSAutoreleasePool alloc] init];
match = [self expensiveSearchForObject:anObject];
if (match != nil) {
[match retain]; // matchを保持する
}
[subPool release];
}
return [match autorelease]; // matchを解放予約して、それを返す
}
subPoolが活動中である間、retainをmatchに送る、そしてsubPoolが開放されたあとautoreleaseを送ることによって、
matchは効果的にsubPoolから、以前活動していたプールへ移動される。
これはmatchの有効期間を延長し、それがループの外でメッセージを受けて、findMatchingObject:呼び出し側に返されるのを許す。
ここでは、あなたがなぜアクセサメソッドを使用しなければならないか、
そして、どのように宣言し、実装しなければならないか述べる。
アクセサメソッドを使用する主要な理由のうちの1つはカプセル化である。
(
Object-Oriented Programming with Objective-C
の「Encapsulation」参照のこと。)
参照カウント環境中では、それらがクラスのために大部分の基本的なメモリ管理の面倒を見ることができるということである。
一般的に、アクセサメソッドを宣言するのにはObjective-Cのプロパティーを使用する。
例:
@property (nonatomic,copy) NSString *firstName;
@property (nonatomic,readonly) NSString *fullName;
@property (nonatomic,retain) NSDate *birthday;
@property NSInteger luckyNumber;
宣言は、プロパティのためのメモリ管理回路を明言する。
多くの場合、Objective-Cのプロパティー宣言を用いてあなた自身のアクセサメソッドを実装して、
コンパイラにあなたのためにアクセサメソッドを自動生成するよう頼むことができる。
@synthesize firstName;
@synthesize fullName;
@synthesize birthday;
@synthesize luckyNumber;
あなたが自分自身で実装する必要があるとしても、
プロパティ宣言を使ってアクセサを宣言するべきである。
もちろん、あなたの実装が仕様を満たすことを確実としなければならない。
(デフォルトで、宣言されたプロパティーはatomicである。あなたがatomicな実装を提供しないならば、宣言においてnonatomicと明記しなければならない。)
単純なオブジェクトのために、アクセサをインプリメントする方法は大まかに3つある。
| getter;読み出し | setter;設定 |
(1) | リターンする前に値を保持し、自動開放する | 古い値を解放し、新しい値を保持またはコピーする |
(2) | 値を返す | 古い値を自動開放し、新しい値を保持またはコピーする |
(3) | 値を返す | 古い値を解放し、新しい値を保持またはコピーする |
(1)では、getterから返された値は呼び出しスコープの中で自動開放される。
-(NSString*)title
// getter
{
return [[title retain] autorelease];
}
- (void) setTitle:(NSString*) newTitle
// setter
{
if (title != newTitle) {
[title release];
title = [newTitle retain]; // またはcopy
}
}
なぜなら、getterから返ったオブジェクトが、現在のスコープの中で自動開放されると、プロパティー値が変えられても、それは有効なままである。
これはアクセッサをより強くするが、更なるオーバーヘッドが必要である。
getterメソッドが定期的に呼ばれると予測するなら、オブジェクトの保持と自動開放のための追加コストは、パフォーマンスコストに見合わないかも知れない。
(2)も(1)と同様に自動開放を使う。しかし、ここではsetterメソッド内ではそうしない。
- (NSString*) title
// getter
{
return title;
}
- (void) setTitle: (NSString*) newTitle
// setter
{
[title autorelease];
title = [newTitle retain]; // またはcopy
}
これのパフォーマンスは、getterの呼び出し回数が多い状況では、(1)よりかなり良い。
(3)は、全くautoreleaseの使用を避ける。
- (NSString*) title
// getter
{
return title;
}
- (void) setTitle: (NSString*) newTitle
// setter
{
if (newTitle != title) {
[title release];
title = [newTitle retain]; // copy
}
}
(3)は、定期的に呼ばれるgetterとsetterメソッドによい。
それは、例えば集合クラスのような、その値の有効期間を延ばしたくないオブジェクトにもよい。
その不利点は、古い値が(他のいかなる所有者もいないならば)すぐに割当てを解除されるかもしれないということである。
そして、他のオブジェクトがそれへの所有してない参照を維持しているならば、それは問題を引き起こす。
たとえば、
NSString *oldTitle = [anObject title]; // 現在値を読み出す;oldTitleに入るのは、この時点でのtitleのアドレス
[anObject setTitle:@"New Title"]; // 変更する;旧のアドレス=oldTitleの内容は解放され、新しいアドレスの内容が設定される
NSLog(@"Old title was: %@", oldTitle); // oldTitleの示すアドレスの内容は不定
anObjectが元のタイトル文字列を所有した唯一のオブジェクトであるならば、
新しいタイトルが設定されたあと、文字列は割当てを解除される。
NSLog()文はoldTitleが解放されたオブジェクトなので、クラッシュの原因になる。
属性(attributes)を示す値オブジェクトをコピーすることは、Objective-Cプログラムでよくあることである。
Cの変数も値オブジェクトの代わりにされることができる。
しかし、値オブジェクトは、一般の操作のために便利なユーティリティを、カプセルに入れるという長所がある。
たとえば、文字エンコードと保管をカプセル化するので、NSStringオブジェクトがchar *の代わりに使われる。
(それこそがクラスの真価である。)
値オブジェクトがメソッド引数として渡されるか、メソッドから返ったとき、
オブジェクト自体の代わりにコピーを使うことは一般的である。
たとえば、文字列をオブジェクトのnameインスタンス変数に割り当てるために、以下のメソッドを考える。
- (void)setName:(NSString *)aName
{
[name autorelease];
name = [aName copy];
}
aNameのコピーを保存することは、オリジナルから独立してオブジェクトを生じ、同じ内容を持つ。
コピーへの以降の変化はオリジナルに影響を及ぼさないし、オリジナルへの変更はコピーに影響を及ぼさない。
同様に、インスタンス変数自体の代わりにインスタンス変数のコピーを返すことも一般的である。
たとえば、このメソッドは、nameインスタンス変数のコピーを返す。
- (NSString *)name
{
return [[name copy] autorelease];
}
この章は、最初の2節を除き、自分で汎用的なクラスを実装する場合にのみ必要だと思われる。
今の私には無用というか理解出来ない部分も多いので、使ったことないし、訳も直訳で適当である。
ここでは、オブジェクトをコピーする、NSCopyingプロトコルのcopyWithZone:メソッドを実装する2つの方法について述べる。
その2つとは、allocとinitを使うか、NSCopyObjectを使うかである。
あなたのクラスにとってどちらが適切か、正しいものを選ぶためには、次の点を考慮する。
- 必要なのは深いコピーか浅いコピーか?
- 親クラスからNSCopyingの動作を継承するか?
通常、オブジェクトをコピーするには、新しいインスタンスを生成して、
元のオブジェクトの値でそれを初期化することが必要である。
ポインターでないインスタンス変数(例えばBOOL、int、float)の値をコピーすることは、直接である。
ポインター・インスタンス変数をコピーするとき、2つの方法がある。
- 浅いコピーは、コピーにポインター値のみを元のオブジェクトからコピーする。
オリジナルとコピーは、参照されるデータを共有する。
- 深いコピーは、ポインターで参照をされるデータ実体を複製して、
それをコピーのインスタンス変数に割り当てる。
インスタンス変数のsetterは、あなたが必要とするコピー種類で実装する。
対応するsetterが、新しい値をコピーするならば、あなたはインスタンス変数を深くコピーしなければならない。
- (void)setMyVariable:(id)newValue
{
[myVariable autorelease];
myVariable = [newValue copy];
}
対応するsetterが新しい値を保持するならば、あなたはインスタンス変数を浅くコピーしなければならない。
- (void)setMyVariable:(id)newValue
{
[myVariable autorelease];
myVariable = [newValue retain];
}
同様に、そのsetterが、コピーや保持することなく新しい値を、単純にインスタンス変数に割り当てるならば、
あなたはインスタンス変数を浅くコピーしなければならない(これはまれである)。
- (void)setMyVariable:(id)newValue
{
myVariable = newValue; // アドレスのコピーのみ
}
オリジナルから本当に独立しているオブジェクトのコピーをするためには、全てのオブジェクトは、深くコピーされなければならない。
あらゆるインスタンス変数は複製されなければならない。
インスタンス変数自体がインスタンス変数を持つならば、それらも複製されなければならない。
多くの場合に、混合方法は、より役に立つ。
デリゲートはターゲットのアドレスを保持し、当然実体であるターゲット(処理そのもの)は共有すべきなので浅くコピーされ、
データ容器とみなされることができるポインター・インスタンス変数は、通常深くコピーされる。
@interface Product : NSObject
<NSCopying>
{
NSString *productName;
float price;
id delegate;
}
@end
たとえば、
このインターフェースで宣言されるように、
Productクラスは、NSCopyingプロトコルに準拠し、インスタンスはname、price、delegateを持つ。
それが階層的でないデータ値を表すので、ProductインスタンスはproductNameの深いコピーを製作する。
一方で、デリゲート・インスタンス変数は、ターゲットを共有すべきなので浅いコピーを行なわなければならない。
図1は、メモリ内でのProductインスタンスとコピーのイメージを表す。
図1 浅いコピーと深いコピーによるインスタンス変数のコピー
productNameのための異なるポインター値は、オリジナルとコピーが各々それら自身のproductName文字列オブジェクト実体を持つことを示す。
priceはポインターではないのでそのままコピーされている。
delegateのためのポインター値は同じである。
それは、2つの製品オブジェクトがそのデリゲートとして同じオブジェクトを共有することを示す。
親クラスがNSCopyingを実装しないならば、
あなたのクラスの実装は、あなたのクラスで宣言されるインスタンスだけでなく、それが継承するインスタンス変数もコピーしなければならない。
通常、こうする最も安全な方法は、alloc、init~(initから始まるメソッド)、そしてsetterを使うことである。
他方、あなたのクラスがNSCopying動作を受け継いで、さらなるインスタンス変数を宣言したならば、あなたはcopyWithZone:を実装する必要もある。
このメソッドでは、継承したインスタンス変数をコピーするために、
親クラスの実装を呼び出し、そして追加のインスタンス変数をコピーする。
あなたが新しいインスタンス変数を取り扱う方法は、あなたが親クラスの実装の熟知度に依存する。
親クラスがNSCopyObjectを使ったか、使ってきただろうなら、allocとinit~を使ったのと違う方法で、
インスタンス変数を取り扱わなければならない。
クラスがNSCopying動作を受け継がないならば、あなたはalloc,init~やsetterを使ってcopyWithZone:を実装しなければならない。
たとえば、先のProductクラスのcopyWithZone:の実装は、以下のようになるかも知れない。
- (id)copyWithZone:(NSZone *)zone
{
Product *copy = [[[self class] allocWithZone: zone]initWithProductName:[self productName] price:[self price]];
[copy setDelegate:[self delegate]];
return copy;
}
継承したインスタンス変数に関連した、実装の詳細が親クラスでカプセル化されるので、
alloc,init~方法でNSCopyingを実装することは、通常、より良い。
そうすることは、必要なインスタンス変数のコピーの種類を決定するために、setterで実装された方針を使う。
クラスがNSCopying動作を継承するとき、あなたは親クラスの実装がNSCopyObject関数を使用する可能性を考慮しなければならない。
NSCopyObjectは、インスタンス変数値をコピーすることによって、それらが指すデータではなく、オブジェクトの正確な浅いコピーを作成する。
たとえば、NSCellのcopyWithZone:の実装は、以下のようになる。
- (id)copyWithZone:(NSZone *)zone
{
NSCell *cellCopy = NSCopyObject(self, 0, zone);
/* 他の初期化がここで起こると仮定しなさい */
cellCopy->image = nil;
[cellCopy setImage:[self image]];
return cellCopy;
}
上記のような実装にて、NSCopyObjectはオリジナルのセルの正確な浅いコピーを生成する。
この動作は、ポインターでないか、浅くコピーされる保持されないデータへのポインターのインスタンス変数をコピーすることにとって望ましい。
保持されたオブジェクトのためのポインターインスタンス変数は、追加の処理を必要とする。
上記例のcopyWithZone:において、imageは保持されたオブジェクトのポインターである。
imageの保持の規則は、以下のようなsetImage:アクセサメソッドの実装に反映される。
- (void)setImage:(NSImage *)anImage
{
[image autorelease];
image = [anImage retain];
}
setImage:はimageを、再設定する前に自動開放する。
上の実装におけるcopyWithZone:は、setImage:を呼び出す前に、コピーのimageインスタンスにnilを明確に設定していない。
コピーとオリジナルから参照されるimageは、一致する保持なしに解放されるだろう。
たとえimageが正しいオブジェクトを指すとしても、それは概念的に初期化されていない。
allocとinit~で生成されるインスタンス変数と違って、これらの初期化されていない変数は、nil値を有しない。
あなたは、それらを利用する前に、初期値をこれらの変数にはっきりと割り当てなければならない。
この場合、cellCopyのimageインスタンス変数は、setImage:メソッドを使ってnilに設定される。
NSCopyObjectの影響は、サブクラスの実装に及ぶ。
たとえば、NSSliderCellの実装は、以下のようにして新しいtitleCellインスタンス変数をコピーすることができた。
- (id)copyWithZone:(NSZone *)zone
{
id cellCopy = [super copyWithZone:zone];
/* 他の初期化がここで起こると仮定しなさい */
cellCopy->titleCell = nil;
[cellCopy setTitleCell:[self titleCell]];
return cellCopy;
}
親のcopyWithZone:メソッドとみなされるところは、このようになる。
id copy = [[[self class] allocWithZone: zone] init];
親クラスのcopyWithZone:メソッドは、継承されたインスタンス変数をコピーするために呼び出される。
親クラスのcopyWithZone:メソッドを呼び出すとき、
親クラス実装がNSCopyObjectを使うという可能性があるならば、
新しいインスタンス変数が初期化されないと仮定しなさい。
それらを利用する前にはっきりと、それに値を割り当てなさい。
この例では、setTitleCell:が呼び出される前に、titleCellはnilをはっきりと設定される。
オブジェクトの保持回数の実装は、NSCopyObjectを使うとき、他の意味を持つ。
あるオブジェクトが、あるインスタンス変数内の保持回数を記録するなら、
copyWithZone:の実装はコピーの保持回数を正確に初期化しなければならない。
図2は、プロセスを例示する。
図2 コピー中の参照カウンタの初期化
 |
 |
 |
|
NSCopyObjectで作ったコピー |
copyWithZone:で割り当てた
初期化してないインスタンス変数
のコピー |
図2の中の最初のオブジェクトは、メモリ内でのProductインスタンスである。
refCountの値は、インスタンスが3回保持されたことを示す。
第2のオブジェクトは、NSCopyObjectが作ったProductインスタンスのコピーである。
そのrefCount値は、オリジナルに一致する。
第3のオブジェクトは、refCountが正しく初期化された後、copyWithZone:から返ってきたコピーを参照している。
copyWithZone:の後、コピーはNSCopyObjectで生成される。
それは、1をrefCountインスタンス変数に割り当てる。
copyWithZone:への送り主は、暗黙のうちに、コピーを保持して、それを解放する役割を果たす。
「不変と可変」の概念がオブジェクトにあてはまる所で、
オリジナルが不変であるかどうかにかかわらず、NSCopyingは不変のコピーをする。
不変のクラスは、非常に効率的にNSCopyingを実装することができる。
不変のオブジェクトは変化しないので、それらを複製する必要がない。
その代わりに、NSCopyingはオリジナルをretainするように実装することができる。
たとえば、不変の文字列クラスのためcopyWithZone:は、以下のように実装できる
- (id)copyWithZone:(NSZone *)zone
{
return [self retain];
}
オブジェクトの可変コピーを製作するためには、NSMutableCopyingプロトコルを使いなさい。
オブジェクト自体は、可変コピーすることをサポートするために可変である必要はない。
プロトコルは、メソッドをmutableCopyWithZone:と宣言する。
可変コピーすることはNSObjectメソッド mutableCopyで一般に呼び出され、
それはデフォルトゾーンでmutableCopyWithZone:を呼び出す。
・・・ってなわけで、わっぱりわやや。
Cocoa Touchアプリケーションのいろいろな場面で、1つ以上のnibファイルが読み込まれ、それが含むオブジェクトが解凍される。
不要になくなったら、それらのオブジェクトを解放することに対する責任がある。
nibファイルとそれらのメモリ管理回路に関する基本は、
nib関連の条件(例えば「アウトレット」、「ファイル所有者」と「トップレベルオブジェクト」)の定義と同様に、
「リソースプログラミングガイド」(Resource Programming Guide)
で「Nib Files」を見なさい。
nibファイルが読み込まれ、アウトレットが確立したとき、nib読み込み機構は、常にアクセサメソッドを使用する。
したがって、一般的に、Objective-Cのプロパティー機能を使用して、アウトレットを宣言しなければならない。
iOSでの宣言は、以下の通りでなければならない:
@property (nonatomic, retain) IBOutlet UIUserInterfaceElementClass *anOutlet;
対応するアクセサメソッドもsynthesizeで合成しなければならない。
または宣言に従って実装し、deallocで対応する変数を解放する。
あなたが最新のランタイムを使い、インスタンス変数を合成するならば、このパターンも働くので、
それはすべての状況を通して首尾一貫したままである。
nibファイル中のオブジェクトは、保持回数=1で生成され、自動開放対象にされる。
それがオブジェクト階層を再構築し、UIKitは、setValue:forKey:を使ってオブジェクト間の接続を復旧する。
それには有効なsetterメソッドを使うか、有効なsetterメソッドないなら保持するオブジェクトをデフォルトで使う。
これは、(あなたが
「アウトレット」で示されるパターンに従うならば)
アウトレットを持っているどんなオブジェクトでも有効なままであることを意味する。
メモリ警告
ビューコントローラーがメモリ警告(didReceiveMemoryWarning)を受信したなら、
それは、現在必要でない、そして、必要とされるならば、後で再現されることができるリソースの所有権を放棄しなければならない。
そのようなリソースの1つは、ビュー・コントローラのビュー自体である。
それが親ビューを持たないなら、ビューは処分される。
(didReceiveMemoryWarningの実装の中で、UIViewControllerは[self setView:nil]を呼び出す。)
しかし、nibファイル中要素へのアウトレットは保持されるので、
たとえメインビューが破棄されるとしても、アウトレットは破棄されない。
もしメインビューが再読込みされるなら、それらは単純に置き換えられる。