Twitter: @jijixi_org
Xbox Live: jijixi
-O0だと問題無いのに最適化がオンだと参照カウントがおかしくなるケースがあるようだ。 iOSシミュレータでも実機でも同じように落ちる。Mac OS X だとどうなるかは未確認。 調べるのめんどくさいから既知の問題なのかは知らない。 その辺詳しい人で、もし未知のバグだよってことなら報告しておいてくれると嬉しいかも。 どこに報告すりゃいいのかわからんし。
環境はXcode 4.2.1 + iOS SDK 5で、再現コードは以下のような感じ。
+ (NSArray *)arrayForTestWithObject:(id)element {
NSArray *aResult = nil;
@autoreleasepool {
NSArray *a = [NSArray arrayWithObjects:element, nil];
if (rand() > 0) { // (1)
aResult = [a mutableCopy];
}
}
return aResult;
}
- (NSArray *)arrayForTest {
return [[self class] arrayForTestWithObject:@"hoge"]; // (2)
}
- (void)testARC {
NSArray *a = [self arrayForTest];
NSLog(@"%@", a);
}
適当なところにこれをコピペして最適化オンの状態で (Releaseビルドにすると手っ取り早い) testARC メソッドを呼ぶと (2) のところで落ちる。 シミュレータで Enable Zombie Objects を ON にしていれば、
*** -[__NSArrayM retain]: message sent to deallocated instance 0x6b2c060
ログにこんなのが出力されるはず。
arrayForTestWithObject: 内で aResult の参照数の調整がおかしなことになっている模様。 (1)のところが肝らしく、例えばここの条件を YES (常に真) にしたりすると落ちなくなる。 if を取っ払っても同じように落ちない。 さらに、@autoreleasepool ブロックを取っ払えば問題は起きないので、@autoreleasepool の中で条件によって変数にアサインしたりしなかったりってことがあると危険ぽい。
そんなこんなで半日潰れた今日であった。
ちなみに、上記の例で言うと変数用意しておいてどうのなんてことはせずに、いきなりreturnしてしまえば問題なかったりする。
+ (NSArray *)arrayForTestWithObject:(id)element {
@autoreleasepool {
NSArray *a = [NSArray arrayWithObjects:element, nil];
if (rand() > 0) {
return [a mutableCopy]; // (3)
}
}
return nil;
}
(3)の部分、@autoreleasepool だとこう書けるので楽だし、上記の問題のことを考えればむしろこう書いた方がいいんだろうね。 まあ、今回は元々自分でNSAutoreleasePoolを使ってたコードをコンバートしたものだったから、こういう事態になったんだけど。
以前 iOS アプリの開発でこんなことを書いてハマったことがある。
return self.someView.frame.size.height;
someView プロパティ (UIView のサブクラスの何か) には何も入っていない場合があるんだが、その場合、最初は 0 を返すもののしばらくすると変な値を返し始める。 デバッグ環境だとなかなかその現象が出ないので原因の特定に難儀したんだが、わかってしまえば「ああそうか、なんで気づかなかったんだ」という話。
Objective-C では nil に対するメッセージ (メソッド呼び出し) は何も起こらず nil が返ることになっているので、特にエラーを出す必要が無い場合にはつい nil かどうかのチェックを省略してしまうんだが、上記のような場合にはそれが仇になる。
UIView の frame プロパティの型は CGRect で、これはオブジェクトではなく構造体である。 構造体に関しては C そのものと考えていい。 つまり上記のようなコードは実際には以下のような処理の流れになっている (と思われる)。
UIView *aView = [self someView]; CGRect frame = [aView frame]; return frame.size.height;
aView が nil の場合、frame には nil が代入されるのだろう。 そうだとすれば当然それは CGRect の初期化としては不十分で、その結果、ある程度メモリ領域が汚れてくるとゴミ値が入ってくるので return される値がおかしくなる。 C の経験がある程度あれば当たり前の挙動ではあるが、そうでない場合にはさっぱり意味がわからないかもしれない。 やっぱり Objective-C を使う人は C も必修にした方がいいんじゃないか。
ここだけゴールドが取れてなかったが、ようやく何とかなったのでメモ。
頭の数字は選択肢の位置。 13A の看板の存在に気付かなかったせいで今まで Perfect が取れてなかった。 位置的に見逃しやすいし、無視しても話が進むのが結構罠。
ちなみに警備員を殺さないようにするミッションは、自分が殺さなければいいだけなので邪魔な場合は泥棒たちに始末してもらうと良い。