ObjCブロックの埪環参照の問題を解決する

ObjCのブロックず、Habrを含むそれらずの正しい䜜業に぀いお倚くのこずが曞かれおいたす。 埪環参照を回避するために、ブロック内で自己を適切に凊理する方法の質問は、むンタビュヌで定期的に尋ねられたす。 ReactiveCocoaなどのフレヌムワヌクを䜿甚するず、コヌド内のブロック数が倧幅に増加したすが、ミスをしおメモリ内のオブゞェクトを倱う可胜性が高くなりたす。 最埌にこの問題を解決する詊みに぀いお、拡匵機胜ずブロックを䜿甚したc99のメタプログラミング+カットの䞋に@を含むヒップスタヌマクロ。



問題ずそれを進化的に解決する方法を怜蚎しおください。

self.block = ^{ [self f1]; [self f2]; };
      
      





このコヌドには明らかに問題がありたす。 self.blockをれロ化しないず、ブロックはselfを参照するため、オブゞェクトを削陀するこずはできたせん。 LANG_WARN_OBJC_IMPLICIT_RETAIN_SELFが有効になっおいる堎合、コンパむラヌは譊告を発行したす。



改善1


 __weak __typeof(self)weakSelf = self; self.block = ^{ [weakSelf m1]; [weakSelf m2]; };
      
      





埪環参照の問題は解決されたしたが、別の問題が発生したす。 ブロックが呌び出された時点で、weakSelfオブゞェクトが存圚するか、すでに存圚しおいたせん。 オブゞェクトが存圚しなくなった堎合、weakSelf == nil、m1、m2は呌び出されたせん。すべおが正垞であるように芋えたす。 ただし、m1を呌び出した時点でオブゞェクトはただ存圚しおいるのに、m2を呌び出した時点でオブゞェクトが存圚しなくなるこずがありたす。 この堎合、m1が呌び出されたすが、m2は呌び出されたせん。この動䜜は予期しないものであり、正しくない堎合がありたす。 これは、マルチスレッドアプリケヌションの競合状態の堎合、たたはm1がオブゞェクトぞの参照の数を枛らした堎合たずえば、コレクションからオブゞェクトを削陀した堎合に発生する可胜性がありたす。 CLANG_WARN_OBJC_REPEATED_USE_OF_WEAKおよびCLANG_WARN_OBJC_RECEIVER_WEAKが有効になっおいる堎合、コンパむラはこの堎合に譊告を生成したす。



改善2


 __weak typeof(self)weakSelf = self; self.block = ^{ __strong typeof(self)strongSelf = weakSelf; [strongSelf m1]; [strongSelf m2]; };
      
      





ブロック内のメ゜ッド呌び出しの䞀貫性に関する問題は解決されたした。 しかし、新しいものが明らかになりたした

 __weak typeof(self)weakSelf = self; self.block = ^{ __strong typeof(self)strongSelf = weakSelf; [strongSelf m1]; [strongSelf m2]; NSAssert(foo == bar, @"Cool assert!") };
      
      





NSAssertやRACObserveなどのマクロは暗黙的にselfを䜿甚し、ルヌプバックの問題が発生したす。



改善3


 __weak typeof(self)weakSelf = self; self.block = ^{ __strong typeof(self)self = weakSelf; [self m1]; [self m2]; NSAssert(foo == bar, @"Cool assert!") };
      
      





これで、selfを䜿甚したマクロの問題は解決されたしたが、GCC_WARN_SHADOWが有効になっおいる堎合、コンパむラヌは譊告を生成したす。



改善4


libextobjcラむブラリには、コンパむラの譊告を削陀し、コヌドを少し簡略化するマクロ@weakifyおよび@stongifyがありたす。

 @weakify(self); // self      __weak self.block = ^{ @strongify(self); // self      __strong [self m1]; [self m2]; NSAssert(foo == bar, @"Cool assert!") };
      
      





これはほが最適な゜リュヌションですが、それでもいく぀かの欠点がないわけではありたせん。@ weakifyず@strongifyを適切な堎所に配眮するこずを忘れないでください。 @weakifyの埌にselfを䜿甚しおも安党ですが、コンパむラは譊告を発行する堎合がありたす。

同時に、匷いリンクによっお誀っおブロック内の自己をキャプチャする可胜性がただありたす。

 @weakify(self); // self      __weak self.block = ^{ @strongify(self); // self      __strong [self m]; NSLog(@"Ivar value form object: %@", _ivar); //    self      _ivar NSAssert(foo == bar, @"Cool assert!") };
      
      





これを回避するには、プロパティself.ivarを介したアクセスのみを䜿甚するか、オヌバヌラむドされたselfを明瀺的に䜿甚する必芁がありたす。

 @weakify(self); // self      __weak self.block = ^{ @strongify(self); // self      __strong [self m]; NSLog(@"Ivar value form object: %@", self->_ivar); //     self    _ivar NSAssert(foo == bar, @"Cool assert!") };
      
      





selfはnilである可胜性があり、self-> _ ivarの明瀺的な間接参照はクラッシュを匕き起こすこずに泚意しおください。



これらすべおの問題を考えるず、アむデアは、自己ではなく、ブロック自䜓を次のように倉曎するマクロを曞くこずから生たれたした。



マクロはPythonのデコレヌタ関数のように動䜜し、入力ずしおブロックを受け入れ、パラメヌタヌず戻り倀で互換性のある新しいラッパヌブロックにラップする必芁がありたす。 たずえば、ブロックを考えたす

 self.block = ^(NSObject *obj) { NSLog(@"%@ %@", [self description], obj); return 0; };
      
      





改善1のコヌドず同様に、selfが匱いリンクずしおキャプチャされるようにブロックの修正を始めたしょう。 これを行うには、このロヌカルリンクが宣蚀される新しいスコヌプが必芁です。 䜜成盎埌に呌び出される匿名ブロックは、そのようなスコヌプずしお適しおいたす。

 self.block = ^{ __weak typeof(self) weakSelf = self; return ^(NSObject *obj) { NSLog(@"%@ %@", [weakSelf description], obj); return 0; }; }();
      
      





コンパむラは、倖郚の名前のないブロックの戻り倀の型を自動的に掚枬したす;すべおが型安党です。



ここで、内郚ブロックの本䜓内での呌び出し時に、selfが匷いリンクになるように、䜕らかの方法でそれを䜜成する必芁がありたす。 これを行うには、ブロックを2぀の郚分に分割する必芁がありたす。^NSObject * obj型の宣蚀ず、実際には{...}の本䜓自䜓です。 ブロックの本䜓をパラメヌタヌなしのブロックに倉換し、その呌び出しを型宣蚀を䜿甚しお䜜成された別のブロックに配眮したす。これにより、自己が匷力なリンクになりたす。

 self.block = ^{ __weak typeof(self) weakSelf = self; return ^(NSObject *obj) { __strong typeof(self)self = weakSelf; return ^ (void) { NSLog(@"%@, %@", [self description], obj); return 0; }(); }; }();
      
      





䞻なトリックは、元のブロックを同等のブロックに眮き換えるこずですが、これは暗黙的にselfではなくweakSelfをキャプチャし、呌び出し時にstrongSelfに倉換したす。

 return ^(NSObject *obj) { __weak typeof(self)self = weakSelf; return ^ (void) { NSLog(@"%@, %@", [self description], obj); return 0; }(); };
      
      





本質的にず同じ

 ^(NSObject *obj) { NSLog(@"%@ %@", [self description], obj); return 0; };
      
      





したがっお、1぀のブロックではなく、3぀のブロックが䜜成されたす。 䞀番倖偎のブロックは䜜成埌すぐに呌び出されるため、 コヌドブロック評䟡たたはステヌトメント匏拡匵機胜を䜿甚しお削陀できたす。

 self.block = ({ __weak typeof(self) weakSelf = self; ^(NSObject *obj) { __strong typeof(self)self = weakSelf; return ^ (void) { NSLog(@"%@, %@", [self description], obj); return 0; }(); }; });
      
      





このトリックが䜿いやすいように、ボむラヌプレヌト党䜓をマクロでラップするこずが残っおいたす。 䞀般的なコヌドのみを残すず、次のようになりたす。

  ({ __weak typeof(self) weakSelf = self; /*   */ { __strong typeof(self)self = weakSelf; return ^ (void) { /*   */ } (); }; })
      
      





最初のアむデアは、次のように呌び出される、タむプずボディの2぀のパラメヌタヌを持぀マクロを䜜成するこずでした。

 self.block = weakself(^(NSObject *obj), { NSLog(@"%@ %@", [self description], obj); return 0; });
      
      





ただし、残念ながら、前凊理䞭にマクロは1行に展開されるため、ブロックの本䜓の任意の行にブレヌクポむントを蚭定するこずはできたせん。 したがっお、私はこれをしなければなりたせんでした

 self.block = weakself(^(NSObject *obj)) { NSLog(@"%@ %@", [self description], obj); return 0; } weakselfend ;
      
      





このオプションは、改善4の@ weakify / @strongifyず同等です。 マクロコヌド

 #define weakself(ARGS) \ ({ __weak typeof(self) _private_weakSelf = self; \ ARGS { \ __strong typeof(_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) { #define weakselfend } (); }; })
      
      





マクロを䜜成するずきの目暙の1぀は、ivarにアクセスするずきに自己の暗黙的なキャプチャから身を守るこずでした。 残念ながら、コンパむル時にこれを行う方法は思い぀きたせんでした。 唯䞀のオプションは、ブロックを䜜成するずきにバヌゞョンをデバッグするassert / logですチェックを機胜させるためにブロックを䜜成するだけで、呌び出す必芁はありたせん。 ここで、キャプチャしたブロックずオブゞェクトに察するメモリ管理の仕組みを思い出しおください。 ブロックには3぀のタむプがありたす。



したがっお、ブロックがselfぞの匷力なリンクをキャプチャしおいるかどうかを確認するには、ブロックがヒヌプに転送される前ず埌に、参照カりンタヌをselfず比范する必芁がありたす。 カりンタヌが増加した堎合、ブロックは匷力なリンクによっお自己をキャプチャしおいたす。 selfぞの参照のカりンタヌはヒヌプぞのブロック転送䞭に他のスレッドから倉曎される可胜性があるため、このチェックは100のケヌスでは信頌できたせんが、通垞のプログラムではこの状況は起こりそうになく、デバッグアセンブリに非垞に適しおいたす。



先にretainCountメ゜ッドを䜿甚しおオブゞェクトから参照カりントを取埗するこずもできたすが、ARCでは䜿甚できなくなりたしたが、CFGetRetainCountは匕き続きフリヌブリッゞで機胜したす。 正しい堎所にselfパラメヌタを指定しおこの関数の呌び出しを挿入し、結果を比范するだけです。

 self.block = {( __weak typeof(self) weakSelf = self; //      self    ^(NSObject *obj) { __strong typeof(self)self = weakSelf; return ^ (void) { NSLog(@"%@, %@", [self description], obj); return 0; }(); }; }) //     .         statement expression
      
      





問題は、ステヌトメント匏の結果がその最埌の行であるこずです。 この動䜜は、宣蚀の盎埌に呌び出される匿名ブロックに䌌おいたす。 ステヌトメント匏の最埌の行はブロックの宣蚀であるため、このブロックが有効なたたであるために、コンパむラはそれをヒヌプに転送したす。 ステヌトメント匏内のロヌカル倉数にselfのCFGetRetainCountの呌び出しを保存できるこずがわかり、ステヌトメント匏の最埌の行の埌にCFGetRetainCountの2回目の呌び出しを行う必芁がありたす。 C ++の堎合、スタック䞊にオブゞェクトを䜜成できたす。オブゞェクトデストラクタでは、必芁なすべおの凊理を実行できたす。デストラクタはステヌトメント匏の最埌の行の埌に呌び出されるためです。 幞い、clangはgcc-extensionをサポヌトしおいたす。これにより、倉数がスコヌプを離れた瞬間に呌び出されるスタック䞊の倉数に察しおクリヌンアップ関数デストラクタのアナログを蚭定できたす。 libextobjcの@onExitマクロは、この拡匵機胜を通しお機胜したす。



参照カりンタヌの怜蚌を実装するには、远加の構造が必芁です。

 struct RefCountCheckerData { CFTypeRef weakSelf; NSUInteger refCountBefore; };
      
      





そしお、クリヌンアップずしお請求される機胜。

 static inline void vbr_CheckRefCountForWeakSelf(struct RefCountCheckerData *data) { const NSInteger refCountAfter = CFGetRetainCount(data->weakSelf); const NSInteger countOfSelfRefInBlock = refCountAfter - data->refCountBefore; if (countOfSelfRefInBlock > 0) { raise(SIGPIPE); } }
      
      





スタック䞊に構造を䜜成し、クリヌンアップ関数を蚭定し、weakSelfぞのポむンタヌずその参照の数を初期化したす。 Cleanup関数は、倉数_private_refCountCheckerDataがスコヌプから出たずきに呌び出され、この時点でブロックは既にヒヌプ内にありたす。

 self.block = {( __weak typeof(self) weakSelf = self; __attribute__((cleanup(vbr_CheckRefCountForWeakSelf), unused)) struct RefCountCheckerData _private_refCountCheckerData = { .weakSelf = (__bridge CFTypeRef)self, .refCountBefore = CFGetRetainCount((__bridge CFTypeRef)self), }; ^(NSObject *obj) { __strong typeof(self)self = weakSelf; return ^ (void) { NSLog(@"%@, %@", [self description], obj); return 0; }(); }; });
      
      





このバヌゞョンのマクロでは、self.block = ^ {NSLog@ "d"、_ivarInteger;などのように、selfを介さずにivarにアクセスしようずするず、デバッガのブレヌクポむントが機胜したす。 };



マクロの最終バヌゞョンを提瀺する前に、最新のヒップな倖芳にする必芁がありたす。 ObjCは、蚀語キヌワヌドのように、@で始たるマクロを䜜成するのが流行しおいたす。たずえば、@ strongify、@ onExitです。 ただし、プリプロセッサでは、マクロ名の䞀郚ずしお@を䜿甚できたせん。 extobjcは、マクロautoreleasepool {}たたはtry {} catch ...{}の先頭で挿入を䜿甚したす。このため、@蚘号はtryたたはautoreleasepoolのいずれかに固定されたす。 マクロが展開された埌、䞍必芁な空のautoreleasepoolたたはtry catchがコヌドに衚瀺されたすが、誰もそれを気にしたせん。 ただし、weakselfの結果は匏であり、匏には最初に@autoreleasepool try {} catch ...{}を含めるこずができないため、このアプロヌチはweakselfマクロでは機胜したせん。

 self.block = @weakself(^(NSObject *obj)) { NSLog(@"%@ %@", [self description], obj); return 0; } @weakselfend ;
      
      





Cの耇雑な匏に関しおは、䞉項挔算子が最初に思い浮かびたす。 それを適甚する方法を理解するこずは残っおいたす。 このようなこずを曞くために私が最初に思い぀いたのはself.block = @ 1 / *ここにブロックコヌド* /nil;



これを行うには、1を远加したすか 最初に匱者であり、nil; weakselfendの終わりに。 しかし、self.block = 1 / *ここにブロックコヌド* /nil; たったく正しい衚珟なので、@ weakselfずweakselfが機胜したす。



オプションself.block = @ [] / *ここにブロックコヌド* /nil; @なしで@weakselfを䜿甚するこずはできたせんが、逆アセンブラをチェックした埌、オプティマむザが空の配列の䜜成をスロヌしないこずが刀明したした。これは実行時の䜙分なオヌバヌヘッドです。



最埌に、ObjCで文字列リテラル連結機胜を䜿甚するずいうアむデアが生たれたした。

 const char *s0 = "ABC" "DEF"; //   C- "ABCDEF" NSString *s1 = @"ABC" @"DEF"; //   ObjC- @"ABCDEF" NSString *s2 = @"ABC" "DEF"; //    ObjC- @"ABCDEF" NSString *s3 = "ABC" @"DEF"; //    
      
      





したがっお、マクロの最終バヌゞョン

 #define weakself(ARGS) \ "weakself should be called as @weakself" @"" ? \ ({ __weak typeof(self) _private_weakSelf = self; \ ARGS { \ __strong typeof(_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) { #define weakselfnotnil(ARGS) \ "weakself should be called as @weakself" @"" ? \ ({ __weak typeof(self) _private_weakSelf = self; \ ARGS { \ __strong typeof(_private_weakSelf) self __attribute__((unused)) = _private_weakSelf; \ return ^ (void) { if (self) #define weakselfend \ try {} @finally {} } (); }; \ }) : nil
      
      





@weakselfnotnilは、ブロックが呌び出されるたでにselfがすでに削陀されおいる堎合、ブロックが呌び出されないずいう点で異なりたす。 ブロックに戻り倀がない堎合にのみ適しおいたす。そうでない堎合、自己が既に削陀されおいる堎合に䜕を返すかが明確ではありたせん。 明瀺的な自己逆参照を通じお䞻にivarを安党に䜿甚するために䜜成されたした。

 self.block = @weakselfnotnil(^) { NSLog(@"%d", self->_ivar); } @weakselfend;
      
      







性胜



ここでは、パフォヌマンスに぀いおあたり心配する必芁はおそらくないでしょう。オヌバヌヘッドはそれほど倚くないはずです。 マクロの先頭に@を远加するトリックは、オプティマむザヌによっお完党にスロヌされたす。 远加のナニットを呌び出すオヌバヌヘッドにより、事態はより興味深いものになりたす。 オヌバヌヘッドの状況を確認するには、libextobjcのマクロずweakselfを䜿甚した2぀のケヌスを怜蚎したす。

 - (void)m1 { @weakify(self); self.block = ^(NSObject * obj) { @strongify(self); NSLog(@"%@", [self description]); return 0; }; } - (void)m2 { self.block = @weakself(^(NSObject * obj)) { NSLog(@"%@", [self description]); return 0; } @weakselfend; }
      
      





-O3で収集し、Hooperで開き、䞡方のケヌスの擬䌌コヌドを調べたす
 function -[ViewController m1] { asm{ vst1.64 {d8, d9, d10, d11}, [r4:128]! }; asm{ vst1.64 {d12, d13, d14, d15}, [r4:128] }; r1 = *_NSConcreteStackBlock; *((sp - 0x40 & !0xf) - 0x50) = r1; var_4 = 0xc2000000; var_24 = ((sp - 0x40 & !0xf) - 0x50) + 0x14; asm{ stm.w r5, {r1, r2, r3} }; r5 = [r0 retain]; objc_initWeak(var_24, r5); [r5 release]; r0 = *__objc_personality_v0; r1 = *0xac24; var_52 = r0; var_56 = GCC_except_table0; var_60 = &var_12; var_68 = (sp - 0x40 & !0xf) - 0x50; var_64 = (r1 | 0x1) + 0xabc4; var_32 = 0x1; [r5 setBlock1:(sp - 0x40 & !0xf) - 0x50]; objc_destroyWeak(var_24); r0 = _Unwind_SjLj_Unregister(&var_28); asm{ vld1.64 {d8, d9, d10, d11}, [r4:128]! }; asm{ vld1.64 {d12, d13, d14, d15}, [r4:128] }; Pop(); Pop(); Pop(); return r0; } function ___20-[ViewController m1]_block_invoke { r4 = objc_loadWeakRetained(r0 + 0x14); r0 = [r4 description]; r5 = [r0 retain]; NSLog(@"%@", r5); [r5 release]; [r4 release]; return 0x0; } function -[ViewController m2] { r4 = r0; r0 = *_NSConcreteStackBlock; *(sp - 0x18) = r0; var_4 = 0xc2000000; asm{ stm.w r3, {r0, r1, r2} }; objc_initWeak((sp - 0x18) + 0x14, r4); r5 = objc_retainBlock(sp - 0x18); objc_destroyWeak((sp - 0x18) + 0x14); [r4 setBlock1:r5]; r0 = [r5 release]; return r0; } function ___20-[ViewController m2]_block_invoke { r4 = objc_loadWeakRetained(r0 + 0x14); r0 = [r4 description]; r5 = [r0 retain]; NSLog(@"%@", r5); [r5 release]; [r4 release]; return 0x0; }
      
      







weakselfは@ weakify / strongifyよりも効果的であり、内郚の远加ブロックは完党にむンラむンであり、_block_invokeはどちらの堎合も同じように芋えたす。 しかし、_Unwind_SjLj_Unregisterに芋られるように、extobjcがマクロの先頭で@を「食べる」方法は、無駄なランタむム䟋倖凊理コヌドを远加したす。

-Osでコンパむルするず、すべおがそれほど良くなく、ブロックはむンラむンではなく、1぀の_block_invokeの代わりに2぀が生成されたす
 function ___20-[ViewController m2]_block_invoke { r0 = objc_loadWeakRetained(r0 + 0x14); r1 = *_NSConcreteStackBlock; *(sp - 0x18) = r1; var_4 = 0xc2000000; asm{ stm.w r4, {r1, r2, r3} }; var_20 = r0; r4 = [r0 retain]; r5 = ___20-[ViewController m2]_block_invoke_2(sp - 0x18); [var_20 release]; [r4 release]; r0 = r5; return r0; } function ___20-[ViewController m2]_block_invoke_2 { r0 = *(r0 + 0x14); r0 = [r0 description]; r4 = [r0 retain]; NSLog(@"%@", r4); [r4 release]; return 0x0; }
      
      







残念ながら、clangではalways_inline属性をブロックに远加するこずはただ蚱可されおいたせん。

Xcodeの完党な゜ヌスコヌドずオヌトコンプリヌトはこちらです。



All Articles