2011年6月6日月曜日

iOSメモリのお作法

本稿は、2011/5/28(土)に開催された、第4回iPhoneアプリ開発合宿in府中にて発表した内容です。
お作法という偉そうな題名にしていますが、ごく基本的な内容です。iOSの公式ドキュメントに書いてある内容をまとめたものになっています。

スライド 「iOSメモリのお作法(改訂版)」


良いアプリとは?
良いアプリの条件は数々ありますが、内容の前に重要なのが落ちない事です。可用性が低いアプリはいくら良いものを持っていても評価が低くなってしまい、レビューが荒れる原因にもなりかねません(^^;)。かなり勿体無いです。本稿は、アプリがなぜ落ちるのか、またiOSでのメモリの作法を理解することで、落ちないアプリに近づいていただくことを目的としています。


アプリはなぜ落ちるのか?
アプリが落ちる原因についてはいくつか考えられます。

  • メモリリーク
メモリリークは確保したメモリが開放されずに残ってしまうことです。すぐに落ちる原因にはならないのですが、空きメモリが足りなくなってくると、iOSに強制終了されられることがあります。

  • 不正アクセス

不正アクセスとは、存在しなかったり、アクセスが許可されていなかったりするメモリエリアをアクセスすることです。ポインタ変数の内容が破壊されたり、そもそもポインタでない値をポインタとしてアクセスすると落ちたり、落ちなくても正しく動作しなかったり(これは非常に見つけにくいタイプのバグです)します。

  • ゾンビ

ゾンビとは、開放してしまったインスタンスを使ってしまうことです。Objective-Cでは、開放時にインスタンスの使っていたメモリ領域は0クリアされ、メソッド呼び出しに必要な情報も消えてしまいます。そのためインスタンスのポインタを覚えておいて解放後にメッセージを送ったりすると落ちます。

  • 存在しないメソッドの呼び出し

存在しないメソッド呼び出しをすると即落ちします。これはタイプミスの他に、iOSのバージョンによってAPIがなくなったりすることによっても引き起こされます。コンパイルエラーにならないので検出しにくいミスの一つです。

などなど。これらの他にもありますが、いずれにしてもメモリが大きな要因の一つであることは間違いありません。



iOSでのメモリのお作法
さて、iOSのメモリの扱い方で理解すべき4つの作法をまとめてみます。
  • 作法1 retainカウンタを理解しよう
retainカウンタとは、Objective-Cの基本的なクラスであるNSObjectが持っている機能で、確保された時は1、retainメッセージが送られると+1、releaseメッセージが送られると-1されるカウンタです。このカウンタが0になると、deallocメソッドが実行され、インスタンスのメモリが解放されます。(スライドアニメーション参照)


複数の箇所から使用されるインスタンスの場合、使っている最中に勝手に開放されると困ります。そこでカウンタを+1してあげることで他の箇所でreleaseされてもインスタンスが開放されてしまうことを防ぐ仕組みです。

  • 作法2 retain/releaseの対応を意識しよう
基本的には、自分が使い始めるときにretainし、使い終わったらreleaseします。
自分がalloc/copy/newが含まれるメソッドで確保したものは責任を持ってreleaseする必要がありますが、これ以外の例えばstringWith〜などで確保したインスタンスは、後述するautoreleaseという仕組みで自動開放されるため、releaseしてあげる必要はありません(してはいけない)。

  • 作法3 autoreleaseを活用しよう
alloc/copy/newが含まれるメソッド以外で確保したもの、およびautoreleaseメッセージを明示的に送ったインスタンスは、autoreleaseプールというところに記憶され、あるタイミングで解放されます。イベント処理したメソッドが終わり、iOSに処理が戻った時に開放されるという理解でよいと思います。(スライドアニメーション参照)


メソッドを抜けたら取っておかなくてもよいインスタンスは、確保時に同時にautorelease指定してあげると、releaseの事を考えなくても済むので楽になります。

ただし、forループなどの中で大量にautoreleaseインスタンスが確保される場合などは、iOSに制御が戻らないため、大量のインスタンスが解放されないまま蓄積することになります。こんな時は自分でautoreleaseプールを作成し、明示的にdrain(開放)してあげる必要があります。

[例]
for (int i = 0; i < 100000; i++) {  //たくさん
    id pool = [[NSAutoreleasePool alloc]init]; //autoreleaseプールの作成
    id c = [[MyClass new]autorelease];
    NSString s = [NSString stringWithFormat:@"〜%d", i];
    //などなど
    [pool drain];
}


作法4 プロパティを活用しよう

これが本稿の目玉です。Objective-Cでは通常、retain〜releaseの対応付けを意識して正しくretainの数だけreleaseを呼ぶことが要求されます。ですがエラー時などすべての実行パスにて正しく実行されることを保障しようとするとかなり面倒です。
そこで、retain属性プロパティの特性を利用します。
メリットは、releaseしすぎることがない、都度retainを明記しなくていい、という所です。

実はretain属性のプロパティのセッターの処理は、こんなふうになっています。

id xxx_;    // xxxプロパティ値の保持変数


//xxxプロパティのセッター
- (void) setXxx:(id)x
{
    if (xxx_ != x) {            //値が変わったら
        [xxx_ release];        //開放してから
        xxx_ = [x retain];    //retainして記憶
    }
}


いま保持している値と違う値をセットする場合、前の値(インスタンス)をreleaseします。そして新しい値をretainして保持します。
ここで新しい値としてnilを指定したらどうなるでしょう。
xxxに保持されているインスタンスはreleaseされ、nilが新たな値として保持されます。Objective-Cでは、nilに対してどんなメッセージを投げても何も引き起こさないことが保証されていますから、[x retain]; (xはnil) は何も起りません。つまり前の値のreleaseだけが行われます。
さらにnilをセットしても、nilをreleaseしてnilをretainするだけなので、何も起りません。つまり何度も実行してもreleaseし過ぎるということがないのです。

確保したいときは、
self.xxx = [[MyClass new] autorelease];   (autoreleaseは必要です)
余談ですが newは、allocしてからinitするのと同じ意味です。

他からもらってきたインスタンスを入れたいときは普通に、
self.xxx = [otherObject getInstance];

開放したいときは、
self.xxx = nil;

念のためdeallocでもnil代入を行っておくと完璧ですね。
- (void) dealloc
{
    self.xxx = nil;
}


簡単ですね。値がプロパティに入っている間はretainカウンタが+1されているのでよそで開放される心配はなく、nilを入れてやれば自動的にretainカウンタは-1されるし、nilを何度いれても開放しすぎることがありません。便利。


余談ですが、self.を付け忘れると別の意味(単なる変数アクセス)になって肝心のプロパティの働きをしないので注意です。


それでも落ちるときは
作法に気をつけて実装したつもりでも落ちるときはデバッグして原因を探ることになりますが、その時役に立つのがAnalyzeとInstrumentsです。
詳しくは割愛しますが、Analyzeは、XCode4の場合Productメニュー>Analyzeまたは、実行ボタン長押しでAnalyzeボタンに切り替えてそれを押すことで起動します。

メモリリーク、または開放ミスに関してグラフィカルにアドバイスを表示してくれます。
基本、Analyzeの警告がない状態を目指しましょう。
ただし完璧ではなく、例えばループのn回目に確保して、n+1回目に開放している場合などはメモリリークの可能性として検知してしまいます。

同様に、Instrumentsは、Product>Profileまたは実行ボタンをProfileに切り替えてそれを押すことで起動します。
メモリリークや、ゾンビ、あと実行時間のプロファイリングなどを行うことが出来ます。かなり強力なツールですので、ぜひ試してみてください。


0 件のコメント:

コメントを投稿