O C高级编程 第二章, Blocks,O C高级编程 第二章, Blocks 详解

马肤

温馨提示:这篇文章已超过470天没有更新,请注意相关的内容是否还可用!

摘要:本章介绍了O C高级编程中的Blocks概念。Blocks是O C语言中的一种重要结构,用于组织和管理代码。本章详细阐述了Blocks的定义、语法和用法,包括如何创建、调用和传递参数等。通过学习和掌握Blocks,读者可以更好地管理代码结构,提高代码的可读性和可维护性。本章内容对于理解O C高级编程中的核心机制具有重要意义。

1.block的使用

1.1什么是block?

Blocks是C语言的扩充功能:带有自动变量(局部变量)的匿名函数。

“带有自动变量”在Blocks中表现为“截取自动变量"

“匿名函数”就是“不带名称的函数”

块,封装了函数调用及调用环境的OC对象

  • block的声明
    // 1.
    @property (nonatomic, copy) void(^myBlock1)(void);
    // 2.BlockType:类型别名
    typedef void(^BlockType)(void);
    @property (nonatomic, copy) BlockType myBlock2;
    // 3.
    // 返回值类型(^block变量名)(参数1类型,参数2类型,...)
    void(^block)(void);
    
    • block的定义
          // ^返回值类型(参数1,参数2,...){};
          // 1.无返回值,无参数
          void(^block1)(void) = ^{
              
          };
          // 2.无返回值,有参数
          void(^block2)(int) = ^(int a){
              
          };
          // 3.有返回值,无参数(不管有没有返回值,定义的返回值类型都可以省略)
          int(^block3)(void) = ^int{
              return 3;
          };
          // 以上Block的定义也可以这样写:
          int(^block4)(void) = ^{
              return 3;
          };
          // 4.有返回值,有参数
          int(^block5)(int) = ^int(int a){
              return 3 * a;
          };
      
      • block的调用
            // 1.无返回值,无参数
            block1();
            // 2.有返回值,有参数
            int a = block5(2);
        

        2.block的底层数据结构

        通过Clang将以下的OC代码转化为C++代码

        // Clang

        xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

        //main.m
        #import 
         
        int main(int argc, const char * argv[]) {
            @autoreleasepool {
                void(^block)(void) = ^{
                    NSLog(@"我是 block");
                };
                block();
            }
            return 0;
        }
        

        转化为C++代码

        //参数结构体
        struct __main_block_impl_0 {
          struct __block_impl impl;// block 结构体
          struct __main_block_desc_0* Desc;// block 的描述对象
        /*
        block 的构造函数
         ** 返回值:__main_block_impl_0 结构体
         ** 参数一:__main_block_func_0 结构体
         ** 参数二:__main_block_desc_0 结构体的地址
         ** 参数三:flags 标识位
        */
          __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
            impl.isa = &_NSConcreteStackBlock;// &_NSConcreteStackBlock 表示存储在栈上
            impl.Flags = flags;
            impl.FuncPtr = fp;
            Desc = desc;
          }
        };
         
        //block 结构体
        struct __block_impl {
          void *isa;//block 的类型
          int Flags;
          int Reserved;
          void *FuncPtr;// block的执行函数指针,指向__main_block_func_0
        };
         
        //封装了 block 中的代码
        //参数是__main_block_impl_0结构体的指针
        static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
         
                    NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_03dcda_mi_0);
                }
         
         
        static struct __main_block_desc_0 {
          size_t reserved;
          size_t Block_size;// block 所占的内存空间
        } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
        int main(int argc, const char * argv[]) {
            /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        /*
                 ** void(^block)(void) = ^{
                        NSLog(@"调用了block");
                    };
                 ** 定义block的本质:
                 ** 调用__main_block_impl_0()构造函数
                 ** 并且给它传了两个参数 __main_block_func_0 和 &__main_block_desc_0_DATA
                 ** __main_block_func_0 封装了block里的代码
                 ** 拿到函数的返回值,再取返回值的地址 &__main_block_impl_0,
                 ** 把这个地址赋值给 block
                 */
                void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        /*
                 ** block();
                 ** 调用block的本质:
                 ** 通过 __main_block_impl_0 中的 __block_impl 中的 FuncPtr 拿到函数地址,直接调用
                 */    
                ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
            }
            return 0;
        }
        

        3.block的变量捕获机制

        • 对于全局变量,不会捕获到block内部,访问方为直接访问
        • 对于auto类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,访问方式为值传递
        • 对于static类型的局部变量,会捕获到block内部,block内部会自动生成一个成员变量,访问方式为指针传递
        • 对于对象类型的局部变量,block会连同修饰符一起捕获

          O C高级编程 第二章, Blocks,O Blocks 详解 第1张

          3.1 auto自动变量

          将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

          int main(int argc, const char * argv[]) {
              @autoreleasepool {
                  int age = 10;
                  void(^block)(void) = ^{
                      NSLog(@"%d",age);
                  };
                  block();
              }
              return 0;
          }
          
          struct __main_block_impl_0 {
            struct __block_impl impl;
            struct __main_block_desc_0* Desc;
            int age;//生成的变量
            __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
              impl.isa = &_NSConcreteStackBlock;
              impl.Flags = flags;
              impl.FuncPtr = fp;
              Desc = desc;
            }
          };
          static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
            int age = __cself->age; // bound by copy
           
                      NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_40c716_mi_0,age);
                 }
          

          可以看出:

          • 在__main_block_impl_0结构体中会自动生成一个相同类型的age变量
          • 构造函数__main_block_impl_0中多出了一个age参数,用来捕获外部的变量

            由于传递方式为值传递,所以我们在block外部修饰age变量时,不会影响到block中的age变量

            总的来说,所谓“截获自动变量”意味着在执行Block语法时,Block语法表达式所用的自动变量被保存到Block的结构体实例中(即Block自身)中

            3.2static类型的局部变量

            将以下OC代码转化为C++代码,并贴出部分变动代码

                static int age = 10;
                void(^block)(void) = ^{
                    NSLog(@"%d",age);
                };
                age = 20;
                block();
                // 20
            
            struct __main_block_impl_0 {
              struct __block_impl impl;
              struct __main_block_desc_0* Desc;
              int *age;
              __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
                impl.isa = &_NSConcreteStackBlock;
                impl.Flags = flags;
                impl.FuncPtr = fp;
                Desc = desc;
              }
            };
            static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
              int *age = __cself->age; // bound by copy
             
                        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_cb7943_mi_0,(*age));
                    }
            

            可以看出:

            • __main_block_impl_0结构体中生成了一个相同类型的age变量
            • __main_block_impl_0构造函数多了个参数,用来捕获外部的age变量的地址

              由于传递方式是指针传递,所以修改局部变量age时,age的值会随之变化

              3.3全局变量

              将以下OC代码转化为C++代码,并贴出部分变动代码

              int height = 10;
              static int age = 20;
              int main(int argc, const char * argv[]) {
                  @autoreleasepool {
                      void(^block)(void) = ^{
                          NSLog(@"%d,%d",height,age);
                      };
                      block();
                  }
                  return 0;
              }
              
              int height = 10;
              static int age = 20;
               
              struct __main_block_impl_0 {
                struct __block_impl impl;
                struct __main_block_desc_0* Desc;
                __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
                  impl.isa = &_NSConcreteStackBlock;
                  impl.Flags = flags;
                  impl.FuncPtr = fp;
                  Desc = desc;
                }
              };
              static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
               
                          NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_7a340f_mi_0,height,age);
                      }
              

              可以看出:

              1. __main_block_impl_0结构体中,并没有自动生成age和height全局变量,也就是说没有将变量捕获到block内部

              虽然block语法的匿名函数部分简单地变换为了C语言函数,但从这个变换的函数中访问静态全局变量/全局变量并没有任何改变,可直接使用。

              但是静态变量的情况下,转换后的函数原本就设置在含有Block语法的函数外,所以无法从变量作用域访问

              为什么局部变量需要捕获,而全局变量不用呢?

              • 作用域的原因,全局变量哪里都可以直接访问,所以不用捕获
              • 局部变量,外部不能直接访问,所以需要捕获
              • auto类型的局部变量可能会销毁,其内存会消失,block将来执行代码的时候不可能再去访问呢块内存,所以捕获其值
              • static变量会一直保存在内存中,所以捕获其地址即可

                3.4 _block修饰的变量

                3.4.1 _block的使用

                默认情况下block是不能修改外面的auto变量,解决办法?

                • 变量用static修饰(原因:捕获static类型的局部变量是指针传递,可以访问到该变量的内存地址
                • 全局变量
                • _block(我们只是希望临时用一下这个变量临时改一下而已,而改为static变量和全局变量会一直在内存中)
                  3.4.2 _block修饰符
                  • _block同于解决block内部无法修改auto变量值的问题;
                  • _block不能修饰全局变量,静态变量;
                  • 编译器会将_block变量包装成一个对象(struct __Block_byref_age_0(byref:按地址传递));
                  • 加_block修饰不会改变变量的性质,他还是auto变量;
                  • 一般情况,对捕获变量进行赋值(赋值!=使用)操作需要添加_block修饰符,比如给数组添加或删除对象,就不用加_bolck修饰符;
                  • 在 MRC 下使用 __block 修饰对象类型,在 block 内部不会对该对象进行 retain 操作,所以在 MRC 环境下可以通过 __block 解决循环引用的问题;

                    将以下 OC 代码转换为 C++ 代码,并贴出部分变动代码

                    int main(int argc, const char * argv[]) {
                        @autoreleasepool {
                            __block int age = 10;
                            void(^block)(void) = ^{
                                age = 20;
                                NSLog(@"block-%d",age);
                            };
                            block();
                            NSLog(@"%d",age);
                        }
                        return 0;
                    }
                    
                    struct __Block_byref_age_0 {
                      void *__isa;
                    __Block_byref_age_0 *__forwarding;//持有指向该实例自身的指针
                     int __flags;
                     int __size;
                     int age;
                    };
                     
                    struct __main_block_impl_0 {
                      struct __block_impl impl;
                      struct __main_block_desc_0* Desc;
                      __Block_byref_age_0 *age; // by ref
                      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
                        impl.isa = &_NSConcreteStackBlock;
                        impl.Flags = flags;
                        impl.FuncPtr = fp;
                        Desc = desc;
                      }
                    };
                    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
                      __Block_byref_age_0 *age = __cself->age; // bound by ref
                     
                                (age->__forwarding->age) = 20;
                                NSLog((NSString *)&__NSConstantStringImpl__var_folders_yx_7jg_wdg128v45l4cn_1g265h0000gn_T_main_75529b_mi_0,(age->__forwarding->age));
                            }
                    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
                     
                    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
                    

                    可以看出:

                    • 编译器会将_block修饰的变量包装成一个__Block_byref_age_0结构体对象
                    • 以上age = 20 的赋值过程:通过block的__main_block_func_0结构体实例中( __Block_byref_age_0)类型的age指针,找到 __Block_byref_age_0结构体的对象, __Block_byref_age_0结构体对象持有指向实例本身的__forwarding指针,通过成员变量_forwarding访问 __Block_byref_age_0结构体里的age变量,并将值改为20;

                      O C高级编程 第二章, Blocks,O Blocks 详解 第2张

                      2.3.4BLock存储域

                      Block转换为Block的结构体类型的自动变量,__block变量转换为_blockb变量的结构体类型的自动变量。

                      所谓结构体类型的自动变量,即栈上生成的该结题的实例。

                      O C高级编程 第二章, Blocks,O Blocks 详解 第3张

                      前面讲到,Block也是OC对象,,将BLock当作OC对象来看时,该block的累是_NSConcreteStackBlock。虽然该类没有出现在已变换的源代码中,但有很多与之类时的类,如

                      • _NSConcereteStackBlock
                      • _NSConcereteGlobalBlock
                      • _NSConcereteMallocBlock

                        O C高级编程 第二章, Blocks,O Blocks 详解 第4张

                        这些类的对象存储区域

                        O C高级编程 第二章, Blocks,O Blocks 详解 第5张

                        到现在的Block例子使用的都是_NSConcereteStackBlock类,且都设置在栈上。也有例外,在记述全区变量的地方使用Block语法时,生成的Block为 _NSConcereteGlobalBlock类对象。

                        O C高级编程 第二章, Blocks,O Blocks 详解 第6张

                        此源代码通过声明全局变量blk来是用BLock语法,如果转换该源代Block结构体用到的成员变量isa的初始化如下

                        O C高级编程 第二章, Blocks,O Blocks 详解 第7张

                        该block的类为 _NSConcereteGlobalBlock类,该Block用的结构体实例设置在程序的数据区域中。应为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量的进行截获由此block同结构体实例的内容不依赖于执行时的状态,所以整个实例只需要一个实例,,因此block用结构体实例设置在与全局变量相同的数据区域中即可。

                        只在截获自动变量时,Block用结构体实例截获的值才会根据执行时的状态变化。

                        只要Block不截获自动变量,就可以将Block用结构体实例设置在程序的数据区域。

                        虽然通过clang转化的源代码通常都是_NSConcereStackBlock类对象,但实现上却有所不同,终结如下:

                        • 记述全局变量的地方有Block语法时
                        • Block语法的表达式中不使用应截获的自动变量时

                          以上情况,Block为_NSConcereteClobalBlock类对象。即Block配置在程序的数据域。除吃以外的Block语法生成的Block为_NSConcereStackBlock类对象,且设置在栈上。

                          那么将block配置在堆上的_NSConcreteMallocStack类何时使用?

                          这正是上节遗留问题的答案:

                          • block超出变量作用域可存在的原因
                          • __block变量用结构体成员变量__forwarding存在的原因

                            配置在全局变量上的Block,从变量作用域外也可以通过指针安全的使用,但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样,如果其所属的变量作用域结束,则该__block变量也会被废弃。

                            O C高级编程 第二章, Blocks,O Blocks 详解 第8张

                            Blocks提供了将Block和_block变量从栈上复制到堆上的方法来解决这个问题

                            将配置在栈上的block复制到栈上,这样block语法记述的变量作用域结束,堆上的Block也可以继续存在。

                            O C高级编程 第二章, Blocks,O Blocks 详解 第9张

                            复制到堆上的block将__NSConcereMallocBlock类对象写入Block用结构体实例的成员变量isa

                            O C高级编程 第二章, Blocks,O Blocks 详解 第10张

                            而__block变量用结构体成员变量__forwarding可以实现无论__block变量配置在啊栈上还是堆上时都能够正确滴访问__block变量。

                            2.3.5节已详细说明,有时在__block变量配置在堆上的状态下,也可以访问栈上的_block变量。在此情形下,只要栈上的结构体实例成员变量__forwording指向堆上的结构体实例,那么不管从栈上的__block变量还是从堆上的__block变量都能够正确访问。

                            那么Blocks提供的复制方法究竟是什么那?

                            实际上当ARC有效时,大多数情形下编译器会恰当进行判断,自动生成将Block从堆上复制到栈上的代码。

                            下面是返回Block的函数

                            O C高级编程 第二章, Blocks,O Blocks 详解 第11张

                            该代码为返回配置在栈上的Block的函数。即程序执行中从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。

                            该源代码通过对应ARC的编译器转换如下:

                            O C高级编程 第二章, Blocks,O Blocks 详解 第12张

                            另外,因为ARC处于有效的状态,所以blk_t tmp实际上与附有__strong 修饰符的blk_t __strong tmp 相同。

                            然而通过 objc4运行时库的runtime/objc-arrmm可知,objc_retainBlock函数实际上就是_Block_copy 函数。即:

                            tmp = _Block_copy(tmp);
                            return objc_autoreleaseReturnValue(tmp);
                            

                            具体的注释

                            /*
                             *将通过Block语法生成的Block,
                             *即配置在栈上的Block用结构体实例
                             *赋值给相当于Block类型的变量tmp中。
                             */
                            tmp=_Block_copy(tmp);
                            /*
                             *_Block_copy 函数
                             *将栈上的Block复制到堆上。
                             *复制后,将堆上的地址作为指针赋值给变量tmp。
                             */
                            return objc_autoreleaseReturnValue(tmp);
                            /*
                             *将堆上的Block作为Objective-c对象
                             *注册到autoreleasepool中,然后返回该对象。
                             */
                            

                            将Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码。

                            前面讲到过“大多数情况下编译器会适当地进行判断”,不过在此之外的情况下需要手动生成代码,将 Block 从栈上复制到堆上。此时我们使用“copy实例方法”。如下所示:

                            • 向方法或函数的参数中传递Block时

                              是如果在方法或函数中适当地复制了传递过来的参数,那么就不必在调用该方法或函数前手动复制了。以下方法或函数不用手动复制。

                              • Cocoa 框架的方法且方法名中含有usingBlock 等时
                              • Grand Central Dispatch 的 API

                                举个具体例子,在使用NSArray类的enumerateObjectsUsingBlock 实例方法以及dispatch_async 函数时,不用手动复制。相反地,在 NSArray 类的 initWithObjects 实例方法上传递 Block时需要手动复制。下面我们来看看源代码。

                                - (id)getBlockArray {
                                	int val =10;
                                	
                                	return [[NSArray alloc] initWithObjects:
                                		^{NSLog(@"blk0:%d",val);},
                                		^{NSLog(@"blk1:%d",val);}, nil];
                                }
                                

                                getBlockArray方法在栈上生成两个Block,并传递给NSArray类的initWithObjects 实例方法。下面,在getBlockArray 方法调用方,从 NSArray 对象中取出 Block 并执行。

                                id obj = getBlockArray();
                                typedef void (^blk_t)(void);
                                blk_t blk =(blk_t)[obj objectAtIndex:0];
                                blk();
                                

                                该源代码的blk(),即Block在执行时发生异常,应用程序强制结束。这是由于在 getBlockArray 函数执行结束时,栈上的Block 被废弃的缘故。可惜此时编译器不能判断是否需要复制。因此只在此情形下让编程人员手动进行复制。

                                该源代码像下面这样修改一下即可正常运行。

                                - (id)getBlockArray {
                                	int val = 10;
                                	
                                	return [[NSArray alloc] initWithObjects:
                                		[^{NSLog(@"blk:%d", val);} copy],
                                		[^{NSLog(@"blk1:%d",val);} copy], nil];
                                }
                                

                                虽然看起来有点奇怪,但像这样,对于Block 语法可直接调用copy方法。当然对于Block类型变量也可以调用copy方法。

                                typedef int (^blk_t)(int);
                                blk_t blk = ^(int count){return rate * count;};
                                blk = [blk copy];
                                

                                对于已配置在堆上的Block以及配置在程序的数据区域上的Block,调用copy方法又会如何呢?下面按配置Block的存储域,将copy方法进行复制的动作总结了出来。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第13张

                                不管Block配置在何处,用copy方法都不会硬气任何问题。在不确定调用copy方法即可。

                                4.__block变量存储域

                                使用__block变量的Block从栈复制到堆上时,___block也会受到影响。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第14张

                                若一个Block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些_block变量也全部被从栈复制到堆上。此时,block持有__block变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block变量没有任何影响。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第15张

                                在多个Block中使用__block中使用__block变量时,因为最先会将所有的Block配置在栈上,所以__block变量会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该Block所持有。当剩下的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用计数。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第16张

                                如果配置在堆上的Block被废弃,那么它使用的__block变量也就被释放。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第17张

                                此思考方式与OC引用计数内存管理完全相同。使用__block的变量的block持有__block变量。如果Block被废弃,它持有的__block变量也就被释放。

                                接下来回顾一下2.3.4讲过的使用__block变量用结构体成员变量__forwarding的原因。“不管__block变量配置在栈上还是堆上,都能够正确访问该变量”

                                通过Block复制,block变量也从栈复制到堆。此时可同时访问栈上的__block变量和堆上的__block变量。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第18张

                                利用copy方法复制了使用__block变量的Block语法。Block和__block变量两者均是从栈复制到堆。

                                此代码中在Block语法表达式中使用初始化后的__block变量;

                                ^{++val;}
                                

                                然后在 Block 语法之后使用与Block 无关的变量。

                                ++val;
                                

                                以上两种源代码均可转换为如下形式:

                                ++(val.__forwarding->val);
                                

                                在变化的Block语法的函数中,该变量val为复制到堆上的_block变量,而使用的雨Block无关的变量val,为复制前栈上的__block变量用结构体实例。

                                但是栈上的__block变量用结构体实例在__block变量从栈复制到堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第19张

                                通过该功能,无论在Block语法中,Block语法外使用__block变量,还是__block变量配置在堆上或栈上,都可以顺利访问同一个__block变量。

                                5.截获对象

                                对于截获OC对象

                                O C高级编程 第二章, Blocks,O Blocks 详解 第20张

                                上面的代码不会出现编译错误,而向截获的变量array赋值则会产生编译错误,该源代码中截获的变量值为NSMutableArray类对象。

                                如果用C语言来描述,即是截获NSMutableArray类对象用的结构体实例指针,使用截获的值不会有任何编译错误。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第21张

                                以下源代码生成并持有NSMutableArray类的对象,由于附有__strong 修饰符的赋值目标变量的作用域立即结束,因此对象被立即释放并废弃。

                                {
                                	id array = [[NSMutableArray alloc] init];
                                }
                                

                                Block语法中使用该变量array的代码:

                                O C高级编程 第二章, Blocks,O Blocks 详解 第22张

                                该源代码运行正常,执行结果如下:

                                O C高级编程 第二章, Blocks,O Blocks 详解 第23张

                                这一结果以为着赋值给变量array的NSMutableArray类的对象,在源代码最后执行部分超出其变量作用域而存在。

                                通过编译器转换后的源码如下:

                                O C高级编程 第二章, Blocks,O Blocks 详解 第24张

                                请注意被截获的自动变量array。我们可以发现它是Block用的结构体中附有__strong 修饰符的成员变量。

                                struct __main_block_impl_0 {
                                	struct __block_impl impl;
                                	struct __main_block_desc_0* Desc;
                                	id __strong array;//附有__strong修饰符	
                                };
                                

                                在OC中,C语言结构体不能含有附有strong修饰符的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好地管理内存。

                                但是OC的运行时库能够准确把握Block从栈复制到堆以及堆上的Block 被废弃的时机,因此Block 用结构体中即使含有附有__strong修饰符或__weak 修饰符的变量,也可以恰当地进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy 和 dispose,以及作为指针赋值给该成员变量的__main_block_copy_0函数和_main_block_dispose_0函数。

                                由于在该源代码的Block用结构体中,含有附有__strong修饰符的对象类型变量array,所以需要恰当管理赋值给变量array的对象。因此__main_block_copy_0 函数使用_Block_object_assign 函数将对象类型对象赋值给Block用结构体的成员变量array中并持有该对象。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第25张

                                _Block_object_assign 函数调用相当于retain 实例方法的函数,将对象赋值在对象类型的结构体成员变量中。

                                另外,__main_block_dispose_0 函数使用_Block_object_dispose 函数,释放赋值在 Block 用结构体成员变量array中的对象。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第26张

                                _Block_object_dispose 函数调用相当于 release 实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

                                虽然此__main_block_copy_0 函数(以下简称 copy 函数)和__main_block_dispose_0 函数(以下简称 dispose 函数)指针被赋值在__main_block_desc_0 结构体成员变量copy 和 dispose 中,但在转换后的源代码中,这些函数包括使用指针全都没有被调用。

                                那么这些函数是从哪调用呢?

                                在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。

                                O C高级编程 第二章, Blocks,O Blocks 详解 第27张

                                那么什么时候栈上的Block会复制到堆那?

                                • 调用Block的copy实例方法时
                                • Block作为函数返回值时
                                • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
                                • 在方法名中含有usingBlock的Coco框架方法时或Grand Central Dispatch的API中传递Block时

                                  在调用Block的 copy实例方法时,如果Block配置在栈上,那么该Block 会从栈复制到堆。Block作为函数返回值返回时、将Block赋值给附有__strong 修饰符id类型的类或Block类型员变量时,编译器自动地将对象的Block作为参数并调用_Block_copy函数,这与调用Block的copy实例方法的效果相同。

                                  在方法名中含有usingBlock 的 Cocoa 框架方法或 Grand Central Dispatch 的API 中传递 Block 时,在该方法或函数内部对传递过来的Block 调用 Block 的 copy 实例方法或者_Block_copy函数。

                                  也就是说,虽然从源代码来看,在上面这些情况下栈上的Block 被复制到堆上,但其实可归结为_Block_copy函数被调用时 Block 从栈复制到堆。

                                  相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的 dealloc 实例方法。

                                  有了这种构造,通过使用附有__strong修饰符的自动变量,Block 中截获的对象就能够超出其变量作用域而存在。

                                  虽然这种使用copy函数和dispose函数的方法在前面没做任何说明,但实际上在使用 block变量时已经用到了

                                  O C高级编程 第二章, Blocks,O Blocks 详解 第28张

                                  转化后的源代码在Block用结构体的部分基本相同。不同之处如下表

                                  O C高级编程 第二章, Blocks,O Blocks 详解 第29张

                                  通过BLOCK_FIELD_IS_OBJECT 和 BLOCK_FIELD_IS_BYREF 参数,区分copy函数和 dispose 函数的对象类型是对象还是**__block变量**。

                                  但是与copy函数持有截获的对象、dispose函数释放截获的对象相同,copy函数持有所使用的block变量,dispose函数释放所使用的__block变量。

                                  由此可知,Block中使用的赋值给附有__strong修饰符的自动变量的对象和复制到堆上的__block变量由于被堆上的Block所持有,因此可超出其变量作用域而存在。

                                  在刚才的源代码上,如果不调用Block的copy实例方法,执行该源代码后,程序会强制结束。

                                  因为只有调用_Block_copy函数才能持有截获的附有__strong修饰符的对象类型的自动变量值,如果不调用_Block_copy函数,即使截获了对象,它也会随着变量的作用域结束而被废弃。

                                  因此,Block中使用对象类型的自动变量时,除以下情形外,推荐调用Block的copy实例方法。

                                  • Block作为函数返回值时
                                  • 将Block赋值给类的附有__strong修饰的id类型或Block类型成员变量时
                                  • 向方法名中含有usingBlock的Coco框架方法时或Grand Central Dispatch的API中传递Block时

                                    因为以上的情形会自动调用_Block_copy函数。

                                    6.__Block变量和对象

                                    __block说明符可指定任何类型的自动变量,。下面指定用于赋值OC对象的id类型自动变量

                                    __block id obj = [[NSObject alloc] init];
                                    

                                    其实该代码等同于:

                                    __block id __strong obj = [[NSObject alloc] init];
                                    

                                    ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。

                                    该代码可通过clang 转换如下:

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第30张

                                    在这里出现了前面讲到的_Block_object_assign 函数和_Block_object_dispose 函数。

                                    在 Block 中使用附有__strong 修饰符的 id 类型或对象类型自动变量的情况下,当Block 从栈复制到堆时,使用_Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_Block_object_dispose 函数,释放Block 截获的对象。

                                    在__block变量为附有__strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block 变量从栈复制到堆时,使用_Block_object_assign 函数,持有赋值给__block变量的对象。当堆上的__block 变量被废弃时,使用_Block_object_dispose 函数,释放赋值给 Block 变量的对象。

                                    由此可知,即使对象赋值复制到堆上的附有__storng修饰符的对象类型__block变量中,只要_block变量在堆上继续存在,那么对象该对象就会继续处于被持有的状态。这与Block 中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。

                                    另外,我们前面用到的只有附有__strong 修饰符的id类型或对象类型自动变量。如果使用__weak 修饰符会如何呢?

                                    首先是在Block 中使用附有__weak 修饰符的 id 类型变量的情况。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第31张

                                    这是由于附有__strong修饰符的变量array在该变量作用域结束时,被释放,废弃,nil被赋值在附有__weak修饰符变量array2变量。代码可正常运行。

                                    若同时制指定__block说明符和__weak修饰符会怎样?

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第32张

                                    这是因为即使附加了__block说明符,附有__strong修饰符的变量array在该变量作用域结束时,被释放,废弃,nil被赋值在附有__weak修饰符变量array2变量。

                                    另外,由于附有__unsafe_unretained 修饰符的变量只不过与指针相同,所以不管是在Block中使用还是附加到__block变量中,也不会像__strong修饰符或__weak修饰符那样进行处理。因此在使用附有__unsafe_unretained 修饰符的变量时,注意不要通过悬垂指针访问已被废弃的对象。

                                    因为并没有设定**__autoreleasing 修饰符与Block同时使用的方法**,所以没必要使用 autoreleasing 修饰符。另外,它与__block 说明符同时使用时会产生编译错误。

                                    __block id __autoreleasing obj = [[NSObject alloc] init];
                                    

                                    变量obj同时指定了__autoreleasing 修饰符和__block 说明符,这会引起编译错误:

                                    error: block variables cannot have autoreleasing ownership
                                    

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第33张

                                    7.Block循环引用

                                    如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈上复制到堆上时,该对象为Block所持有。这样容易引起循环引用。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第34张

                                    该源代码中的MyObject类的dealloc实例方法一定没有调用。

                                    MyObject类对象的Block类型成员变量blk_持有赋值为Block的强引用。即MyObjectl类对象持有Block。init实例方法中执行的Block语法使用附有__strong修饰符的id类型变量self。由于Block语法赋值在了成员变量blk_中(2.3.6讲到),因此通过Block语法生成在栈上的Block此时由栈上复制到堆上,并持有所使用的self。self持有Block,Block持有self。这正是循环引用。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第35张

                                    编译器在编译该代码时能够查出循环引用,因此编译器能正确地进行警告。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第36张

                                    为了避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第37张

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第38张

                                    在该源代码中,由于block的存在,持有该block的MyObject类对象即赋值在变量tmp中的self必定存在,因此不需要判断变量tmp的值是否为nil.

                                    在面向iOS4,Snow Leopard 的应用程序中,必须使用__unsafe_unretained 修饰符代替__weak 修饰符。在此源代码中也可使用__unsafe_unretained 修饰符,且不必担心悬垂指针。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第39张

                                    另外,Blcok中没有使用self同样截获了self,引起了循环引用。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第40张

                                    通过编译器给出的警告可知原因。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第41张

                                    即Block语法中使用的obj_实际上截获了self。

                                    obj_只不过是对象用结构体的成员变量。

                                    blk_ = ^{
                                    	NSLog(@"obj_ = %@", self->obj_);
                                    };
                                    

                                    该源代码也基本与前面一样,声明附有__weak修饰符的变量并赋值obj_使用来避免循环引用。在此源代码中也可安全地使用__unsafe_unretained 修饰符,原因同上。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第42张

                                    在为避免循环引用而使用修饰符时,虽说可以确定使用__weak修饰符的变量是否为nil,但更有必要使之生存以使用赋值给附有__weak修饰符变量的对象。

                                    另外,还可以使用__block变量来避免循环引用。

                                    O C高级编程 第二章, Blocks,O Blocks 详解 第43张

                                    该源代码没有引起循环引用。但是如果不调用execBlock实例方法,即不执行赋值给成员变量blk_的 Block,便会循环引用并引起内存泄漏。在生成并持有MyObject 类对象的状态下会引起以下循环引用。

                                    • MyObject类对象持有Block
                                    • Block持用__block对象
                                    • __block变量持有MyObject类对象

                                      O C高级编程 第二章, Blocks,O Blocks 详解 第44张

                                      如果不执行exeBlock实例方法,就会持续该循环从而造成循环引用。

                                      通过执行exeBlock实例方法,Block被实行,nil被赋值在_block变量tmp中;因此__block变量tmp对MyObject类对象的强引用失效。

                                      避免循环引用如下图所示;

                                      • MyObject类对象持有Block
                                      • Block持用__block对象

                                        O C高级编程 第二章, Blocks,O Blocks 详解 第45张

                                        下面我们对使用block变量避免循环引用的方法和使用__weak修饰符及__unsafe_unretained 修饰符避免循环引用的方法做个比较。

                                        使用__block变量的优点如下:

                                        • 通过__block变量可以控制对象的持有期间
                                        • 在不能使用__weak修饰符的环境下使用__unsafe_unretained修饰符即可(不必担心悬垂指针)

                                          在执行Block时可以动态地决定是否将nil或者其他对象赋值在__block变量中

                                          使用__block变量的缺点如下:

                                          • 未避免循环引用必须执行Block

                                            存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用__block变量、__weak 修饰符或__unsafe_unretained修饰符来避免循环引用。

                                            8.copy/release

                                            ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放复制的Block。这时我们用copy实例方法用来复制,用release实例方法来释放。

                                            void (^blk_on_heap)(void) = [blk_on_stack copy];
                                            
                                            [blk_on_heap release];
                                            

                                            只要 Block 有一次复制并配置在堆上,就可通过retain实例方法持有。

                                            [blk_on_heap retain];
                                            

                                            但是对于配置在栈上的Block 调用retain实例方法则不起任何作用。

                                            [blk_on_stack retain];
                                            

                                            该源代码中,虽然对赋值给blk_on_stack的栈上的Block调用了retain 实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block。

                                            另外,由于Blocks是C语言的扩展,所以在C语言中也可以使用Block语法。此时使用“Block_copy 函数”和“Block_release 函数”代替copy/release 实例方法。使用方法以及引用计数的思考方式与Objective-C 中的copy/release 实例方法相同。

                                            void (^blk_on_heap)(void) = Block_copy(blk_on_stack);
                                            
                                            Block_release(blk_on_heap);
                                            

                                            另外ARC无效时,__block说明符被用来避免BLock中的循环引用。这是由于当Block从栈上复制到堆上,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有block说明符的id类型或对象类型的自动变量,则被 retain。

                                            例如下面的源代码中,不管ARC有效无效都会引起循环引用,Block 持有 self,且 self持有 Block。

                                            O C高级编程 第二章, Blocks,O Blocks 详解 第46张

                                            使用__block变量来避免该问题

                                            O C高级编程 第二章, Blocks,O Blocks 详解 第47张

                                            正好在ARC有效时能够同__unsafe_unretained修饰符一样来使用。由于ARC 有效时和无效时__block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码是在ARC效情况下编译还是在ARC无效情况下编译。

                                            了解block的参考博客

                                            iOS开发 - OC - block的详解 - 基础篇 - BennyLoo - 博客园

                                            https://www.cnblogs.com/FBiOSBlog/p/6667371.html

                                            iOS开发 - OC - block的详解 - 深入篇 - BennyLoo - 博客园

                                            https://www.cnblogs.com/FBiOSBlog/p/6667435.html


0
收藏0
文章版权声明:除非注明,否则均为VPS857原创文章,转载或复制请以超链接形式并注明出处。

相关阅读

  • 【研发日记】Matlab/Simulink自动生成代码(二)——五种选择结构实现方法,Matlab/Simulink自动生成代码的五种选择结构实现方法(二),Matlab/Simulink自动生成代码的五种选择结构实现方法详解(二)
  • 超级好用的C++实用库之跨平台实用方法,跨平台实用方法的C++实用库超好用指南,C++跨平台实用库使用指南,超好用实用方法集合,C++跨平台实用库超好用指南,方法与技巧集合
  • 【动态规划】斐波那契数列模型(C++),斐波那契数列模型(C++实现与动态规划解析),斐波那契数列模型解析与C++实现(动态规划)
  • 【C++】,string类底层的模拟实现,C++中string类的模拟底层实现探究
  • uniapp 小程序实现微信授权登录(前端和后端),Uniapp小程序实现微信授权登录全流程(前端后端全攻略),Uniapp小程序微信授权登录全流程攻略,前端后端全指南
  • Vue脚手架的安装(保姆级教程),Vue脚手架保姆级安装教程,Vue脚手架保姆级安装指南,Vue脚手架保姆级安装指南,从零开始教你如何安装Vue脚手架
  • 如何在树莓派 Raspberry Pi中本地部署一个web站点并实现无公网IP远程访问,树莓派上本地部署Web站点及无公网IP远程访问指南,树莓派部署Web站点及无公网IP远程访问指南,本地部署与远程访问实践,树莓派部署Web站点及无公网IP远程访问实践指南,树莓派部署Web站点及无公网IP远程访问实践指南,本地部署与远程访问详解,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南,树莓派部署Web站点及无公网IP远程访问实践详解,本地部署与远程访问指南。
  • vue2技术栈实现AI问答机器人功能(流式与非流式两种接口方法),Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法探究,Vue2技术栈实现AI问答机器人功能,流式与非流式接口方法详解
  • 发表评论

    快捷回复:表情:
    评论列表 (暂无评论,0人围观)

    还没有评论,来说两句吧...

    目录[+]

    取消
    微信二维码
    微信二维码
    支付宝二维码