2013年10月28日月曜日

iOSのメモリ管理 その1


retainとかreleaseとか意味分からないんですけど!
という訳で、今更ですがObjective-Cのメモり周りについて調べてまとめました。

このエントリは
という本を参考にしています。

ARC全盛の昨今ですが、この本はオススメなので買うといいよ!

このエントリの目次
  1. 参照カウント方式
  2. retain と release
  3. retainしなくても参照カウントが上がるメソッドがある
  4. メモリが解放されるタイミング
  5. さいごに

1.参照カウント方式
Objective-Cのメモリ管理は参照カウント方式で行われます。

「高度なメモリ管理プログラミングガイド」によると、 所有権に関する処理は、参照カウントを使って実装されています。通常、retainメソッドの呼び出し後は、これを「保持カウント」と呼びます。各オブジェクトに保持カウントがあります。
  • オブジェクトを作成すると、オブジェクトの保持カウントは1になります。
  • オブジェクトにretainメッセージを送信すると、保持カウントが1つ増えます。
  • オブジェクトにreleaseメッセージを送信すると、保持カウントが1つ減ります。
    オブジェクトにautoreleaseメッセージを送信すると、現在の自動解放プールブロックの末尾で、保持カウントが1つ減ります。
  • オブジェクトの保持カウントがゼロになると、割り当て解除されます。

はい、全然意味が分かりませんね。

もう少し具体的に
以下のようなコードがあるとします。
NSString *str = [NSString stringWithFormat:@"test"];
[str retain];
1行目でstr 変数に「test」というNSString インスタンスを設定しました。
2行目のretain で、保持カウント(=参照カウント)が1つ増えます。

このretainにて、str 変数を持つインスタンスが「『str変数が保持しているNSStringオブジェクトのアドレス』を参照しているよ」ということを明示的に示しています。

この、「オブジェクトが参照されている数」が、そのオブジェクトの参照カウントの数になります。

参照されていると何が嬉しいのか
参照されているということは保持カウントが1以上であるということです。
そして保持カウントが1以上のとき、メモリが勝手に解放されることがなくなります。

つまり、後でstr変数を参照しようとしたときに「『str変数が保持していたNSStringオブジェクトのアドレス』が既に解放されていて存在しておらず、EXEC_BAD_ACCESSエラーが起こる」ということがなくなります。
(このときの「既に存在しないNSStringオブジェクトのアドレス」を保持しているstr変数を、ゾンビオブジェクトと呼びます)

逆に保持カウントが0になると、そのメモリは即座に解放されます。

2.retain と release
どちらも、オブジェクトの参照カウントを即座に変化させるものです。

繰り返しになってしまいますが、
  1. オブジェクトにretainメッセージを送信すると、保持カウント(=参照カウント)が1つ増えます。
  2. オブジェクトにreleaseメッセージを送信すると、保持カウントが1つ減ります。

これら保持カウントの増減は即座に行われます。

オブジェクトに対してreleaseメッセージを送信して参照カウントが即座に0になったとき、それ以降そのオブジェクトはゾンビオブジェクトになります。

では、参照カウントが1以上の場合はどうなるでしょうか。
メモリ解放という後片付けをしてあげないと、ずっとメモリ上にそのアドレスが確保されることになります。

どのオブジェクトからも参照されないのにメモリ上にアドレスが確保されている状態、これをメモリリークと呼びます。

3.retainしなくても参照カウントが上がるメソッドがある
大きく言うと4種類あります。

「高度なメモリ管理プログラミングガイド」から抜粋すると、
オブジェクトの作成は、「alloc」、「new」、「copy」、「mutableCopy」で始まる名前のメソッド(たとえば、alloc、newObject、mutableCopy)で行います。

従って、先述したサンプルコードにあった
NSString *str = [NSString stringWithFormat:@"test"];
のときは、『str変数が保持していたNSStringオブジェクトのアドレス』の参照カウントが増えません。
「alloc」、「new」、「copy」、「mutableCopy」で始まる名前のメソッドが使われていないからです。

つまり、このあとで
[str retain];
のように retain メッセージを送信してあげないと、既にメモリ解放されていてEXEC_BAD_ACCESSエラーが起こってしまいます。

下記のようにalloc で始まる名前のメソッドを使えば、参照カウントは増えます。
NSString *str = [[NSString alloc] initWithFormat:@"test"];

そしてこのときは retain メッセージを送信する必要はありません。
送信すると参照カウントが更に増えて 参照カウントが0にならずにメモリ解放されることがなくなり、メモリリークしてしまう恐れがあります。

と言いつつ他のメソッドでも参照カウンタが増減します
addSubView をするとカウンタが1増え、removeFromSuperView すると1減ります。

UIView を addSubView したときは、参照カウンタが増えていることに気をつけましょう。

4.メモリが解放されるタイミング
参照カウントが0になったオブジェクトはいつメモリ解放されるのでしょうか。

自動解放プールブロックが利用されます。
暗黙的に使用されているのですが、明示的に使用する場合は下記のように書きます。
@autoreleasepool {
     SampleViewController *sampleViewController = [[[SampleViewController alloc] init] autorelease];
     self.window.rootViewController = sampleViewController;
}

ここで先述したことを思い出してください。
「高度なメモリ管理プログラミングガイド」に下記の記述がありました。
オブジェクトにautoreleaseメッセージを送信すると、現在の自動解放プールブロックの末尾で、保持カウントが1つ減ります。

もう少し具体的な仕組みを説明します。

オブジェクト(上の例で言う sampleViewController)は、autorelease メッセージを投げられると自身を自動解放プールに登録します。
他のオブジェクトも autorelease メッセージを投げられるたびに、自身を自動解放プールに登録します。

そして、自動解放プールの末尾にきたとき、つまり } に処理がきたとき、登録されたすべてのオブジェクトに対して release メッセージを送ります。
ここで初めて参照カウントが減ります。

でも autorelease を使わない場合はいつ解放されているのか
イベント毎に、自動解放プールが作られています。
そしてイベントに対応したメソッドが終わるごとに自動解放プールも終わり、メモリが解放されます。

イベントとは、
UITableViewController にてセルが表示されるとき(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath)や、
セルをタップしたとき(- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {)
などのことです。

それらのイベントのメソッドが完了したとき、メモリは解放されるのです。

逆に言うと、上記のように表示とタップしたときのようにメソッドをまたいで変数を使いたいときは、reatain メッセージを送信して参照カウンタを1つ増やしておく必要があるのです。

ちなみに、自動解放プールは入れ子にできます。

例えば、自動解放プールAの中に自動解放プールBを作成できます。
そして、自動解放プールAにとうろくしたオブジェクトは、自動解放プールBが完了しても、自動解放プールAが完了するまではメモリ解放されません。

変数のスコープと同じように考えられますね。

さいごに
ARCを使っていればあまり意識することはないかもしれませんが、内部の動きを把握しておくのは大切なことだと思います。

他にもアクセサメソッドの挙動やインスタンス変数の宣言の意味も把握しておきたいです。

というわけで、続く!

認識違いがあればご指摘頂きたいです!

参考書籍

Related Posts Plugin for WordPress, Blogger...