インフィニットループ 技術ブログ

2014年03月13日 (木)

著者 : tecperator

Objective-Cのメモリ管理おさらいと解放tips

こんにちは。
iOS/Objective-Cによるアプリ開発では参照カウントによるメモリ管理が必要になります。
今回はiOS/Objective-Cでのメモリ管理の基礎からメモリリークの検知まで解説したいと思います。

参照カウント

Objective-Cの全てのオブジェクトはNSObjectを継承しています。
全てのオブジェクトはこのNSObjectのメソッドを通じて参照カウントを扱います。
abc3
abc0

この参照カウントが、オブジェクトがいくつのオブジェクトから強参照されているかを表し、
0になったオブジェクトは誰からも参照されていないため解放される、という仕組みになっています。
C++に詳しい方はshared_ptrをイメージすると良いでしょう。
より詳細な解説はWikipediaなどが参考になります。

Objective-Cの参照カウント

Manual Reference Counting

iOS5以前のメモリ管理の手法です。
MRC自体は今からObjective-Cを書く場合には推奨しませんが、
ARCはMRCの上に成り立っているので解説したいと思います。
NSObjectに実装された参照カウントを上下するメソッドを手動(Manual)で呼びます。

// _foo, _barはインスタンス変数
- (id)initWithView:(UIView*)view
{
  self = [super init];
  if (self) {
    _foo = [view retain]; // retain: 参照カウントを+1する
    _bar = [[NSMutableArray alloc] init]; // alloc: 新たにオブジェクトを生成, カウントは1
  }
  return self;
}
- (void)dealloc
{
  [_foo release]; // releaseで参照カウントを-1
  [_bar release];
  [super dealloc];
}

このように、MRCでのメモリ管理は面倒です。しかし、実際には以下のいくつかの機能によって負担を軽減できます。
 

autorelease

NSObjectにはretain/releaseの他にautoreleaseが用意されています。

NSMutableArray *foo = [[[NSMutableArray alloc] init] autorelease];
...

autoreleasereleaseと同様に参照カウントを-1しますが、即座には行わずにNSAutoreleasePoolに積みます。
NSAutoreleasePoolの解放と共に積まれたオブジェクトのreleaseが実行されます。
そのため一時オブジェクトの生成に適しています。
勿論、NSAutoreleasePoolの解放時にはreleaseされるため、このオブジェクトを継続して使用する場合は
retainして所有する必要があります。

NSMutableArray *foo = [[[NSMutableArray alloc] init] autorelease];
NSMutableArray *foo = [NSMutableArray array];

上の二行は同等な意味を持ちます。
標準ライブラリの多くに生成からautoreleaseまでを行うショートカットメソッドが用意されています。

property

インスタンス変数へのsetter/getter(アクセスメソッド)の定義は、同じような処理が多く手間がかかります。
Objective-Cでは@propertyによってこれらを自動生成することができます。それどころか、インスタンス変数も自動で宣言されます。

@interface Hoge : NSObject
@property (nonatomic, retain) UIView *foo;
@property (nonatomic, retain) NSMutableArray *bar;
...
@end

これによって、最初のコードは以下のように書き直せます。

- (id)initWithView:(UIView*)view
{
  self = [super init];
  if (self) {
    self.foo = view; // propertyの生成したsetterがretainする
    self.bar = [NSMutableArray array];
  }
  return self;
}
- (void)dealloc
{
  self.foo = nil; // setterが以前の値をreleaseする
  self.bar = nil;
  [super dealloc];
}

propertyの属性にretainを指定したため、setterは前のオブジェクトをreleaseして
新たなオブジェクトをretainするように働きます。

オーナーシップ

後述するARCでは意識することはありませんが、参照カウントに関わる規則がメッセージの命名規則に沿って決められています。
また、闇雲なretain/releaseはメモリリーク/クラッシュの温床になるため、
MRCを使う必要に迫られたときはMac Developer LibraryのMemory Management Policyを読むことを強くおすすめします。(ARCの場合でも読むとよいです)

Automatic Reference Counting

現在主流のメモリ管理方法です。参照カウントを上下するコードをコンパイラが自動挿入します。
これに伴い、NSObjectのretain/release/autoreleaseをプログラマが直接触る必要が無くなり、
実際、触ることができなくなります。

@interface Hoge : NSObject
// propertyに指定する属性も,
// MRCで使用していた実動作的な名称(assign, retain, ..)から
// 抽象的な名称(strong, weak, ..)となる
@property (nonatomic, strong) UIView *foo;
@property (nonatomic, strong) NSMutableArray *bar;
...
@end
...
@implementation Hoge
...
- (id)initWithView:(UIView*)view
{
  self = [super init];
  if (self) {
    self.foo = view;
    self.bar = [NSMutableArray array];
  }
  return self;
}
// コンパイラがdeallocでnilをsetするコードを挿入するため, deallocは不要

この例ではpropertyを使用しているためdeallocの自動挿入の他に違いが見られませんが、
ARCではローカル変数などでも同様にpropertyが隠蔽していたようなretain/releaseの自動挿入が行われます。
そのため、通常はautoreleaseが不要であると言えますが、@autoreleasepoolによって使用することができます (詳細は省きます)

循環参照

ARC, MRC問わず参照カウントによるメモリ管理には循環参照の問題が発生します。
ab
aがbを強参照し、bがaを強参照します。このような状態は参照カウントでは解決できず、
このまま他からの参照が切り離されれば、永遠に解放されない状態…メモリリークとなります。
参照カウント方式ではプログラマが依存関係を解決できるようにしなければなりません。

ブロック

特にブロックでは循環参照を引き起こしがちになります。
最も単純な例では:

self.hogeCallback = ^{
  ...
  [self fuga];
};

ブロックは参照している変数をデフォルトで強参照するため、selfがブロックを強参照し、ブロックがselfを強参照しています。
そのため、明示的にhogeCallbackにnilなどを代入してselfからブロックへの参照を切り離さなければなりません。

self.hogeCallback = ^{
  ...
  [self fuga];
};
...
self.hogeCallback = nil; // self から ブロック への参照を切り離す

weak

明示的に参照を切り離さなければならないケースも存在しますが、多くのケースでは弱参照を用いるのが有効です。
上の例では以下のように弱参照する変数を通すことでブロックからselfへの強参照を防ぐことができます。

__weak typeof(self) self_ = self;
self.hogeCallback = ^{
  ...
  [self_ fuga];
};

ブロックに限らず、weakが適したケースは多くあります。所有関係を意識するのが重要となります。
delegateを例に取ると、委譲する側が委譲先のオブジェクトを強参照した場合に循環してしまうので、delegateはweakで弱参照するのが適切です。
いくつかの標準ライブラリの使用にも注意が必要になります。
例えば、NSTimerはランループが所有しているため、明示的にinvalidateして止める必要があります。

メモリリークを見つける

Xcodeのツールを利用する

Analyze

Xcode上で、Product > Analyzeでソースコード上の潜在的なリークの原因などを解析してくれます。

Profile

leaks
Xcode上で、Product > Profile > Leaksでメモリの使用状況を確認できます。
少々煩雑ですが、詳細なメモリリークの情報が得られます。

init/deallocハック

より簡潔に、大まかにメモリ解放されているかを確認したい場合は、
特定の基底オブジェクトのinit/deallocを利用すると良いでしょう。
ここではcocos2dを例に取ります。
cocos2dのオブジェクトはCCNodeから派生しているため、全てCCNodeinit/deallocを通過することになります。
そこで、以下のようにCCNodeinit/deallocにオブジェクトの使用状態を記録オブジェクトへ通知するようにします。

// CCNode.m
...
-(id) init
{
  if ((self=[super init]) ) {
#ifdef DEBUG
    [AllocateLogger.sharedLogger notifyAllocate:[self class]];
#endif
    ...
  }
  return self;
}
...
// ARCでもdeallocは書くことができ、このような後処理を記述できる
// ただしARCでは`[super dealloc]`を呼ぶ必要はない (コンパイラが挿入する)
-(void) dealloc
{
#ifdef DEBUG
  [AllocateLogger.sharedLogger notifyDeallocate:[self class]];
#endif
  ...
}

AllocateLogger側では、NSStringFromClass関数を利用して得られた文字列をキーとして、
インスタンスの生成/解放をNSMutableDictionaryなどに集めます。
この収集記録をアプリから表示できるデバッグ機能を用意することで、
生存しているCCNode配下のオブジェクトの状態を大まかに確認できます。

// AllocateLoggerの例
// 各クラスの生存しているオブジェクトの数を記録する
@property (nonatomic, retain) NSMutableDictionary *counts;
...
- (void)notifyAllocate:(Class)c
{
  NSString *name = NSStringFromClass(c);
  NSInteger count = [self.counts[name] integerValue];
  self.counts[name] = @(count + 1);
}
- (void)notifyDeallocate:(Class)c
{
  NSString *name = NSStringFromClass(c);
  NSInteger count = [self.counts[name] integerValue];
  self.counts[name] = @(count - 1);
}

MethodSwizzlingを用いれば既存のクラスにも手を加えずにこのハックを適用することができます。
このハックで注意する点として、initdeallocの対応を正確に合わせるために指定イニシャライザを置き換える必要があります。
以下はUIViewの例です。UIViewはInterface Builderの関係でinitWithFrame:initWithCoder:を置き換える必要があります。

#import <objc/runtime.h>
// classのインスタンスメソッドfromSELとtoSELを入れ替える
void swapMethod(Class class, SEL fromSEL, SEL toSEL) {
  Method from = class_getInstanceMethod(class, fromSEL);
  IMP fromImp = method_getImplementation(from);
  const char* fromTypes = method_getTypeEncoding(from);
  Method to = class_getInstanceMethod(class, toSEL);
  IMP toImp = method_getImplementation(to);
  const char* toTypes = method_getTypeEncoding(to);
  class_replaceMethod(class, fromSEL, toImp, toTypes);
  class_replaceMethod(class, toSEL, fromImp, fromTypes);
}
...
@interface UIView (AllocateLogger)
- (id)replacedInitWithFrame:(CGRect)rect;
- (id)replacedInitWithCoder:(NSCoder*)coder;
- (void)replacedDealloc;
@end
@implementation UIView (AllocateLogger)
- (id)replacedInitWithFrame:(CGRect)rect
{
  self = [self replacedInitWithFrame:rect];
  [AllocateLogger.sharedLogger notifyAllocate:[self class]];
  return self;
}
- (id)replacedInitWithCoder:(NSCoder*)coder
{
  self = [self replacedInitWithCoder:coder];
  [AllocateLogger.sharedLogger notifyAllocate:[self class]];
  return self;
}
- (void)replacedDealloc
{
  [AllocateLogger.sharedLogger notifyDeallocate:[self class]];
  [self replacedDealloc];
}
@end
...
// アプリの起動直後に
#ifdef DEBUG
swapMethod([UIView class], @selector(initWithFrame:), @selector(replacedInitWithFrame:));
swapMethod([UIView class], @selector(initWithCoder:), @selector(replacedInitWithCoder:));
swapMethod([UIView class], @selector(dealloc), @selector(replacedDealloc));
#endif

まとめ

参照カウントは規模に比例して管理が難しくなります。
極力疎な依存関係を目指して、所有関係を意識してメモリリークしないアプリを目指しましょう。

今回説明/紹介を省略した事柄

  • __block
  • nonatomic
  • retaincopyの違い
  • unsafe_unretained
  • NSAutoreleasePool

弊社開発の「勇者と1000の魔王」がリリースされました!

そんなメモリリークしないアプリを目指した「勇者と1000の魔王」を、昨日リリースしました!
株式会社オレンジキューブと共同開発したゲームです。ぜひ遊んでみてください!

勇者と1000の魔王[ドットRPG]
カテゴリ:ゲーム>ロールプレイング
価格:無料(一部有料アイテムあり)

技術ブログ記事:
あの頃のドット絵 & ゲームミュージックここに復活! 本格ドットRPG『勇者と1000の魔王』いよいよ本日配信開始!

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!