Objective-C中block实现和技巧学习

LynetteFure 8年前
   <p>什么是block?</p>    <p>首先,看一个极简的block:</p>    <pre>  <code class="language-objectivec">int main(int argc, const char * argv[]) {   @autoreleasepool {     ^{ };   }   return 0;  }  </code></pre>    <p>如何声明一个block在 Objective-C ?</p>    <ul>     <li> <p>As a local variable:</p> <pre>  <code class="language-objectivec">returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};  </code></pre> </li>     <li> <p>As a property:</p> <pre>  <code class="language-objectivec">@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);  </code></pre> </li>     <li> <p>As a method parameter:</p> <pre>  <code class="language-objectivec">- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;  </code></pre> </li>     <li> <p>As an argument to a method call:</p> <pre>  <code class="language-objectivec">[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];  </code></pre> </li>     <li> <p>As a typedef:</p> <pre>  <code class="language-objectivec"> typedef returnType (^TypeName)(parameterTypes);  TypeName blockName = ^returnType(parameters) {...};  </code></pre> </li>    </ul>    <h3><strong>block编译转换结构</strong></h3>    <pre>  <code class="language-objectivec">int myMain()  {   ^{ } ();     ^{ } ();     return 0;  }  </code></pre>    <p>对其执行 clang -rewrite-objc 编译转换成C++实现,得到以下代码:</p>    <p><img src="https://simg.open-open.com/show/2848af74d920ddbb5f6900a07cac85fd.jpg"></p>    <h3><strong>block的实际结构</strong></h3>    <p>关于block的数据结构和runtime是开源的,可以在llvm项目看到,或者下载苹果的 <a href="/misc/goto?guid=4959715250410987694" rel="nofollow,noindex">libclosure</a> 库的源码来看。苹果也提供了 <a href="/misc/goto?guid=4959715250499654849" rel="nofollow,noindex">在线的代码查看方式</a> ,其中包含了很多示例和文档说明。</p>    <p>接下来观察下Block_private.h文件中对block的相关结构体的真实定义:</p>    <p><img src="https://simg.open-open.com/show/3aefe64abfd56116d107cb438c968cc5.jpg"></p>    <ul>     <li>invoke,同上文的FuncPtr,block执行时调用的函数指针,block定义时内部的执行代码都在这个函数中</li>     <li>Block_descriptor,block的详细描述</li>    </ul>    <p>总体来说,block就是一个里面存储了指向 函数体中包含定义block时的代码块 的函数指针,以及 block外部上下文 变量等信息的结构体。</p>    <h2><strong>block的类型</strong></h2>    <p>在block runtime中,定义了6种类:</p>    <p><img src="https://simg.open-open.com/show/bcfeb19de97f07afed597a6fceef061d.png"></p>    <p><img src="https://simg.open-open.com/show/fef09a71cb1b7771050f90db93a1c5fe.png"></p>    <ul>     <li>_NSConcreteStackBlock 栈上创建的block</li>     <li>_NSConcreteMallocBlock 堆上创建的block</li>     <li>_NSConcreteGlobalBlock 作为全局变量的block</li>     <li>_NSConcreteWeakBlockVariable</li>     <li>_NSConcreteAutoBlock</li>     <li>_NSConcreteFinalizingBlock</li>    </ul>    <p>其中我们能接触到的主要是前3种,后三种用于GC,咱们就先不看了。</p>    <p>block的常见类型有3种:</p>    <ul>     <li>_NSConcreteGlobalBlock(全局)</li>     <li>_NSConcreteStackBlock(栈)</li>     <li>_NSConcreteMallocBlock(堆)</li>    </ul>    <p>APUE(Unix环境高级编程)的进程虚拟内存段分布图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/6c3218a21d4de16dc7e5dd5f9403f931.png"></p>    <p>其中前2种在Block.h种声明,后1种在Block_private.h中声明。</p>    <h3><strong>NSConcreteGlobalBlock和NSConcreteStackBlock</strong></h3>    <p>首先,根据前面两种类型,编写以下代码:</p>    <pre>  <code class="language-objectivec">void (^globalBlock)() = ^{    };      int block_type_Main()  {   void (^stackBlock1)() = ^{     };     stackBlock1();   globalBlock();     return 0;  }  </code></pre>    <p>对其进行编译转换后得到以下代码:</p>    <p><img src="https://simg.open-open.com/show/847c62d7a299b27b9044536b2bd96874.jpg"></p>    <p>可以看出globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局区域创建,编译时就已经确定了,位于上图中的代码段;stackBlock的isa指向了_NSConcreteStackBlock,即在栈区创建。</p>    <h3><strong>NSConcreteMallocBlock</strong></h3>    <p>堆中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存放到堆中)。由于block的拷贝最终都会调用_Block_copy_internal函数,所以观察这个函数就可以知道堆中block是如何被创建的了:</p>    <p><img src="https://simg.open-open.com/show/520a31339e6ba5fe185e7c2f7886f2f5.jpg"></p>    <h3><strong>三种类型block测试(MRC)</strong></h3>    <pre>  <code class="language-objectivec">#import "block_in_MRC.h"    typedef long (^BlkSum)(int, int);      @implementation block_in_MRC  + (void)main  {   BlkSum blk1 = ^ long (int a, int b) {   return a + b;   };   NSLog(@"blk1 = %@", blk1);// blk1 = <__NSGlobalBlock__: 0x47d0>       int base = 100;   BlkSum blk2 = ^ long (int a, int b) {   return base + a + b;   };   NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>     BlkSum blk3 = [[blk2 copy] autorelease];   NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>  }  @end  </code></pre>    <h2><strong>捕捉变量对block结构的影响</strong></h2>    <p>编译转换捕捉不同变量类型的block,以对比它们的区别。</p>    <h3><strong>局部变量</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">// 局部变量    int capture_var_effect_block_Main()  {     int a;   ^{a;};     // 报错 var is not assignable(missing __block type specifier)  // ^{a = 10;};     return 0;  }  </code></pre>    <p>对其进行编译转换后得到以下代码(注释不会被编译):</p>    <p><img src="https://simg.open-open.com/show/c1662c7f737ef7a54ae22e281aab6450.jpg"></p>    <p>我们通过指针传递</p>    <pre>  <code class="language-objectivec">int test()  {   int a = 0;   // 利用指针p存储a的地址   int *p = &a;     ^{   // 通过a的地址设置a的值   *p = 10;   }();     return 0;  }  </code></pre>    <p>变量a的生命周期是和方法test的栈相关联的,当test运行结束,栈随之销毁,那么变量a就会被销毁,p也就成为了野指针。如果block是作为参数或者返回值,这些类型都是跨栈的,也就是说再次调用会造成野指针错误。</p>    <h3><strong>全局变量</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">// 全局静态  static int a;  // 全局  int b;    int capture_global_var_effect_block()  {   ^{   a = 10;   b = 10;   }();     return 0;  }  </code></pre>    <p>编译转换后</p>    <p><img src="https://simg.open-open.com/show/873a448723a27533fdffa64b35fd4f22.jpg"></p>    <p>直接使用了a,b变量;</p>    <h3><strong>局部静态变量</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">int capture_local_var_effect_block()  {   static int a;   // 静态局部变量是存储在静态数据存储区域的,也就是和程序拥有一样的生命周期,也就是说在程序运行时,都能够保证block访问到一个有效的变量。但是其作用范围还是局限于定义它的函数中,所以只能在block通过静态局部变量的地址来进行访问。   ^{   a = 10;   }();     return 0;  }  </code></pre>    <p>编译转换后</p>    <p><img src="https://simg.open-open.com/show/864e4089f6e70feb6abd8e0700f61083.jpg"></p>    <h3><strong>__block修饰的变量</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">int __block_modify_var()  {   __block int a;     ^{   a = 10;   }();     return 0;  }  </code></pre>    <p>编译转换后</p>    <p><img src="https://simg.open-open.com/show/8b96b8cf6856cefa9ce495b1324dfd8b.jpg"></p>    <p>runtime.c _Block_byref_assign_copy 方法</p>    <p><img src="https://simg.open-open.com/show/3e582ccdc459fec6f6db2b8118a6b936.jpg"></p>    <h3><strong>self隐式循环引用</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">@implementation self_hidden_retain_cycle  {   int _a;   void (^_block)();  }    - (void)test  {   void (^_block)() = ^{   _a = 10;   };  }  @end  </code></pre>    <p>编译转换后</p>    <p><img src="https://simg.open-open.com/show/52a0181f9093e2c837ea486910465e34.jpg"></p>    <h3><strong>ObjC对象(MRC)</strong></h3>    <p>代码</p>    <pre>  <code class="language-objectivec">@interface MyClass : NSObject {   NSObject* _instanceObj;  }  @end    @implementation MyClass    NSObject* __globalObj = nil;    - (id) init {   if (self = [super init]) {   _instanceObj = [[NSObject alloc] init];   }   return self;  }    - (void) test {   static NSObject* __staticObj = nil;   __globalObj = [[NSObject alloc] init];   __staticObj = [[NSObject alloc] init];     NSObject* localObj = [[NSObject alloc] init];   __block NSObject* blockObj = [[NSObject alloc] init];     typedef void (^MyBlock)(void) ;   MyBlock aBlock = ^{   NSLog(@"%@", __globalObj);   NSLog(@"%@", __staticObj);   NSLog(@"%@", _instanceObj);   NSLog(@"%@", localObj);   NSLog(@"%@", blockObj);   };   aBlock = [[aBlock copy] autorelease];   aBlock();     NSLog(@"%d", [__globalObj retainCount]);   NSLog(@"%d", [__staticObj retainCount]);   NSLog(@"%d", [_instanceObj retainCount]);   NSLog(@"%d", [localObj retainCount]);   NSLog(@"%d", [blockObj retainCount]);  }  @end  </code></pre>    <p>执行结果为1 1 1 2 1。</p>    <p>globalObj和staticObj在内存中的位置是确定的,所以Block copy时不会retain对象。</p>    <p>_instanceObj在Block copy时也没有直接retain _instanceObj对象本身,但会retain self。所以在Block中可以直接读写_instanceObj变量。</p>    <p>localObj在Block copy时,系统自动retain对象,增加其引用计数。</p>    <p>blockObj在Block copy时也不会retain。</p>    <p>非ObjC对象,如GCD队列dispatch_queue_t。Block copy时并不会自动增加他的引用计数。</p>    <h3><strong>Block中使用的ObjC对象的行为</strong></h3>    <pre>  <code class="language-objectivec">@property (nonatomic, copy) void(^myBlock)(void);    block_in_MRC* obj = [[[block_in_MRC alloc] init] autorelease];   self.myBlock = ^ {   // obj doSomething   };  </code></pre>    <p>对象obj在Block被copy到堆上的时候自动retain了一次。因为Block不知道obj什么时候被释放,为了不在Block使用obj前被释放,Block retain了obj一次,在Block被释放的时候,obj被release一次。</p>    <h2><strong>不同类型block的复制</strong></h2>    <p>block的复制代码在_Block_copy_internal函数中。</p>    <p><img src="https://simg.open-open.com/show/60c75852e65ab069b7a74f966d023d03.jpg"></p>    <h3><strong>Block的copy、retain、release操作(MRC)</strong></h3>    <pre>  <code class="language-objectivec">+ (void)test  {   int base = 100;   BlkSum blk2 = ^ long (int a, int b) {   return base + a + b;   };   NSLog(@"blk2 = %@", blk2); // blk2 = <__NSStackBlock__: 0xbfffddf8>     BlkSum blk3 = [[[[[blk2 copy] copy] copy] copy] copy];   NSLog(@"blk3 = %@", blk3); // blk3 = <__NSMallocBlock__: 0x902fda0>   NSLog(@"blk3 retainCount = %@", @([blk3 retainCount]));// blk3 retainCount = 1       BlkSum blk4 = [blk2 copy];   [blk4 retain];   NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1   [blk4 release];   NSLog(@"blk4 retainCount = %@", @([blk4 retainCount]));// blk4 retainCount = 1  }  </code></pre>    <p>Block_release in runtime.c</p>    <p><img src="https://simg.open-open.com/show/e2f06c54959d679f2d090733832b371f.jpg"></p>    <ul>     <li>对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount始终是1;</li>     <li>NSGlobalBlock:retain、copy、release操作都无效;</li>     <li>NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之后生成新的NSMallocBlock类型对象。</li>     <li>NSMallocBlock支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。copy之后不会生成新的对象,只是增加了一次引用,类似retain;</li>    </ul>    <h2><strong>ARC中的block</strong></h2>    <p><img src="https://simg.open-open.com/show/4efb234089c1928ab8ca98949de16e92.png"></p>    <p><a href="/misc/goto?guid=4959674157494650120" rel="nofollow,noindex">苹果文档</a> 提及,在ARC模式下,在栈间传递block时,不需要手动copy栈中的block,即可让block正常工作。主要原因是ARC对栈中的block自动执行了copy,将_NSConcreteStackBlock类型的block转换成了_NSConcreteMallocBlock的block。</p>    <h3><strong>block 实验</strong></h3>    <pre>  <code class="language-objectivec">+ (void)main  {   int i = 10;   void (^block)() = ^{i;};     __weak void (^weakBlock)() = ^{i;};     void (^stackBlock)() = ^{};     // ARC情况下     // 创建时,都会在栈中   // <__NSStackBlock__: 0x7fff5fbff730>   NSLog(@"%@", ^{i;});     // 因为stackBlock为strong类型,且捕获了外部变量,所以赋值时,自动进行了copy   // <__NSMallocBlock__: 0x100206920>   NSLog(@"%@", block);     // 如果是weak类型的block,依然不会自动进行copy   // <__NSStackBlock__: 0x7fff5fbff728>   NSLog(@"%@", weakBlock);     // 如果block是strong类型,并且没有捕获外部变量,那么就会转换成__NSGlobalBlock__   // <__NSGlobalBlock__: 0x100001110>   NSLog(@"%@", stackBlock);     // 在非ARC情况下,产生以下输出   // <__NSStackBlock__: 0x7fff5fbff6d0>   // <__NSStackBlock__: 0x7fff5fbff730>   // <__NSStackBlock__: 0x7fff5fbff700>   // <__NSGlobalBlock__: 0x1000010d0>  }  </code></pre>    <p>可以看出,ARC对类型为strong且捕获了外部变量的block进行了copy。并且当block类型为strong,但是创建时没有捕获外部变量,block最终会变成 <strong>NSGlobalBlock</strong> 类型(这里可能因为block中的代码没有捕获外部变量,所以不需要在栈中开辟变量,也就是说,在编译时,这个block的所有内容已经在代码段中生成了,所以就把block的类型转换为全局类型)</p>    <h3><strong>block作为参数传递</strong></h3>    <p>在栈中的block需要注意的情况:</p>    <pre>  <code class="language-objectivec">NSMutableArray *arrayM;    void myBlock()  {   int a = 5;   Block block = ^ {   NSLog(@"%d", a);   };     [arrayM addObject:block];   NSLog(@"%@", block);  }    + (void)test  {   arrayM = @[].mutableCopy;     myBlock();     Block block = [arrayM firstObject];   // 非ARC这里崩溃   block();  }  </code></pre>    <p>可以看到,ARC情况下因为自动执行了copy,所以返回类型为 <strong>NSMallocBlock</strong> ,在函数结束后依然可以访问;而非ARC情况下,需要我们手动调用[block copy]来将block拷贝到堆中,否则因为栈中的block生命周期和函数中的栈生命周期关联,当函数退出后,相应的堆被销毁,block也就不存在了。</p>    <p>如果把block的以下代码删除:</p>    <pre>  <code class="language-objectivec">NSLog(@"%d", a);  </code></pre>    <p>那么block就会变成全局类型,在test中访问也不会出崩溃。</p>    <h3><strong>block作为返回值</strong></h3>    <p>在非ARC情况下,如果返回值是block,则一般这样操作:</p>    <pre>  <code class="language-objectivec">return [[block copy] autorelease];  </code></pre>    <p>对于外部要使用的block,更趋向于把它拷贝到堆中,使其脱离栈生命周期的约束。</p>    <h3><strong>block属性</strong></h3>    <p>这里还有一点关于block类型的ARC属性。上文也说明了,ARC会自动帮strong类型且捕获外部变量的block进行copy,所以在定义block类型的属性时也可以使用strong,不一定使用copy。也就是以下代码:</p>    <pre>  <code class="language-objectivec">/** 假如有栈block赋给以下两个属性 **/    // 这里因为ARC,当栈block中会捕获外部变量时,这个block会被copy进堆中  // 如果没有捕获外部变量,这个block会变为全局类型  // 不管怎么样,它都脱离了栈生命周期的约束    @property (strong, nonatomic) Block *strongBlock;    // 这里都会被copy进堆中  @property (copy, nonatomic) Block *copyBlock;  </code></pre>    <h2><strong>ARC与非ARC(MRC)下的Weak-Strong Dance</strong></h2>    <h3>ARC</h3>    <p>在使用block过程中,经常会遇到 retain cycle 的问题,例如:</p>    <pre>  <code class="language-objectivec">- (void)dealloc   {    [[NSNotificationCenter defaultCenter] removeObserver:_observer];   }     - (void)loadView   {      [super loadView];      _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"    object:nil    queue:nil    usingBlock:^(NSNotification *note) {    [self dismissModalViewControllerAnimated:YES];    }];   }  </code></pre>    <p>在block中用到了self,self会被block retain,而_observer会copy一份该block,就是说_observer间接持有self,同时当前的self也会retain _observer,最终导致self持有_observer,_observer持有self,形成 retain cycle 。</p>    <p>对于在block中的 retain cycle ,在2011 WWDC Session #322 (Objective-C Advancements in Depth)有一个解决方案 weak-strong dance ,很漂亮的名字。其实现如下:</p>    <pre>  <code class="language-objectivec">- (void)dealloc   {    [[NSNotificationCenter defaultCenter] removeObserver:_observer];   }     - (void)loadView   {    [super loadView];    __weak TestViewController *wself = self;    _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"    object:nil    queue:nil    usingBlock:^(NSNotification *note) {    __strong TestViewController *sself = wself;    [sself dismissModalViewControllerAnimated:YES];    }];   }  </code></pre>    <p>在block中使用self之前先用一个 __weak 变量引用self,导致block不会retain self,打破retain cycle,然后在block中使用wself之前先用 __strong 类型变量引用wself,以确保使用过程中不会dealloc。简而言之就是推迟对self的retain,在使用时才进行retain。这有点像lazy loading的意思。</p>    <p>注:iOS5以下没有 __weak ,则需使用 __unsafe_unretained 。</p>    <h3><strong>非ARC(MRC)</strong></h3>    <p>在非ARC环境中,显然之前的使用的 __weak 或 __unsafe_unretained 将会是无效的,那么我们需使用另外一种方法来代替,这里就需要用到 __block 。</p>    <p>__block 在ARC和非ARC中有点细微的差别( <a href="/misc/goto?guid=4959518189778229840" rel="nofollow,noindex">Automatic Reference Counting : Blocks</a> ):</p>    <ul>     <li> <p>在ARC中, __block 会自动进行retain</p> <pre>  <code class="language-objectivec">// ARC 中 `__block`会自动进行retain 实验  + (void)test__Block  {   // You can use CFGetRetainCount with Objective-C objects, even under ARC:   NSObject *objc = [[NSObject alloc] init];   NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));   __block NSObject *objcNew = objc;   NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));  }  // 输出  // test__Block-- objc Retain count is 1  // test__Block-- objc Retain count is 2  </code></pre> </li>     <li> <p>在非ARC中, __block 不会自动进行retain</p> <pre>  <code class="language-objectivec">// 在MRC中 __block不会自动进行retain  + (void)test__Block  {   // You can use CFGetRetainCount with Objective-C objects, even under ARC:   NSObject *objc = [[NSObject alloc] init];   NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));   __block NSObject *objcNew = objc;   NSLog(@"test__Block-- objc Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)objc));  }  // 输出  // test__Block-- objc Retain count is 1  // test__Block-- objc Retain count is 1  </code></pre> </li>    </ul>    <p>因此首先要注意的一点就是用 __block 打破 retain cycle 的方法仅在非ARC下有效,下面是非ARC的 weak-strong dance :</p>    <pre>  <code class="language-objectivec">- (void)dealloc   {    [[NSNotificationCenter defaultCenter] removeObserver:_observer];    [_observer release];    [super dealloc];   }     - (void)loadView   {    [super loadView];    __block TestViewController *bself = self;    _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"    object:nil    queue:nil    ngBlock:^(NSNotification *note) {      [bself retain];    [bself dismissModalViewControllerAnimated:YES];    [bself release];      }];   }  </code></pre>    <p>将self赋值为 __block 类型变量,在非ARC中 __block 类型变量不会进行retain,从而打破retain cycle,然后在使用bself前进行retain,以确保在使用过程中不会dealloc 。</p>    <h2><strong>总结</strong></h2>    <p>打破循环引用:</p>    <ul>     <li>ARC下: __week</li>     <li>非ARC(MRC)下:__block</li>    </ul>    <p>__block的作用:</p>    <p>非ARC(MRC)下</p>    <ol>     <li>说明变量可改</li>     <li>说明指针指向的对象不做隐式retain操作。</li>    </ol>    <p>ARC下只有1。</p>    <p> </p>    <p>来自:http://www.lijianfei.cn/2016/07/21/objective-block-learning/</p>    <p> </p>