こんにちは。
iOS/Objective-Cによるアプリ開発では参照カウントによるメモリ管理が必要になります。
今回はiOS/Objective-Cでのメモリ管理の基礎からメモリリークの検知まで解説したいと思います。
参照カウント
Objective-Cの全てのオブジェクトはNSObjectを継承しています。
全てのオブジェクトはこのNSObjectのメソッドを通じて参照カウントを扱います。
この参照カウントが、オブジェクトがいくつのオブジェクトから強参照されているかを表し、
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]; ...
autorelease
はrelease
と同様に参照カウントを-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問わず参照カウントによるメモリ管理には循環参照の問題が発生します。
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
Xcode上で、Product > Profile > Leaksでメモリの使用状況を確認できます。
少々煩雑ですが、詳細なメモリリークの情報が得られます。
init/deallocハック
より簡潔に、大まかにメモリ解放されているかを確認したい場合は、
特定の基底オブジェクトのinit/dealloc
を利用すると良いでしょう。
ここではcocos2dを例に取ります。
cocos2dのオブジェクトはCCNode
から派生しているため、全てCCNode
のinit/dealloc
を通過することになります。
そこで、以下のようにCCNode
のinit/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を用いれば既存のクラスにも手を加えずにこのハックを適用することができます。
このハックで注意する点として、init
とdealloc
の対応を正確に合わせるために指定イニシャライザを置き換える必要があります。
以下は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
retain
とcopy
の違いunsafe_unretained
NSAutoreleasePool
弊社開発の「勇者と1000の魔王」がリリースされました!
そんなメモリリークしないアプリを目指した「勇者と1000の魔王」を、昨日リリースしました!
株式会社オレンジキューブと共同開発したゲームです。ぜひ遊んでみてください!
勇者と1000の魔王[ドットRPG]
カテゴリ:ゲーム>ロールプレイング
価格:無料(一部有料アイテムあり)
技術ブログ記事:
あの頃のドット絵 & ゲームミュージックここに復活! 本格ドットRPG『勇者と1000の魔王』いよいよ本日配信開始!