実践的なメモリ管理
メモリ管理ポリシー で説明した基本的な概念は単純ですが、メモリ管理を容易にし、プログラムの信頼性と堅牢性を維持しながら同時にリソース要件を最小限に抑えるための実践的な手順があります。
アクセッサメソッドを使用してメモリ管理を容易に
クラスに、オブジェクトであるプロパティがある場合は、値として設定されているオブジェクトが使用中に割り当て解除されないようにしなければなりません。したがって、オブジェクトが設定されている場合、その所有権を主張しなければなりません。また、現在保持されている値の所有権を放棄することを確認なければなりません。
時には、退屈でつまらないように見えるかもしれませんが、アクセッサメソッドを一貫して使用すると、メモリ管理に問題が発生する可能性がかなり減ります。コード全体でインスタンス変数に retain と release を使用しているなら、間違ったことをほぼ確実にやっています。
カウントを設定したい Counter オブジェクトを考えてみましょう。
@interface Counter : NSObject @property (nonatomic, retain) NSNumber *count; @end;
この プロパティ は、2 つのアクセッサメソッドを宣言します。通常、メソッドを合成するようにコンパイラに依頼する必要があります。しかし、それがどのように実装されるかを知ることは有益です。
"get" アクセッサでは、合成インスタンス変数を返すだけなので、retain または release する必要はありません:
- (NSNumber *)count { return _count; }
"set" メソッドでは、他の人が同じ規則でプレイしている場合は、新しいカウントはいつでも破棄されるため、オブジェクトの所有権を保持しなければなりません。retain メッセージを送信する事によって、オブジェクトが破棄されないことを確認します。古いカウントオブジェクトの所有権を放棄するには、release メッセージを送信しなければなりません。(Objective-C でメッセージを nil に送信することは許されています。したがって、_count がまだ設定されていない場合でも実装は機能します)。[newCount retain] の後に、2 つが同じオブジェクトの場合は、これを送信しなければなりません。不注意にそれを割り当て解除する事は望ましくありません。
- (void)setCount:(NSNumber *)newCount { [newCount retain]; [_count release]; // Make the new assignment. _count = newCount; }
アクセッサメソッドを使用してプロパティ値を設定
カウンタをリセットするメソッドを実装するとしましょう。いくつかの選択肢があります。最初の実装では、alloc で NSNumber インスタンスを作成するので、release とのバランスを取ります。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [self setCount:zero]; [zero release]; }
2 番目は、コンビニエンスコンストラクタを使用して新しい NSNumber オブジェクトを作成します。したがって、メッセージを retain したり release する必要はありません。
- (void)reset { NSNumber *zero = [NSNumber numberWithInteger:0]; [self setCount:zero]; }
両方ともセットアクセッサ (set accessor) メソッドを使用することに注意してください。
以下は単純なケースではほぼ確実に正しく動作しますが、アクセッサーメソッドを避けるように誘うため、ある段階で間違いが発生する可能性が非常にあります (たとえば、retain または release を忘れた場合や、インスタンス変数の変更のためのメモリ管理の意味で)。
- (void)reset { NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; [_count release]; _count = zero; }
また、キー値監視 を使用している場合、この方法で変数を変更することは KVO に準拠していないことにも注意してください。
イニシャライザメソッドと dealloc でアクセッサメソッドを使用しない
インスタンス変数を設定するためにアクセッサメソッドを使用してはならない場所は、イニシャライザ メソッドと dealloc だけです。ゼロを表す数値オブジェクトを使用してカウンタオブジェクトを初期化するには、以下のように init メソッドを実装します。
- init { self = [super init]; if (self) { _count = [[NSNumber alloc] initWithInteger:0]; } return self; }
カウンタがゼロ以外のカウントで初期化されるようにするには、以下のように initWithCount: メソッドを実装します。
- initWithCount:(NSNumber *)startingCount { self = [super init]; if (self) { _count = [startingCount copy]; } return self; }
Counter クラスにはオブジェクトインスタンス変数があるため、dealloc メソッドも実装しなければなりません。インスタンス変数の所有権を release メッセージを送信して放棄し、最終的に super の実装を呼び出す必要があります。
- (void)dealloc { [_count release]; [super dealloc]; }
弱い参照を使用して循環保持を避ける
オブジェクトを保持すると、そのオブジェクトへの 強い参照 を作成します。その強い参照がすべて解放されるまで、オブジェクトは割り当て解除できません。したがって、循環保持 と呼ばれる問題は、2 つのオブジェクトが循環参照を持つ場合に発生する可能性があります。つまり、相互に強い参照を持っています (直接または次の、最初に戻る強い参照を持つ他のオブジェクトの連鎖を通して)。
図 1 に示すオブジェクトの関係は、潜在的な循環保持を示しています。Document オブジェクトには、ドキュメント内の各ページの Page オブジェクトがあります。各 Page オブジェクトには、どのドキュメントにそれがあるかを追跡するプロパティがあります。Document オブジェクトに Page オブジェクトへの強い参照があり、Page オブジェクトに Document オブジェクトへの強い参照がある場合、どちらのオブジェクトも割り当て解除できません。Document の参照カウントは、Page オブジェクトが解放されるまでゼロになることはできず、Page オブジェクトは Document オブジェクトが割り当て解除されるまで解放されません。
図 1 : 循環参照の図解
循環保持の問題に対する解決方法は、弱い参照を使用することです。弱い参照 は、ソースオブジェクトがそれへの参照を持つオブジェクトを保持しない非所有関係です。
しかし、オブジェクトグラフをそのままにするには、強い参照がどこかに存在しなければなりません (弱い参照しかない場合は、ページと段落 (paragraph) に所有者がなく、割り当て解除される可能性があります)。したがって、Cocoa は、"親" オブジェクトがその "子" への強い参照を維持すべきであり、子は親への弱い参照を持たなければならないという規約を確立しています。
したがって、図 1 では、ドキュメントオブジェクトはそのページオブジェクトへの強い参照を持っていますが、ページオブジェクトはドキュメントオブジェクトに対する弱い参照を持っています (保持しません)。
Cocoa の弱参照の例には、テーブルデータソース、アウトラインビューアイテム、通知 オブザーバ、およびその他のターゲットと delegate がありますが、これらに限定されません。
弱い参照のみを保持するオブジェクトにメッセージを送信する場合は注意が必要です。割り当て解除された後にオブジェクトにメッセージを送信すると、アプリケーションがクラッシュします。オブジェクトが有効である場合は、明確な条件を持たなければなりません。ほとんどの場合、弱く参照されたオブジェクトは、循環参照の場合のように、他のオブジェクトの弱い参照を認識し、解放されたときに他のオブジェクトに通知する責任があります。たとえば、オブジェクトを通知センターに登録すると、通知センターはオブジェクトへの弱い参照を格納し、適切な通知がポストされるとメッセージを送信します。オブジェクトが割り当て解除されると、通知センターとの登録を解除して、通知センターが、もはや存在しないオブジェクトにメッセージをもう送信しないようにする必要があります。同様に、デリゲートオブジェクトが割り当て解除されるときは、他のオブジェクトに nil 引数を指定して setDelegate: メッセージを送信してデリゲートリンクを削除する必要があります。これらのメッセージは通常、オブジェクトの dealloc メソッドから送信されます。
使用中のオブジェクトの割り当て解除を避ける
Cocoa の所有権ポリシーでは、受信したオブジェクトは通常、呼び出し元のメソッドの有効範囲を通じ有効であるべきであることを指定しています。また、現在のスコープから受け取ったオブジェクトを、それが解放されることを恐れることなく返すこともできなければなりません。アプリケーションでは、オブジェクトの getter メソッドがキャッシュされたインスタンス変数または計算された値を返すことは重要ではありません。重要なのは、オブジェクトが必要な間だけ有効であるということです。
この規則には時折例外があり、主に 2 つのカテゴリのいずれかに分類されます。
- オブジェクトが基本 コレクションクラス の 1 つから削除されたとき。
- "親オブジェクト" が割り当て解除されたとき。
heisenObject = [array objectAtIndex:n]; [array removeObjectAtIndex:n]; // heisenObject could now be invalid.
オブジェクトが基本コレクションクラスの 1 つから削除されると、release (autorelease ではなく) メッセージが送信されます。コレクションが削除されたオブジェクトの唯一の所有者であった場合、削除されたオブジェクト (この例では heisenObject) はすぐに割り当て解除されます。
id parent = <#create a parent object#> // ... heisenObject = [parent child] ; [parent release]; // Or, for example: self.parent = nil; // heisenObject could now be invalid.
状況によっては、別のオブジェクトからオブジェクトを取得し、親オブジェクトを直接的または間接的に解放します。親を解放するとそれは割り当て解除され、親が子の唯一の所有者だった場合、子 (この例では heisenObject) は同時に割り当て解除されます(親の dealloc メソッドで autorelease メッセージではなく release が送信されたと仮定します)。
これらの状況から保護するために、受信時に heisenObject を保持し、使用を終了したら解放します。例えば:
heisenObject = [[array objectAtIndex:n] retain]; [array removeObjectAtIndex:n]; // Use heisenObject... [heisenObject release];
dealloc を使って希少リソースを管理しない
通常は、ファイルディスクリプタ、ネットワーク接続、バッファやキャッシュなどの希少なリソースを dealloc メソッドで管理しないでください。特に、dealloc が呼び出されると思ったときに呼び出されるように、クラスを設計するべきではありません。dealloc の呼び出しは、バグやアプリケーションの破損のために遅延または回避される可能性があります。
代わりに、そのインスタンスが希少なリソースを管理するクラスを持っている場合は、リソースが不要になったことを知って、その時点でインスタンスに "クリーンアップ" するようにアプリケーションを設計する必要があります。通常、インスタンスを解放し、dealloc を実行しますが、それ以外の場合は問題は発生しません。
dealloc の上でリソース管理をピギーバックしようとすると問題が発生する可能性があります。例えば:
- オブジェクトグラフ のティアダウンへの順序依存。
オブジェクトグラフのティアダウンメカニズムは、本質的に順序付けられていません。典型的には特定の順序を期待したり得るかもしれませんが、脆弱性を導入します。例えば、オブジェクトが予期せずに release されるのではなく autorelease されると、ティアダウンの順序が変更され、予期しない結果が生じる可能性があります。 - 希少なリソースを再生しない。
メモリリークは修正すべきバグですが、一般的に即時に致命的ではありません。しかし、希少リソースがリリースされると期待されるときにリリースされない場合、より深刻な問題に陥る可能性があります。たとえば、アプリケーションでファイル記述子が不足している場合、ユーザーはデータを保存できないことがあります。 - クリーンアップ・ロジックが間違ったスレッドで実行されています。
オブジェクトが予期しない時に autorelease された場合、そのオブジェクトはどのスレッドの自動解放プールブロックでも割り当て解除されます。これは、1 つのスレッドからのみアクセスする必要があるリソースにとっては簡単に致命的になる可能性があります。
コレクションに含まれるオブジェクトを所有
オブジェクトを コレクション (配列、ディクショナリ、またはセットなど) に追加すると、コレクションの所有権が取得されます。オブジェクトがコレクションから削除されたとき、またはコレクション自体が解放されたとき、コレクションは所有権を放棄します。したがって、たとえば、数値の配列を作成したい場合は、以下のいずれかを行います。
NSMutableArray *array = <#Get a mutable array#> NSUInteger i; // ... for (i = 0; i < 10; i++) { NSNumber *convenienceNumber = [NSNumber numberWithInteger:i]; [array addObject:convenienceNumber]; }
この場合、あなたは alloc を呼び出さなかったので、release を呼び出す必要はありません。新しい数値 (convenienceNumber) を保持する必要はありません。なぜなら、配列がそうするからです。
NSMutableArray *array = <#Get a mutable array#> NSUInteger i; // ... for (i = 0; i < 10; i++) { NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i]; [array addObject:allocedNumber]; [allocedNumber release]; }
この場合、for ループの範囲内で allocedNumber に release メッセージを送信 して、alloc のバランスを取る必要があります。配列には addObject: によって追加されたときの数字を保持していため、配列中にある内は割り付け解除されません。
これを理解するには、コレクションクラスを実装した人の立場に立ってください。あなたの下から消えたたオブジェクトの世話をするようにされたオブジェクトは全く手に負えないので、渡されたときにそれらに retain メッセージを送信します。それらが削除されている場合は、バランスの取れた release メッセージを送信しなければならず、残りのオブジェクトは、あなた自身の dealloc メソッド中に release メッセージを送信する必要があります。
保持カウントを使用して所有権ポリシーの実装
所有権ポリシーは、retain メソッドの後に通常 "保持カウント(retain count)" と呼ばれる参照カウントによって実装されます。各オブジェクトには保持カウントがあります。
- オブジェクトを作成すると、保持カウントは 1 になります。
- オブジェクトに retain メッセージを送信すると、その保持カウントは 1 だけ増分されます。
- オブジェクトに release メッセージを送信すると、その保持カウントは 1 だけ減分されます。
- オブジェクトに autorelease メッセージを送信すると、現在の自動解放プールブロックの最後に、保持カウントは 1 減分されます。
- オブジェクトの保持カウントがゼロに減少すると、オブジェクトは割り当て解除されます。
前の章 次の章