iOS Block用法和实现原理

dyydp 2年前
   <p>《Objective-C高级编程》是一本有趣又难懂的书,全书就讲了引用计数、Block、GCD三个概念,有趣是因为讲原理、实现的部分是其它iOS专业书籍里少有的。然而每个章节不读个三五遍还是比较难理解贯通的。本文针对其中的Block部分做些简单的笔记记录,讲述Block的用法和部分实现原理,详细解说从原书中寻。</p>    <h2>Block概要</h2>    <p>Block:带有 <strong>自动变量</strong> 的 <strong>匿名函数</strong> 。</p>    <p>匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作用域。</p>    <p>自动变量:栈上声明的一个变量不是静态变量和全局变量,是不可以在这个栈内声明的匿名函数中使用的,但在Block中却可以。</p>    <p>虽然使用Block不用声明类,但是Block提供了类似Objective-C的类一样可以通过成员变量来 <strong>保存作用域外变量值</strong> 的方法,那些在Block的一对{}里使用到但却是在{}作用域以外声明的变量,就是Block截获的自动变量。</p>    <h2>Block常规概念</h2>    <h3>Block语法</h3>    <p>Block表达式语法:</p>    <p>^ 返回值类型 (参数列表) {表达式}</p>    <p>例如:</p>    <pre>  <code class="language-objectivec">^ int (int count) {          return count + 1;      };</code></pre>    <p>其中,可省略部分有:</p>    <ul>     <li>返回类型,例: <pre>  <code class="language-objectivec">^ (int count) {        return count + 1;    };</code></pre> </li>     <li>参数列表为空,则可省略,例: <pre>  <code class="language-objectivec">^ {        NSLog(@"No Parameter");    };</code></pre> 即最简模式语法为: <p>^ {表达式}</p> </li>    </ul>    <h3>Block类型变量</h3>    <p>声明Block类型变量语法:</p>    <p>返回值类型 (^变量名)(参数列表) = Block表达式</p>    <p>例如,如下声明了一个变量名为blk的Block:</p>    <pre>  <code class="language-objectivec">int (^blk)(int) = ^(int count) {          return count + 1;      };</code></pre>    <p>当Block类型变量作为函数的参数时,写作:</p>    <pre>  <code class="language-objectivec">- (void)func:(int (^)(int))blk {      NSLog(@"Param:%@", blk);  }</code></pre>    <p>借助typedef可简写:</p>    <pre>  <code class="language-objectivec">typedef int (^blk_k)(int);    - (void)func:(blk_k)blk {      NSLog(@"Param:%@", blk);  }</code></pre>    <p>Block类型变量作返回值时,写作:</p>    <pre>  <code class="language-objectivec">- (int (^)(int))funcR {      return ^(int count) {          return count ++;      };  }</code></pre>    <p>借助typedef简写:</p>    <pre>  <code class="language-objectivec">typedef int (^blk_k)(int);    - (blk_k)funcR {      return ^(int count) {          return count ++;      };  }</code></pre>    <h3>截获自动变量值</h3>    <p>Block表达式可截获所使用的自动变量的值。</p>    <p>截获:保存自动变量的 <strong>瞬间值</strong> 。</p>    <p>因为是“瞬间值”,所以声明Block之后,即便在Block外修改自动变量的值,也不会对Block内截获的自动变量值产生影响。</p>    <p>例如:</p>    <pre>  <code class="language-objectivec">int i = 10;      void (^blk)(void) = ^{          NSLog(@"In block, i = %d", i);      };      i = 20;//Block外修改变量i,也不影响Block内的自动变量      blk();//i修改为20后才执行,打印: In block, i = 10      NSLog(@"i = %d", i);//打印:i = 20</code></pre>    <h3>__block说明符号</h3>    <p>自动变量截获的值为Block声明时刻的瞬间值,保存后就不能改写该值,如需对自动变量进行重新赋值,需要在变量声明前附加__block说明符,这时该变量称为__block变量。</p>    <p>例如:</p>    <pre>  <code class="language-objectivec">__block int i = 10;//i为__block变量,可在block中重新赋值      void (^blk)(void) = ^{          NSLog(@"In block, i = %d", i);      };      i = 20;      blk();//打印: In block, i = 20      NSLog(@"i = %d", i);//打印:i = 20</code></pre>    <h3>自动变量值为一个对象情况</h3>    <p>当自动变量为一个类的 <strong>对象</strong> ,且没有使用__block修饰时,虽然不可以在Block内对该变量进行重新赋值,但可以修改该对象的属性。</p>    <p>如果该对象是个Mutable的对象,例如NSMutableArray,则还可以在Block内对NSMutableArray进行元素的增删:</p>    <pre>  <code class="language-objectivec">NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];      NSLog(@"Array Count:%ld", array.count);//打印Array Count:2      void (^blk)(void) = ^{          [array removeObjectAtIndex:0];//Ok          //array = [NSNSMutableArray new];//没有__block修饰,编译失败!      };      blk();      NSLog(@"Array Count:%ld", array.count);//打印Array Count:1</code></pre>    <h2>Block实现原理</h2>    <h3>使用Clang</h3>    <p>Block实际上是作为极普通的 <strong>C语言源码</strong> 来处理的:含有Block语法的源码首先被转换 <strong>成C语言编译器能处理的源码</strong> ,再作为普通的C源代码 <strong>进行编译</strong> 。</p>    <p>使用LLVM编译器的clang命令可将含有Block的Objective-C代码转换成C++的源代码,以探查其具体实现方式:</p>    <p>clang -rewrite-objc 源码文件名</p>    <p>注:如果使用该命令报错: <em>’UIKit/UIKit.h’ file not found</em> 。</p>    <h3>Block结构</h3>    <p>使用Block的时候,编译器对Block语法进行了怎样的转换?</p>    <pre>  <code class="language-objectivec">int main() {      int count = 10;      void (^ blk)() = ^(){          NSLog(@"In Block:%d", count);      };      blk();  }</code></pre>    <p>如上所示的最简单的Block使用代码,经clang转换后,可得到以下几个部分(有代码删减和注释添加):</p>    <pre>  <code class="language-objectivec">static void __main_block_func_0(      struct __main_block_impl_0 *__cself) {      int count = __cself->count; // bound by copy        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0,       count);  }</code></pre>    <p>这是一个函数的实现,对应Block中{}内的内容,这些内容被当做了C语言函数来处理,函数参数中的 <strong>__cself</strong> 相当于Objective-C中的self。</p>    <pre>  <code class="language-objectivec">struct __main_block_impl_0 {    struct __block_impl impl;    struct __main_block_desc_0* Desc; //描述Block大小、版本等信息    int count;    //构造函数函数    __main_block_impl_0(void *fp,            struct __main_block_desc_0 *desc,            int _count,            int flags=0) : count(_count) {      impl.isa = &_NSConcreteStackBlock; //在函数栈上声明,则为_NSConcreteStackBlock      impl.Flags = flags;      impl.FuncPtr = fp;      Desc = desc;    }  };</code></pre>    <p>__main_block_impl_0即为 <strong>main()函数栈上的Block结构体</strong> ,其中的__block_impl结构体声明如下:</p>    <pre>  <code class="language-objectivec">struct __block_impl {    void *isa;//指明对象的Class    int Flags;    int Reserved;    void *FuncPtr;  };</code></pre>    <p>__block_impl结构体,即为Block的结构体,可理解为 <strong>Block的类结构</strong> 。</p>    <p>再看下main()函数翻译的内容:</p>    <pre>  <code class="language-objectivec">int main() {      int count = 10;      void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);  }</code></pre>    <p>去除掉复杂的类型转化,可简写为:</p>    <pre>  <code class="language-objectivec">int main() {      int count = 10;      sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,         //函数指针                                                             &__main_block_desc_0_DATA)); //Block大小、版本等信息        (*blk->FuncPtr)(blk);   //调用FuncPtr指向的函数,并将blk自己作为参数传入  }</code></pre>    <p>由此,可以看出, <strong>Block也是Objective-C中的对象</strong> 。</p>    <p>Block有三种类(即__block_impl的 <strong>isa</strong> 指针指向的值,根据Block对象创建时 <strong>所处数据区不同</strong> 而进行区别:</p>    <ul>     <li><strong>_NSConcreteStackBlock</strong> :在栈上创建的Block对象</li>     <li><strong>_NSConcreteMallocBlock</strong> :在堆上创建的Block对象</li>     <li><strong>_NSConcreteGlobalBlock</strong> :全局数据区的Block对象</li>    </ul>    <h3>如何截获自动变量</h3>    <p>上部分介绍了 <strong>Block的结构</strong> ,和 <strong>作为匿名函数的调用机制</strong> ,那 <strong>自动变量截获</strong> 是发生在什么时候呢?</p>    <p>观察上节代码中 <strong>__main_block_impl_0</strong> 结构体(main栈上Block的结构体)的构造函数可以看到,栈上的变量count以参数的形式传入到了这个构造函数中,此处即为 <strong>变量的自动截获</strong> 。</p>    <p>因此可以这样理解: <strong>__block_impl</strong> 结构体已经可以代表Block类了,但在栈上又声明了 <strong>__main_block_impl_0</strong> 结构体,对 <strong>__block_impl</strong> 进行 <strong>封装</strong> 后才来表示栈上的 <strong>Block类</strong> ,就是为了获取Block中使用到的栈上声明的变量( <strong>栈上没在Block中使用的变量不会被捕获</strong> ),变量被保存在Block的结构体实例中。</p>    <p>所以在blk()执行之前,栈上简单数据类型的count无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。但这个变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。</p>    <h3>Block的存储域</h3>    <p>上文已提到,根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。</p>    <pre>  <code class="language-objectivec">void (^blk)(void) = ^{      NSLog(@"Global Block");  };    int main() {      blk();      NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__  }</code></pre>    <p>像上面代码块中的全局blk自然是存储在全局数据区,但注意在 <strong>函数栈上</strong> 创建的blk,如果 <strong>没有截获自动变量</strong> ,Block的结构实例还是会被设置在程序的 <strong>全局数据区,而非栈上</strong> :</p>    <pre>  <code class="language-objectivec">int main() {      void (^blk)(void) = ^{//没有截获自动变量的Block          NSLog(@"Stack Block");      };      blk();      NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__        int i = 1;      void (^captureBlk)(void) = ^{//截获自动变量i的Block          NSLog(@"Capture:%d", i);      };      captureBlk();      NSLog(@"%@",[captureBlk class]);//打印:__NSMallocBlock__  }</code></pre>    <p>可以看到截获了自动变量的Block打印的类是 <strong>NSGlobalBlock</strong> ,表示存储在全局数据区。</p>    <p>但为什么捕获自动变量的Block打印的类却是设置在堆上的 <strong>NSMallocBlock</strong> ,而非栈上的 <strong>NSStackBlock</strong> ?这个问题稍后解释。</p>    <h3>Block复制</h3>    <p>配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了 <strong>将Block从栈上复制到堆上的方法</strong> 来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。</p>    <p>复制到堆上的Block, <strong>将_NSConcreteMallocBlock</strong> 类对象写入Block结构体实例的成员变量isa:</p>    <pre>  <code class="language-objectivec">impl.isa = &_NSConcreteMallocBlock;</code></pre>    <p>在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况 <strong>栈上的Block会自动复制到堆上</strong> :</p>    <ul>     <li>调用Block的copy方法</li>     <li>将Block作为函数返回值时</li>     <li>将Block赋值给__strong修改的变量时</li>     <li>向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时</li>    </ul>    <p>其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。</p>    <p>上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是 <em>NSMallocBlock_</em> 类,就是因为这个Block对象赋值给了 <em> <strong>_strong修饰的变量</strong> captureBlk( <em> </em></em></p>    <p>strong是ARC下对象的默认修饰符)。</p>    <p>因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是__NSStackBlock</p>    <p>:</p>    <pre>  <code class="language-objectivec">int count = 0;      blk_t blk = ^(){          NSLog(@"In Stack:%d", count);      };        NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__      NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__      NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__      NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__</code></pre>    <p>另外,原书存在ARC和MRC混合讲解、区分不明的情况,比如书中几个使用到栈上对象导致Crash的例子是MRC条件下才会发生的,但书中没做特殊说明。</p>    <h3>使用__block发生了什么</h3>    <p>Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。</p>    <p>自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。</p>    <p>__block保证了栈上和Block内(通常在堆上)可以访问和修改 <strong>“同一个变量”</strong> ,__block是如何实现这一功能的?</p>    <p>__block发挥作用的 <strong>原理</strong> :将栈上用__block修饰的自动变量 <strong>封装成一个结构体</strong> ,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。</p>    <p>验证过程:</p>    <p>现在对刚才的代码段,加上__block说明符,并在block内外读写变量count。</p>    <pre>  <code class="language-objectivec">int main() {      __block int count = 10;      void (^ blk)() = ^(){          count = 20;          NSLog(@"In Block:%d", count);//打印:In Block:20      };      count ++;      NSLog(@"Out Block:%d", count);//打印:Out Block:11        blk();  }</code></pre>    <p>将上面的代码段clang,发现Block的结构体 <strong>__main_block_impl_0</strong> 结构如下所示:</p>    <pre>  <code class="language-objectivec">struct __main_block_impl_0 {    struct __block_impl impl;    struct __main_block_desc_0* Desc;    __Block_byref_count_0 *count; // by ref    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {      impl.isa = &_NSConcreteStackBlock;      impl.Flags = flags;      impl.FuncPtr = fp;      Desc = desc;    }  };</code></pre>    <p>最大的变化就是count变量不再是int类型了,count变成了一个指向 <strong>__Block_byref_count_0</strong> 结构体的指针, <strong>__Block_byref_count_0</strong> 结构如下:</p>    <pre>  <code class="language-objectivec">struct __Block_byref_count_0 {    void *__isa;  __Block_byref_count_0 *__forwarding;   int __flags;   int __size;   int count;  };</code></pre>    <p>它保存了int count变量,还有一个指向 <strong>__Block_byref_count_0</strong> 实例的指针 <strong>__forwarding</strong> ,通过下面两段代码 <strong>__forwarding</strong> 指针的用法可以知道,该指针其实指向的是对象自身:</p>    <pre>  <code class="language-objectivec">//Block的执行函数  static void __main_block_func_0(struct __main_block_impl_0 *__cself) {    __Block_byref_count_0 *count = __cself->count; // bound by ref            (count->__forwarding->count) = 20;//对应count = 20;          NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0,           (count->__forwarding->count));      }</code></pre>    <pre>  <code class="language-objectivec">//main函数  int main() {      __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,      (__Block_byref_count_0 *)&count,       0,       sizeof(__Block_byref_count_0),       10};        void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,       &__main_block_desc_0_DATA,       (__Block_byref_count_0 *)&count,       570425344));        (count.__forwarding->count) ++;//对应count ++;        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1,       (count.__forwarding->count));        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);  }</code></pre>    <p>为什么要通过__forwarding指针完成对count变量的读写修改?</p>    <p>为了保证无论是在栈上还是在堆上,都能通过都__forwarding指针找到在堆上创建的count这个 main_block_func_0结构体,以完成对count->count(第一个count是 main_block_func_0对象,第二个count是int类型变量)的访问和修改。</p>    <p>示意图如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/c7d4ebc24559a1e01ed9054e28c22780.jpg"></p>    <p> </p>    <p>来自:http://www.jianshu.com/p/d28a5633b963</p>    <p> </p>