Blocksの罠


プログラムのコーディングについて書くのはプロである以上責任もあるのであんまり書かないようにしてるんですよね。
でもちょっと気をつけなきゃいけない内容があったので書いてみたいと思います。

以下のコードを見て下さい。

@interface MyViewController () {
	NSInteger myInteger ;
}

@property (nonatomic, retain) NSMutableArray* myArray ;

@end

@implementation MyViewController

- (void)awakeFromNib {
	[super awakeFromNib] ;
	
	self.myArray = [NSMutableArray array] ;

	NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter] ;
	
	NSLog(@"%d", self.retainCount) ;
	
	[defaultCenter addObserverForName:@"MyNotification"
							   object:nil
								queue:nil
						   usingBlock:^(NSNotification *note) {
		myInteger = 0 ;
		[_myArray addObject:@"aaaa"] ;
	}] ;
	
	NSLog(@"%d", self.retainCount) ;
}

- (void)dealloc {
	NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter] ;

	[defaultCenter removeObserver:self] ;

	self.myArray = nil ;

	[super dealloc] ;
}

// 以下省略
@end

一見なんてことないViewControllerの初期化と終了のコードですが、これdeallocが呼ばれません。
2箇所のNSLogの出力を見るとselfの参照カウンターが1つ上がっています。

このソース、結論から言うと「循環参照」と言う状態になっています。
なぜaddObserverForNameの前後でカウンターが上がるかの理由は後で説明するとして、addObserverForNameが理由でselfの参照カウンターが上がってるとするならばおそらくremoveObserverで減るでしょう。しかし、addObserverForNameで上がっているために正しいタイミングでdeallocが呼ばれずremoveObserverも呼ばれない。

つまりこのViewControllerは画面から消えてもメモリ上に消されずに残ってしまいます。

この現象はBlocksの罠(とでもいいましょうか)なのです。

Blocksはコピーされる時、内部のオブジェクトの参照カウンターをすべて1つ上げます。

え?でもこの中でselfは使ってないじゃないか?

と言いたくなるかも知れませんが、暗黙に使われているのです。

myInteger = 0 ;
[_myArray addObject:@"aaaa"] ;

これは暗黙に

self->myInteger = 0 ;
[[self myArray] addObject:@"aaaa"] ;

と言うコードが呼ばれており、バッチリselfを使っています。
なのでaddObserverForNameの前後でBlockがコピーされる際に参照カウンターが1つ上がるのです。

これは結構な罠でしょ?だって呼ばれると期待してるはずのdeallocが呼ばれないのになかなか気が付きにくい。
(Blockで参照カウンターが上がることを知っていてもです。)

ではこれ、どうやって回避しましょうか?
ちゃんと方法があり、こう書きます。

	__block typeof(self) blockSelf = self ;
	
	[defaultCenter addObserverForName:@"MyNotification"
							   object:nil
								queue:nil
						   usingBlock:^(NSNotification *note) {
		blockSelf->myInteger = 0 ;
		[blockSelf.myArray addObject:@"aaaa"] ;
	}] ;

まず参照カウンターが上がらないBlock変数でselfを定義します。ちょっと変わった書き方ですが、typeofで変数宣言できます。これでself型の変数を定義できます。
宣言したBlock変数を使えばselfの参照を避けることができます。メンバ変数へのアクセスも「->演算子」でできます。

もちろん関数内で一時的に終わるBlock(UIViewのanimationとか)は気にしなくても大丈夫です。あくまでこれは相互参照の可能性がある部分でのことです。NSNotificationCenterなんかはいい例なのではないでしょうか?(もちろんviewWillAppearとviewWillDisappearで行うパターンもありますけど)

何にせよ、Blocksプログラミングを取り入れた時点で最終的にはdeallocにNSLogを書いてちゃんと呼び出したViewControllerが消えているかを確認するのがいいと思います。ちなみにこれは構文上間違ってないのでAnalyzeでも検出されません。

もしこの記事を読んで何か間違っていることや別のアプローチがあればコメントなりメールなりいただけたら幸いです。

しかし、いろんな言語をやってますが、やっぱりC++が最強ですね。
あの言語で表現できない世界はないと思ってます。

Objective-Cはよく言えば柔軟、悪く言えば緩い。
まぁ、嫌いじゃないですけどねw