iOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール

2012/3/22 いくつか修正、加筆しました。
追記もご覧下さい:iOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール [追記] カプセル化について « Zero4Racer PRO Developer’s Blog
対象がiOS4以上の場合は、ARCを使用するのもオススメです。iOS 5 公開記念! Objective-Cのメモリ管理の革命、 ARC 超入門(サンプルはgitHubに公開) « Zero4Racer PRO Developer’s Blog をご覧下さい。

@Awaresoft さんのこの記事が、ほとんどすべての疑問に答えています。とてもよい記事なので合わせてご覧下さい。プロパティに対応するインスタンス変数の命名規則について – Awaresoft

iOS プログラミングでのメモリ管理の基本

iPhone開発で主に使用される言語は、Objective-C 2.0 です。とくにiPhoneで使用されるObjective-Cには、ガベージコレクションが搭載されていないため、Javaや、C#などの言語のように自動でメモリの確保、解放を行ってくれないため、プログラマがメモリの確保、解放を管理してあげる必要がある必要があります。
その反面、使用するメモリをコントロール出来るのでiPhone上の限られた(少ない)メモリを効率的に使用することが出来ます。しかし、これは、メモリ管理に慣れていないプログラマにとっては至難の作業です。
EXC_BAD_ACCESSというエラーは、あるメモリにアクセスしようとしたが、そのメモリ領域にあったデータは既に解放されていて、何も見つかりませんでしたという、致命的なエラーです。このエラーは、シンプルなプログラムだと簡単に原因を見つけられるのですが、複雑に入り組んだプログラムになると、探すのが非常に難しく、小一時間、私の場合最長半日、原因解明に費やしたことがあります。EXC_BAD_ACCESSが解決出来ずに、もう、iPhoneアプリ作成やめてやれ!と思った事も多少なりとも有りました。この記事で、EXC_BAD_ACCESSエラーを出しにくくする、また対処しやすくするために私が行っている対策方法を記します。

iOS プログラミングでのメモリ管理の原理

iOS Reference Library日本語ドキュメントの中には、Cocoaメモリ管理プログラミングガイドというPDFドキュメントが有り、そこで詳しく説明されているのですが,基本原理は

  • 参照カウンタというのは、各オブジェクトが、いくつのオブジェクトから必要とされているかを表す値
  • alloc, copy と書くと、そのオブジェクトの参照カウンタが一つ増え、メモリが確保される
  • retainと書くと、そのオブジェクトの参照カウンタが一つ増える
  • releaseと書くと、そのオブジェクトの参照カウンタが一つ減る
  • autoreleaseと書くと、そのイベントが終了した時に参照カウンタが一つ減る
  • 参照カウンタが0になった瞬間、そのデータ領域は解放され、使用不可能になる(このあとにアクセスしようとすると、EXC_BAD_ACCESSエラーが出る

というものです。簡単そうですが、これが以外と面倒です。正しく使うためには、

  • 使い始める時に参照カウンタを一つ増やす
  • 使い終わって、用が無くなった時に参照カウンタを減らす

事をしてあげる必要がある訳です。これを、忘れずに、確実に行うために、私は我流の6つのルールを作成しています。パフォーマンスのためにチューニングが必要になるときも有りますが、基本的にこのルールの中でチューニングするようにしています。

我流、iOSメモリ管理6つのルール

  1. retain, release と(init, deallocの中以外では)書かない

  2. これは、意識的に書かないようにしています。書けば簡単になる事は確かに多く有りますが、あとからの問題をなくすために、書きません。他のルールを守れば、書かずにすむはずです。

  3. オブジェクト型を生成する時には、必ずプロパティを作成する

  4. Objective-Cプログラミング言語 PDF(第 5 章 宣言済みプロパティ p71-)に有るようなプロパティを作成するようにします。int, bool, double, NSRange, CGRectなどの、値に関しては、外部からアクセスが有る場合のみプロパティを作成しますが、NSString, NSData, UITableView, UIViewControllerなどのオブジェクト型に関しては、内部保持のためであっても、関数がいのコードでの利用の場合は、必ずプロパティを作成します。メソッドの中身は、自分で定義も出来るのですが、簡潔を期すために、@synthesizeを使用するようにしています。
    [sourcecode language=”objc”]
    //
    // MemoryManagementTest.h
    // memoryTest
    //
    // Created by Tomohisa Takaoka on 2/19/11.
    // Copyright 2011 Tomohisa Takaoka. All rights reserved.
    //

    #import <Foundation/Foundation.h>

    @interface MemoryManagementTest : NSObject
    @property (nonatomic, retain) UIImage * imageMemTest;

    @end

    //
    // MemoryManagementTest.m
    // memoryTest
    //
    // Created by Tomohisa Takaoka on 2/19/11.
    // Copyright 2011 Tomohisa Takaoka. All rights reserved.
    //

    #import "MemoryManagementTest.h"

    @implementation MemoryManagementTest
    @synthesize imageMemTest=_imagesoft;
    @end
    [/sourcecode]
    こんな感じです。delegate以外の場合は、基本的にretainを使う事が多いです。プロパティに対応するインスタンス変数の命名規則について – Awaresoft の記事にある様に、name=_name;の書式をするのが最新、かつ、正しい理解のようです。

    そして、値を保存するときは、ドット記法を使うようにしています。(setter、init、deallo以外では)

    [sourcecode language=”objc”]
    // 良い例 ↓
    self.imageMemTest = [UIImage imageNamed:@"test.png"];

    // 悪い例 ↓
    imageMemTest = [UIImage imageNamed:@"test.png"];
    [/sourcecode]
    こんな感じです。悪い例は、値だけ代入して、保持しないので、勝手にメモリが解放されます。
    追記:2012/3/22
    initで、self.something=obj; を記述するのは、勧められていません。理由は、クラスがサブクラス化された時に、selfアドレスが変わる可能性があるからです。それを理解したうえでご使用ください。

  5. allocを書くときは、必ずautoreleaseを同じ行に書く

  6. オブジェクトを作成する方法には二つあって、

    • 自分でallocと書く場合(自分でメモリを確保する)
    • [sourcecode language=”objc”]
      self.myController = [[[UIViewController alloc] initWithNibName:@"testNib" bundle:nil] autorelease ];
      self.myTableView = [[[UITableView alloc] initWithFrame:CGRectZero] autorelease];
      [/sourcecode]
      のようなもの

    • 自分でallocと書かずに、関数の中でクラスがメモリの自動確保する場合
    • [sourcecode language=”objc”]
      self.imageTest = [UIImage imageNamed:@"test.png"];
      self.testString = [NSString stringWithFormat:@"Test is %d percent",100];
      [/sourcecode]
      のようなもの

    の二パターンが有ります。
    iOS Reference Library日本語ドキュメントの中には、Cocoaメモリ管理プログラミングガイド(オブジェクトの所有権ポリシー p11)で書かれているように、

    自分が作成したオブジェクトはすべて自分が所有する。
    オブジェクトを所有している場合は、それを使い終わったときに所有権を放棄する責任がある。
    当然、オブジェクトを所有していなければ、オブジェクトの所有権を放棄することはできない。

    という原則が有るので、パターン1(自分でallocと書く場合)は、必ず、autoreleaseとセットにして書きます。パターン2の場合は、たいてい、関数の中で、allocとautoreleaseが書かれているので、パターン1で、autoreleaseを書いた場合と、パターン2は、同じ結果になります。ルール2であるように、必ず、プロパティを使って、ドット記法を使用して保存しているので、ここの部分で、オブジェクトとしてのメモリの確保がなされます。
    追記:2012/3/22
    外のクラスからアクセスする必要のない物は,Imprementationの中にカプセル化しますiOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール [追記] カプセル化について « Zero4Racer PRO Developer’s Blog

  7. Array, Dictionaryは、オブジェクトを保持してくれる

  8. よく混乱するのが、Array, Dictionaryの記述方法です。原則は通常と一緒なので、サンプルだけ書きます。
    [sourcecode language=”objc”]
    self.myArray = [NSMutableArray arrayWithCapacity:0];
    [myArray addObject:[UIImage imageNamed:@"test.png"]];
    [myArray addObject:[[[UIImage alloc] initWithContentsOfFile:@"test.png"] autorelease]];

    self.myDic = [[[NSMutableDictionary alloc] init] autorelease];
    [myDic setObject:[NSString stringWithFormat:@"test %d",100] forKey:@"value1"];
    [myDic setObject:[[[NSString alloc] initWithCString:"test" encoding:NSASCIIStringEncoding] autorelease] forKey:@"value2"];
    [/sourcecode]
    ルール2、3に従って、ドット記法、allocとautoreleaseの組み合わせを使っている事に気づいていただけますか?このように、allocとautoreleaseは必ず一緒に使う事を思いに止めておくと、間違えにくくなります。

  9. メモリ解放時は、dealloc以外では、=nil, dealloc内で、[release]する

  10. メモリ解放時は、release を呼んであげる必要があるのですが、releaseを呼んでも、変数には値が入りっぱなしになるので、ドット記法で、nilを代入するのが確実です。そうすることによって,変数の初期化と、メモリの解放が同時に行われます。
    追記:2012/3/22
    Appleのドキュメントを見ると、deallocで、self.something=nilを行うのは、KVO[キー値監視通知]が発生する危険があるため、勧められていません。viewDidUnloadで行えば問題ありません。(setter、init、deallo以外では)しかし、viewDidUnloadは、必ず呼ばれる訳ではないので、deallocで、releaseしてあげましょう。

    [sourcecode language=”objc”]
    -(void) dealloc {
    [imageMemTest release];
    }
    [/sourcecode]

  11. Delegateをnilにするのは、受け取り側の責任

  12. iOS SDKでは、delegateデザインパターンがよく使われます。これは、イベントが発生した時に通知を受けるオブジェクトを指定する物で、画面要素のUIKitで用いられています。このパターンの特徴として、イベントを発生させる側の、delegateオブジェクトは、ルール2で示されている、プロパティを作成する際に、値を保持しない、”assingn”というプロパティが用いられている事です。これは、メモリ管理でありがちな、お互いに保持し合って、メモリが解放されなくなる事のないためだと思われます。それで、delegateパターンを使う場合、パターンを使うオブジェクトが、自分が終了する時に、もう呼ばれないようにdelegateの登録の解除を行う必要があります。サンプルコードを記述します。
    [sourcecode language=”objc”]
    //
    // memoryTestViewController.m
    // memoryTest
    //
    // Created by Tomohisa Takaoka on 2/19/11.
    // Copyright 2011 Tomohisa Takaka. All rights reserved.
    //

    #import "memoryTestViewController.h"

    @implementation memoryTestViewController

    // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
    – (void)viewDidLoad {
    self.table.delegate = self;
    [super viewDidLoad];
    }

    – (void)dealloc {
    self.table.delegate = nil;
    [super dealloc];
    }

    @end
    [/sourcecode]
    いかがでしょうか?”self.table.delegate = nil;”として、自分自身のオブジェクトが終了する際に、もう呼ばれないようにdelegateにnilを代入しているのが分かるでしょうか?そのようにして、意図せぬメモリアクセスを防いでいます。
    追記:2012/3/22
    ARCでは、weakプロパティを使用することによって、上記の作業を自動でコンパイラに行わせることが出来ます。

ここまでで、メモリ管理に関して、我流の6ルールを紹介しました。このやり方だけだと、メモリの消費が激しく、対策をしないといけないときも有るのですが、ほとんどの場合問題なく使えています。全ての事を説明することが出来無いので、所々、理由の説明をはしょっています。ここがなぜか、わからない!とか、このやり方は無いだろ!という突っ込みや質問を大歓迎です。是非情報共有お願いします。

追記1

KatokichiSoft さんから、表現の誤りの指摘をうけ、追記しました。

「iOS 開発で、EXC_BAD_ACCESS とさよならするための6つのルール」への17件のフィードバック

  1. オブジェクトは必ずプロパティにする、というのは、利便性を考えれば理解はできますが、
    それはクラス設計者が楽をするためであって、カプセル化を無意味にする行為なので、それほどいい選択とは言えないと思いますねぇ。

  2. 参考になりました。
    ちなみに、記事の冒頭で
    「とくにiPhoneで使用されるObjective-Cには、ガベージコレクションが搭載されて_いる_ため」
    となってしまっています。単純な書き間違いだと思いますので、一応ご報告まで。

    1. 指摘の通りの、書き間違いでした!指摘ありがとうございます。

  3. 「2.オブジェクト型を生成する時には、必ずプロパティを作成する」
    ですが、それでもついうっかり

    // 悪い例 ↓
    imageMemTest = [UIImage imageNamed:@”test.png”];

    とやってしまうことがあります(私だけ?)。

    これを防ぐため、私はインスタンス変数名に以下のとおりアンダースコアを
    付けるようにしています。すると「悪い例」の場合はビルドエラーとなります。
    多少手間ですが。。。

    @interface MemoryManagementTest : NSObject {
    UIImage * imageMemTest_; // 変数名をアンダースコア付きに
    }

    @implementation MemoryManagementTest
    @synthesize imageMemTest = imageMemTest_; // 対応する変数名を指定
    @end

    「Google Objective-Cスタイルガイド」に記載されている方法です。
    http://www.textdrop.net/google-styleguide-ja/objcguide.xml

    1. この方法は、いい方法ですね。変数名をアンダー付きにする時に、最初にせずに,最後にしているのがポイントですね。(最初にアンダーがつくのは、アップルに予約されています)
      アンダー付きか、付けないかは、好みの問題になると思いますが、アップルのサンプルでは、かなりの物がアンダー無しになっているので、私はアンダー無しでやっています。
      それでも、self.を付け忘れて、エラーになる時がたまに有ります。。。
      コメントありがとうございます。

  4. おかげで、iOS5で動かなくなっていたアプリが動くようになりました。ありがとうございます。

    1. よかったです。ARCが使えないプロジェクトもあるので、まだこれが必要かもしれませんね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください