設計変更
結果から書くとGCDを使った非同期処理で発生していたEXC_BAD_ACCESSは漸く解決することが出来た。
原因はInstrumentでプロファイルした通りGCD、というかメインスレッド(UIスレッド)と別スレッドで動作するメソッドの戻り値の参照カウント。
以前にも書いたが今回問題になった処理だが、簡単にスレッド毎に書くと
[UIThread] ボタンタップ--> HogeAction --> HogeTask.preExecute [Thread-2] | |-> HogeTask.executeInBackground -> 戻り値使用 | [GCDによる待ち合わせ] | [UIThread] | |-> HogeTask.postExecute --> 戻り値使用 × EXC_BAD_ACCESS
こんな感じになるのだが、HogeAction中で呼び出すHogeTask.executeInBackgroundの戻り値を使う時、戻り値は既にAutoRelease Poolにドレインされてしまっているため、HogeTask.postExecuteで戻り値を使用する時には既に解放されており_NSZombieがセットされている、という訳だ。
この戻り値が解放されてしまうのをどうにかして制御してやろうと、__autoreleasing指定をしたり、他の変数に格納してオーナシップを移動したり、挙げ句の果ては「__attribute__((ns_returns_retained))」アノテーションを付けてみたりもしたのだが、根本的な解決には至らなかった。
結局、ARCを使う限りはretain/releaseの制御をこちら側で行うのは不可能だという結論だ。
ARCを廃止してこの処理だけを非ARC下で書くこともできるのだが、なんだか目的と手段をはき違えているような気がしてきたので止めた。
根治方法としてはベタなのだが、元々用意したプロトコルから設計を変更することにした。
新たなBBAsyncTaskProtocol
@protocol BBAsyncTaskProtocol <NSObject> @optional - (void)preExecute:(id)param moreParams:(NSMutableArray*)pramsArray; - (void)executeInBackgound:(id)param moreParams:(NSMutableArray*)pramsArray; - (void)postExecute:(id)param moreParams:(NSArray*)pramsArray; @end
非同期タスクでは戻り値は一切使わないようにする。戻り値が必要な場合はパラメタのpramsArray中に付加して後に使う。NSMutableArrayではなく、専用のクラスやデリゲートを用意するのも良いかもしれない。
戻り値の同期を必要としなくなった実装に変えた後、EXC_BAD_ACCESSは一切発生しなくなった。
BBAsuncTask.mから非同期処理部分を抜粋
static NSString* SEL_PRE_EXECUTE = @"preExecute:moreParams:"; static NSString* SEL_ASYN_EXECUTE = @"executeInBackgound:moreParams:"; static NSString* SEL_POS_EXECUTE = @"postExecute:moreParams:"; 〜 - (void)doExecute:(NSInvocation*)inv execBackGround:(BOOL)aSync { // preExecute 実行 SEL pre = NSSelectorFromString(SEL_PRE_EXECUTE); if ( pre && [[self targetObject] respondsToSelector:pre] ) { [inv setTarget:[self targetObject]]; [inv setSelector:pre]; [inv invoke]; [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]]; } dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_t group = dispatch_group_create(); @try { // executeをGCDで実行 dispatch_group_async(group, queue, ^{ SEL exec = NSSelectorFromString(SEL_ASYN_EXECUTE); [inv setTarget:[self targetObject]]; [inv setSelector:exec]; [inv invoke]; [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]]; }); // execute の終了を待ち合わせ dispatch_group_wait(group, DISPATCH_TIME_FOREVER); //postExecute 実行 SEL post = NSSelectorFromString(SEL_POS_EXECUTE); if (post && [[inv target] respondsToSelector:post]) { [inv setTarget:[self targetObject]]; [inv setSelector:post]; [inv invoke]; [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantPast]]; } } @finally { dispatch_release(group); } }
戻り値を見なくて良くなったのでシンプルだ。
今回のバグ発生〜解決までの一連の流れではDelphiを使っていた頃を思い出した。あの頃はXcodeもInstrumentsも無く、今よりも大変だった記憶がある。それにしても、JavaやC#を使っていると当たり前になっているGCのありがたさが身に染みる。
ARCを使うことによってretain/releaseを意識してコーディングする必要は無くなったが、それは逆に自分でそれらを制御できないということであり、ARCによってどのようなコードが生成されているかを理解した上でそれに沿った設計、実装をしないと駄目なのだろう、というのが今回の教訓だ。