Powered By Blogger

2012年3月5日月曜日

iOS,ARC:weakポインタを使う場合の注意

うっかり循環強参照(循環参照では無い)にしてしまうとオブジェクトが解放されずリークとなりますので、
弱い参照を使って循環強参照を防ぎます。

ただし、weak修飾子は iOS5以降でしか利用できないので、iOS4ではunsafe_unretainedをweak修飾子の代わりとします。
ところがunsafe_unretainedは挙動が素直と言うかweakほど気が利かないので、iOS5/ARCに慣れてしまうとiOS4/ARCで痛い目を見ると言う話です。

詳細はこの元ネタ参照。
元ネタはObj-cお馴染みのデリゲートパターンでは循環参照になるので、これが循環強参照とならない様にdelegateの属性を弱参照とするのだが、うっかりiOS4/ARCで痛い目を見たという話です。

weak属性が気が利いてるというのは、例えばこの本によると、
コンパイラは、__weak修飾子付きの変数をアクセスする場合、必ずautoreleasepoolに登録してからアクセスする様なコードを生成するらしい。
//
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);

//
id __weak obj1 = obj0;
id __autoreleasing tmp = obj1;
NSLog(@"class=%@", [tmp class]);
と等価なコードに生成するそうです。

わざわざこんなコードを生成する理由は元ネタを読むと分かるので説明は省略します。

●対策
元ネタにも対策が書いてあるのですが、問題が発覚してからの対処の様な感じでちょっと分かりにくいと感じるかもしれません。
ここは敢えて別の方法を考えてみましょう。
要はレシーバが弱参照の場合は__weakの時と同じ挙動をする様にすれば良いのです。

こんなマクロ関数(WeakSendMessage)を作ります。
//
//1:弱参照にweakを使う。0:弱参照にunsafe_unretainedを使う
#define kUsingWeak 0

#if kUsingWeak
  #define unretained   weak
  #define __unretained __weak
  #define WeakSendMessage(_r, _s, _a1) \
         [(_r) _s(_a1)];
#else
  #define unretained   unsafe_unretained
  #define __unretained __unsafe_unretained
  #define WeakSendMessage(_r, _s, _a1) \
         __autoreleasing NSObject *tmp = (_r); \
         [tmp performSelector:@selector(_s) withObject:(_a1)];
#endif
いったん__autoreleasing属性の一時ポインタに置き換えてます。

この様にレシーバが弱参照のメッセージングの箇所を
//
  //self.delegateは弱参照
  [self.delegate callFromClassB:self];
こう変えます。
//
  //self.delegateは弱参照
  WeakSendMessage(self.delegate, callFromClassB:, self);

これでiOS4でもiOS5でも同じ挙動になるはず。

と言うか、循環参照するならiOS4は切り捨てろ。
iOS4を切り捨てられないなら循環参照はするな、というのが正しいのかもしれません。

因みにWeakSendMessageの一時ポインタの属性__autoreleasingを__strongに変えても問題無く動作します。
//
  #define WeakSendMessage(_r, _s, _a1) \
         __strong NSObject *tmp = (_r); \
         [tmp performSelector:@selector(_s) withObject:(_a1)];
この方法はC++0x(あるいはboost)のスマートポインタweak_ptrと考え方がだいたい似ていると思われます。
弱参照weak_ptrからは直接参照先にアクセス出来なくて、メンバ関数lock()で強参照を取り出す必要があります。
こんな感じ
//弱参照から強参照を取り出している
weakPtr.lock()->doIt();
//weakPtr->doIt(); //これはコンパイルエラー
weak_ptrは使った事が無いので、確信はありませんが…

0 件のコメント:

コメントを投稿