Objective-Cの宣言プロパティにはまる

iPhoneの開発において、Objective-C 2.0で導入された宣言プロパティ(declared property)にはまったので覚え書。プロパティ経由でアクセスする度にリファレンスカウンタが増加してしまい、結果としてリークするという状態が発生した。単に参照しているだけなのだから、インスタンス側の状態は何も変わらないハズだし、変わって欲しくない。それなのにリファレンスカウンタが増えてしまうとは、一体どういうことなのか?

試しにこんなテストコードを書いて、いろいろ調べてみた。

NSLog(@"(1) %d", [self.selectedImage retainCount]);
NSLog(@"(2) %d", [self.selectedImage retainCount]);
NSLog(@"(3) %d", [self.selectedImage retainCount]);

プロパティの宣言にnonatomicオプションが指定されている場合、リファレンスカウンタは変わらず予想通りの結果が返ってきた。これは問題ない。

@property(nonatomic,retain) UIImage *selectedImage;
2010-12-10 11:20:27.915 MyApp[15364:207] (1) 2
2010-12-10 11:20:27.917 MyApp[15364:207] (2) 2
2010-12-10 11:20:27.918 MyApp[15364:207] (3) 2

nonatomicオプションが指定されていない場合、リファレンスカウンタは増え続けるという結果が返ってきた。これが今回の問題に該当する。

@property(retain) UIImage *selectedImage;
2010-12-10 11:54:14.181 MyApp[16320:207] (1) 3
2010-12-10 11:54:14.182 MyApp[16320:207] (2) 4
2010-12-10 11:54:14.183 MyApp[16320:207] (3) 5

Objective-Cの教科書である「詳解Objective-C 2.0」を改めて読み返してみたら、nonatomicを指定しない場合の処理に関して、このように説明されていた。(p.296)

(前略)ゲッタは@synchronizedが追加されただけではなく、わざわざretainとautoreleaseを適用しています。このようにするのは、ゲッタから返された値を受け取った側が処理をしている間に、別のスレッドが元のプロパティを解放してしまう可能性を考慮しているためです。

- (TYPE)name{
    @synchronized(self)
    {
        return [[name retain] autorelease];
    }
}

なるほど、確かにその通りの動作結果だ。だから参照しているだけなのに、リファレンスカウンタは増え続けるのだ。スレッド内でループを回して何度もアクセスするような処理では、あっという間にリファレンスカウンタが増えてしまうので注意が必要となるわけだ。

もちろん「参照するだけだから、勝手にリファレンスカウンタを増やして欲しくない」というニーズもあり得るわけで、その時にはプロパティを使うのではなく、自前で@synchronized付きのアクセッサを書けば良い。

同様の疑問をもつ人は多いのか、Stack Overflowにもこんな質問が載っていた。

What do atomic and nonatomic mean in property declarations?

ios - What's the difference between the atomic and nonatomic attributes? - Stack Overflow

言語仕様やAPIの理解はなかなか難しい。バグに出会って初めてその仕様が分かることも珍しくない。今回のケースも、バグの発生をきっかけとしてようやく理解できた気がしている。

詳解 Objective-C 2.0

詳解 Objective-C 2.0



関連