A Day In The Life

とあるプログラマの備忘録

iOSアプリ開発のメモリ管理で気をつけること

Objective-C での開発にだいぶ慣れてきたのですが、いまだメモリ管理まわりでハマることが多いのでまとめてみました。

メモリを確保してから解放するまでの流れ

メモリを確保してから解放するまでの流れは以下の図のようになります(図はアップルのサイトから引用)。
メモリ管理のイメージ

alloc または init すると参照カウントが1になります

alloc または init 系メソッドを呼ぶと参照カウントが1になります。
このように書いたのは、クラスの仕様によって alloc メソッドで retainCount が1になるオブジェクトと init 系のメソッドで retainCount が1になるオブジェクトがあるためです。alloc と init はセットで呼ばれることがほとんどなのでこの違いが問題になることはないと思いますが念のため。

int main() {
  /* allocで参照カウント1 */
  NSObject *o = [NSObject alloc];
  // 参照カウント1
  NSLog(@"%d", [o retainCount]);
  o = [o init];
  // 参照カウント1
  NSLog(@"%d", [o retainCount]);

  /* initで参照カウント1 */
  NSArray *a = [NSArray alloc];
  // 参照カウント-1
  NSLog(@"%d", [a retainCount]);
  a = [a init];
  // 参照カウント1
  NSLog(@"%d", [a retainCount]);
}

NSArray, NSDictionary などのコンテナ系クラスや NSValue などは alloc メソッドで retainCount が-1になります。これらのクラスは alloc メソッドを再定義している可能性が高いです。alloc メソッドを再定義しない限り alloc で参照カウンタが1になると覚えておきましょう。
推測ですが Apple が提供しているライブラリは alloc したあと必ず init メソッドで初期化しないとプログラムが落ちる仕様になっているのだと思います。

retain されると参照カウントが+1される

retain メソッドを呼ぶと参照カウントが+1されます。

int main() {
  id hoge1 = [[Hoge alloc] init];
  // 参照カウント1
  NSLog(@"%d", [hoge1 retainCount]);
  id hoge2 = [hoge1 retain];
  // 参照カウントはともに2
  NSLog(@"%d", [hoge1 retainCount]);
  NSLog(@"%d", [hoge2 retainCount]);
}

Copy メソッドはオブジェクトの性質によって振る舞いが変わります

インスタンスをコピーする場合、浅いコピーをするか深いコピーをするかどうかで copy メソッド後の retainCount の値が変わります。まずは実装を見てください。

-(void)method {
  // NSArrayのインスタンス生成
  NSArray *foo = [[NSArray alloc] init];
  NSArray *bar = [foo copy];
  // retainCountはともに2
  NSLog(@"%d", [foo retainCount]);
  NSLog(@"%d", [bar retainCount]);
  // 同一アドレス 実行環境ではともに0x52018c0と出力された
  NSLog(@"%p", foo);
  NSLog(@"%p", bar);
}
-(void)method2 {
  // NSMutableArrayのインスタンス生成
  NSArray *foo = [[NSMutableArray alloc] init];
  NSArray *bar = [foo copy];
  // retainCountはともに1
  NSLog(@"%d", [foo retainCount]);
  NSLog(@"%d", [bar retainCount]);
  // 別アドレス 実行環境では0xb2ad50,0xb6acc0と出力された
  NSLog(@"%p", foo);
  NSLog(@"%p", bar);
}

インスタンスが NSArray か NSMutableArray で retainCount の値が違うことがわかると思います。これはNSArray が浅いコピー、NSMutableArray が深いコピーをしているからです。
浅いコピーと深いコピーの違いは以下のようになります。

  • 浅いコピー
    アドレスのコピー
  • 深いコピー
    インスタンスを新たに生成してオブジェクトの持つ値をすべてコピー

NSArray のように後から値を変更することができないオブジェクトは深いコピーをしてもメモリ領域の無駄なので浅いコピーが実装されているのだと思います。浅いコピーが実装されているオブジェクトの copy メソッドは実質的に retain と同じ動きになります。
copy メソッドを使うときは対象のオブジェクトが深いコピーを実装しているかどうか確認するようにしてください。
可変オブジェクト(NSMutableArray, NSMutableString など)を使う場合にそのオブジェクトを値としてみる場合は copy そうじゃない場合は retain を使うようにしましょう。

参考

release メソッドでメモリを解放する

参照カウントが1の状態で release メソッドを呼ぶとメモリが解放されます(オブジェクトの dealloc メソッドが呼ばれます)。参照カウントが1以上のときは参照カウントが-1されます。
参照カウントが1の状態で release メソッドを呼ばないとメモリが解放されないので注意してください。また参照カウント1で release メソッドを呼んでからさらに release メソッドをよぶと EXC_BAD_ACCESS で落ちます。

int main() {
  // 参照カウント1
  id hoge1 = [[Hoge alloc] init];
  // メモリ解放
  [hoge1 release];
  // EXEC_BAD_ACCESSで落ちます(二重解放)。
  [hoge1 release];
  // 参照カウント1
  id hoge2 = [[Hoge alloc] init];
  // 参照カウント2
  [hoge2 retain];
  // 参照カウント1(メモリは解放されない)
  [hoge2 release];
  // メモリ解放
  [hoge2 release];
}

自分で alloc, copy, retain したインスタンスは自分で release すること

例を3パターンあげてみます。

@interface Hoge : NSObject {
  NSArray *foo_;
  NSArray *bar_;
  NSMutableArray *baz_;
}
// パターン1:retain指定あり
// プロパティに値が代入されたときに参照カウントが+1される
@property(retain) NSArray foo;
// パターン2:retain指定なし
// プロパティに値が代入されても参照カウントは+1されない
@property(assign) NSArray bar;
// パターン3:copy
// プロパティに値が代入されると参照カウントが1になる
@property(copy) NSMutableArray baz;
@end

@implementation Hoge
@synthesize foo = foo_;
@synthesize bar = bar_;
@synthesize baz = baz_;
- (id)init {
  if ((self = [super init])) {
  }
  return self;
}
- (void)dealloc {
  // retainCountは2
  NSLog(@"%d", [foo_ retainCount]);
  // retainCountは1
  NSLog(@"%d", [bar_ retainCount]);
  NSLog(@"%d", [baz_ retainCount]);
  // bar_のreleaseは不要
  [foo_ release];
  [baz_ release];
  [super dealloc];
}
@end

// 呼び出し元
int main() {
  // allocしたインスタンスはreleaseすること 
  Hoge *hoge = [[Hoge alloc] init];
  id foo = [[NSArray alloc] init];
  hoge.foo = foo;
  id bar = [[NSArray alloc] init];
  hoge.bar = bar;
  id baz = [[NSMutableArray alloc] init];
  hoge.baz = baz;
  : 処理
  [hoge release];
  // retainCountはともに1
  NSLog(@"%d", [foo retainCount]);
  NSLog(@"%d", [bar retainCount]);
  NSLog(@"%d", [baz retainCount]);
  [foo release];
  [bar release];
  [baz release];
}

Hoge クラスの bar プロパティは注意が必要です。例示のため assign プロパティに直接インスタンスを生成して代入してますがあまり良い方法ではありません。

release を忘れてメモリリークする例

以下の例では Foo クラスと Bar クラスのインスタンスを生成した後に release をしていないためメモリリークします。

int main() {
  Hoge *hoge = [[Hoge alloc] init];
  hoge.foo = [[NSArray alloc] init]; // メモリリーク
  hoge.bar = [[NSArray alloc] init]; // メモリリーク
  hoge.baz = [[NSMutableArray alloc] init]; // メモリリーク
  : 処理
  [hoge release];
}
@property に retain を指定するかどうかの基準

外部からプロパティの値を変更できる場合は retain 指定、読み取り専用プロパティ(readonly)やデリゲートプロパティは assign にするのがおすすめです。

複数インスタンスを管理するときは NSAutoreleasePool がおすすめ

複数インスタンスを管理するときは NSAutoreleasePool を使うと便利です。
C++ .NET の Struct みたいにスタックにクラスのインスタンスを生成できない代わりにこのような仕組みがあるのではないかと思います。

// releaseで解放
-(void)method {
  // メモリが確保される
  id hoge1 = [[Hoge alloc] init];
  id hoge2 = [[Hoge alloc] init];
  id hoge3 = [[Hoge alloc] init];

  // メモリ解放
  [hoge1 release];
  [hoge2 release];
  [hoge3 release];
}
// AutoreleasePoolを使う
-(void)method2 {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  // メモリが確保される
  id hoge1 = [[[Hoge alloc] init] autorelease];
  id hoge2 = [[[Hoge alloc] init] autorelease];
  id hoge3 = [[[Hoge alloc] init] autorelease];

  // hoge1,hoge2,hoge3のreleaseメソッドが呼ばれる
  [pool release];
}

release してはいけない場合もあります。

alloc せずにクラスファクトリメソッドを使ってインスタンス生成したオブジェクトやAutorelease 対象のオブジェクトを release してはいけません。また NSUserDefaults などシングルトンのオブジェクトも release してはいけません。release すると思わぬところで EXC_BAD_ACCESS が発生しアプリが落ちます。

  // Autorelease対象になるのでreleaseしなくてよい。
  NSArray *array = [NSArray array]; // クラスファクトリメソッド
  NSArray *array2 = [[[NSArray alloc] init] autorelease];
  NSDictionary *dic = [NSDictionary dictionary]; // クラスファクトリメソッド
  // シングルトンオブジェクトreleaseしなくてよい
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  UIAccelerometer *theAccelerometer = [UIAccelerometer sharedAccelerometer];

イベント内で autorelease を使うときの注意点

iOS の場合、イベントループイの間で AutoreleasePool の管理がされています。そのため UIViewController で発生する UI 系のイベントメソッドで作成したオブジェクトは autorelease を呼ぶだけでメモリ開放されます。

@interface HogeViewController : UIViewController {
}
@end

@implementation HogeViewController
-(void)viewDidLoad {
  id hoge1 = [[[Hoge alloc] init] autorelease];
  id hoge2 = [[[Hoge alloc] init] autorelease];
  id hoge3 = [[[Hoge alloc] init] autorelease];
  : 処理
}
@end

ただしiOS Application Programming Guide : Allocating Memory Wiselyによるとイベントループに張られた AutoreleasePool を当てにしてプログラミングすることは、リソースの限られた iPhone アプリの開発ではあまりおすすめされてないようです。autorelease を呼んでも次のイベントループまでメモリが解放されないので不要なインスタンスはすぐにrelease するかイベント内で自前で AutoreleasePool を設定するほうが望ましいとのことです。イベント内で大量のインスタンス生成をするときは自前で AutoreleasePool を作成しましょう。[NSArray array]や[NSDictionary dictionary]なども autorelease を使うことになるので注意しましょう。

文字列定数は release しなくてもよい

NSString クラスのインスタンスを文字列定数として初期化した場合 release する必要がありません。ただし NSMutableString クラスのインスタンスや NSString#initWithFormat: メソッドで初期化した文字列は文字列定数として扱われないので release する必要があります。

  // releaseしなくてよい
  NSString *hoge1 = @"aaaaaa";
  NSString *hoge2 = [NSString stringWithString:@"bbbbbbbbb"];
  NSString *hoge3 = [[NSString alloc] initWithString:@"cccccc"];
  // 参照カウントは1だがreleaseしたらEXEC_BAD_ACCESSで落ちる
  NSMutableString *hoge4 = [NSMutableString stringWithString:@"dddd"];
  // releaseする必要がある
  NSMutableString *hoge5 = [[NSMutableString alloc] initWithString:@"eeee"];
  NSString *hoge6 = [[NSString alloc] initWithFormat:@"%@", @"fffffff"];
  // 出力結果は2147483647
  NSLog(@"%d", [hoge1 retainCount]);
  NSLog(@"%d", [hoge2 retainCount]);
  NSLog(@"%d", [hoge3 retainCount]);
  // 出力結果は1
  NSLog(@"%d", [hoge4 retainCount]);
  NSLog(@"%d", [hoge5 retainCount]);
  NSLog(@"%d", [hoge6 retainCount]);
  [hoge5 relase];
  [hoge6 relase];

NSTimer はメモリ管理方法が特殊なので気をつける

NSTimer クラスのオブジェクトは NSRunLoop で管理されているため retain,release する必要がありません。NSTimer#invalidate メソッドを呼ぶとタイマーが停止され NSRunLoop 内の NSTimer のオブジェクトがリリースされます。
また NSTimer オブジェクトは target: に指定されたオブジェクトを retain するので UIViewController のdealloc メソッドより先のタイミングで invalidate する必要があります。

@interface HogeViewController : UIViewController {
  NSTimer *timer;
}
@end

@implementation HogeViewController
- (void)viewWillAppear:(BOOL)animated {
  NSLog(@"self retainCount:%d", [self retainCount]);
  // autoreleaseも不要
  timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                           target:self
                                         selector:@selector(onTime)
                                         userInfo:nil
                                          repeats:YES];
  NSLog(@"timer retainCount:%d", [timer retainCount]);
  NSLog(@"self retainCount:%d", [self retainCount]);
}
- (void)onTime {
  ・・・処理
}
- (void)viewDidDisappear:(BOOL)animated {
  NSLog(@"self retainCount:%d", [self retainCount]);
  // relase不要(releaseするとアプリが落ちる)。
  if (timer) [timer invalidate];
  NSLog(@"timer retainCount:%d", [timer retainCount]);
  NSLog(@"self retainCount:%d", [self retainCount]);
  timer = nil;
}
@end
invalidate のタイミングを間違ったためにメモリリークする例

UIViewController の dealloc メソッド内に invalidate 呼び出しを書くと永遠に UIViewController の dealloc メソッドが呼び出されなくなってしまいますので気を付けてください。

@implementation HogeViewController
- (void)viewDidLoad {
  NSLog(@"self retainCount:%d", [self retainCount]);
  timer = scheduledTimerWithTimeInterval:100
                                  target:self
                                selector:@selector(onTime)
                                userInfo:nil
                                 repeats:YES];
  // timerオブジェクトがHogeViewControllerオブジェクトをretainする
  NSLog(@"self retainCount:%d", [self retainCount]);
}
- (void)onTime {
  ・・・処理
}
// deallocメソッドは永遠に呼び出されない!
- (void)dealloc {
  if (timer) [timer invalidate], timer = nil;
  [super dealloc];
}
@end

UIWebView または MKMapView オブジェクトの release には気をつける

UIWebView または MKMapView オブジェクトを release する場合は、先に必ず delegate プロパティに nil をセットしてください。忘れるとアプリがクラッシュする場合があります。
UIWebView を使ったサンプルを示します。

@interface HogeViewController :UIViewController {
  UIWebView *webView;
}
@end

@implementation HogeViewController
// viewDidUnloadメソッドについては後述します。
- (void)viewDidUnload {
  [super viewDidUnload];
  webView.delegate = nil;
  [webView release];
}
- (void)dealloc {
  webView.delegate = nil;
  [webView release];
  [super dealloc];
}
@end

ページ読み込み中に UIWebView オブジェクトを relaese するとページ読み込み終了時に解放済みメモリにアクセスしてしまいクラッシュします。

Delegate メソッド内でインスタンス開放するときは気をつける

CLLocationManagerDelegate で起こった現象です。少し複雑なので別記事にまとめました。

メモリ警告時は不要インスタンスのメモリを解放すること

メモリ領域が圧迫されると UIViewController の didReceiveMemoryWarning イベントや viewDidUnload イベントが発生します(下図)。
※わかりやすくするために loadView,viewWillDisappear および viewDidDisappear は図から端折ってます。
メモリ警告時の流れ
viewDidUnload では viewDidLoad でインスタンス化したオブジェクトのメモリを解放します。こうすることでアプリがメモリ圧迫でクラッシュするのを防ぎます。viewDidUnload でメモリを解放しないと再び viewDidLoad が呼ばれたタイミングでメモリリークします。
またメモリ圧迫時 viewDidUnload メソッドが呼び出される状況下において Interface Builder でインスタンス化したオブジェクトは自動でメモリ解放されます。ただし UIViewController と IBOutlet 接続されたインスタンスプログラマがきちんと解放する必要があります。このときプロパティには必ず nil を代入するようにしてください。オブジェクトが nil になっていないとメモリ解放されません。
id:tokoromさん情報ありがとうございました。

@interface HogeViewController :UIViewController {
  UIText *text;
  UILabel *label;
}

@property (retain) IBOutlet UILabel *label;
@end

@implementation HogeViewController
@synthesize label;
- (void)viewDidLoad {
  text = [[UITextView alloc] init];
}
- (void)viewDidUnload {
  [super viewDidUnload];
  // viewDidLoadでインスタンス化したオブジェクトは解放する
  [text release];
  /* 
   * IBOutlet 接続されているオブジェクトも解放が必要
   * 必ず nil を代入すること IBOutlet 接続が切れません
   */
  [label release], label = nil;
}
/* iPhone OS 2.2まではこの書き方
- (void)didReceiveMemoryWarning {
  if ([self.view superview] == nil) {
    [text release];
    [label release], label = nil;
  }
  [super didReceiveMemoryWarning];
}
*/
- (void)dealloc {
  [text release];
  [label release], label = nil;
  [super dealloc];
}
@end

[label release], label = nil;の部分はself.label = nil;と書いてもOKです(プロパティのセッターメソッドで release してくれるため)。
※以前 Interface Bulder でインスタンス化したオブジェクトは viewDidUnload で解放しなくても良いと書きましたが IBOutlet で UIViewController から参照されているオブジェクトは解放する必要があります。間違っていたため一部文章を修正しました(2011/04/25)。

マルチスレッド環境でのメモリ管理

マルチスレッド環境ではスタックごとに AutoreleasePool を作成する必要があります。詳細は長くなるので別記事にまとめました。

Instrumentsを活用しましょう。

Xcode付属のInstrumentsを使うとメモリの使用状況がわかります。Run > Start with Performance Tool > Leaksで起動します。

静的解析ツールを使うとある程度メモリリークを防げます。

Clang Static Analyzer を使うとメモリリーク箇所を解析して教えてくれます。完璧じゃないので油断は禁物ですけど。ちなみに Xcode3.2 からは標準で Clang Static Analyzer が組み込まれています。Build > Build and Analyze で解析できます。

サンプルコード

NSTimerとメモリ警告の検証用サンプルコードを公開します。参考にどうぞ。

参考書籍

オブジェクト指向入門 第2版 原則・コンセプト オブジェクト指向入門 第2版 原則・コンセプト

「9章メモリ管理」の「参照カウンタを使ってオブジェクトの管理をするときの利点と欠点」についての説明がかなり参考になりました。

詳細 Objective-C 2.0 詳解 Objective-C 2.0 改訂版

言わずと知れた Objectice-C の名著メモリ管理に関しても詳しく書かれています。