NSProxyはARCと相性が悪い
以前にバックグラウンドスレッドでタスクを動かすためにNSProxyのフォワーディングの機能を利用する例を書いたが、その後ARCならでは問題が発生して頓挫している。
問題を簡単にするために、NSProxyを単純に継承したクラスと、それを利用するクラスを書いてみた。
TestObj.m
#import <SenTestingKit/SenTestingKit.h> @interface ProxyObject : NSProxy { __weak id targetObject; } @end @implementation ProxyObject - (id)initWithObject:(id)realObject { targetObject = realObject; return self; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [targetObject methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setTarget:targetObject]; [anInvocation invoke]; return; }@end @interface TestObj : NSObject { ProxyObject* proxy; } @end @implementation TestObj - (id)init { self = [super init]; return (TestObj*)[[ProxyObject alloc] initWithObject:self]; } - (void)log { NSLog(@"target was released may be."); } @end
TestObjはそのイニシャライザで実際にはProxyObjectを生成して返している。ProxyObjectはTestObjのふりをして全てのメッセージを捕捉する。これにより様々な付加処理をTestObjに対して実行できるという訳だ。
しかし、以下のテストを実行して見るとこの方法が上手く動かないことがすぐ判るだろう。
TestARCProxyIssue.m
@interface TestARCProxyIssue : SenTestCase @end @implementation TestARCProxyIssue - (void)testImmediatelyZombie { [[[TestObj alloc] init] log]; } @end
このテストを実行するとlogメッセージが送れずにdoesNotRecognizeSelectorになってしまう。
最初に書いたが、TestObjのイニシャライザで返すインスタンスはTestObj(に見せかけた)ProxyObjectである。本来戻すはずだった本当のTestObjクラスのインスタンスは所有者がいないため、アロケート直後だがARCによってイニシャライザ終了後にに解放されてしまうのである。
その後実際メッセージを受け取るのはProxyObjectだが、処理は元々のTestObjがあることを期待して再びメッセージを戻すが、その時には時既に遅しTestObjは既にゾンビなのである。
ProxyObjectは一応TestObjの参照を持つのだが、TestObj自体がProxyObjectを強参照で所有しているため、同じ参照強度にすると循環参照が発生するために__weakつまり弱参照でしか所持できないので、解放を抑制することができないのだ。
以下のようにTestObjのインスタンスが解放されないように、自身のインスタンスを保持することもできるが、
@interface TestObj : NSObject { ProxyObject* proxy; } @property id selfReference; @end @implementation TestObj @synthesize selfReference; - (id)init { self = [super init]; selfReference = self; return (TestObj*)[[ProxyObject alloc] initWithObject:self]; }
このようしてしてしまうと、今度はTestObjを解放する手立てが無くなってしまう。同じようにProxyObject側で解放されないように弱参照を強参照に変えることもできるが、そうすると循環参照の出来上がりである。
明示的にdeallocを呼び出すことができればなんとかなるのだが、ARC下ではそれが出来ないので普通のコーディングではちょっとお手上げだ。