• iOS Block的本质(二)


    iOS Block的本质(二)

    1. 介绍引入block本质

    1. 通过上一篇文章Block的本质(一)已经基本对block的底层结构有了基本的认识,block的底层就是__main_block_impl_0
    2. 通过以下这张图展示底层各个结构体之间的关系。

    2. block的变量捕获

    • 为了保证block内部能够正常访问外部的变量,block有一个变量捕获机制。

    局部变量

    1. auto变量

      • Block的本质(一)我们已经了解过block对age变量的捕获。
      • auto自动变量,离开作用域就销毁,局部变量前面自动添加auto关键字。自动变量会捕获到block内部,也就是说block内部会专门新增加一个参数来存储变量的值。
      • auto只存在于局部变量中,访问方式为值传递,通过上述对age参数的解释我们也可以确定确实是值传递。
    2. static变量

      • static 修饰的变量为指针传递,同样会被block捕获。
    3. 分析aotu修饰的局部变量和static修饰的局部变量之间的差别

      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              auto int a = 10;
              static int b = 10;
              void(^block)(void) = ^{
                 NSLog(@"age is %d, height is %d", a, b);
              };
              a = 1;
              b = 2;
              block();
          }
          return 0;
      }
      // log : 信息--> age = 10, height = 2
      // block中a的值没有被改变而b的值随外部变化而变化。
      
    4. 重新生成c++代码看一下内部结构中两个参数的区别。

      struct __main_block_impl_0 {
          struct __block_impl impl;
          struct __main_block_desc_0* Desc;
          int a;	// a 为值
          int *b;	// b 为指针
          __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
              impl.isa = &_NSConcremainackBlock;
              impl.Flags = flags;
              impl.FuncPtr = fp;
              Desc = desc;
          }
      };
      static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
          int a = __cself->a; // bound by copy
          int *b = __cself->b; // bound by copy
      
          NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
      }
      
      static struct __main_block_desc_0 {
          size_t reserved;
          size_t Block_size;
      } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
      
    5. 从上述源码中可以看出,a,b两个变量都有捕获到block内部。但是a传入的是值,而b传入的则是地址。

    6. 为什么两种变量会有这种差异呢,因为自动变量可能会销毁,block在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递了。而静态变量不会被销毁,所以完全可以传递地址。而因为传递的是值得地址,所以在block调用之前修改地址中保存的值,block中的地址是不会变得。所以值会随之改变。

    7. 全局变量

      • 我们同样以代码的方式看一下block是否捕获全局变量
      int age_ = 10;
      static int height_ = 10;
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              void(^block)(void) = ^{
                  NSLog(@"age is %d, height is %d", age_, height_);
              };
              age_ = 1;
              height_ = 2;
              block();
          }
          return 0;
      }
      // log 信息--> age = 1, height = 2
      
    8. 同样生成c++代码查看全局变量调用方式

      int age_ = 10;
      static int height_ = 10;
      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 = &_NSConcremainackBlock;
              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_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, age_, height_);
      }
      
      static struct __main_block_desc_0 {
          size_t reserved;
          size_t Block_size;
      } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
      
    9. 通过上述代码可以发现,__main_block_imp_0并没有添加任何变量,因此block不需要捕获全局变量,因为全局变量无论在哪里都可以访问。

      • 局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。
    10. block的变量总结

      • 总结:局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获

    3. 变量捕获拓展

    1. 以下Persion类代码中block变量分析

      @interface Person : NSObject
      @property (copy, nonatomic) NSString *name;
      
      - (void)test;
      
      - (instancetype)initWithName:(NSString *)name;
      @end
      
      #import "Person.h"
      @implementation Person
      int age_ = 10;
      - (void)test
      {
          void (^block)(void) = ^{
              NSLog(@"-------%d", [self name]);
          };
          block();
      }
      
      - (instancetype)initWithName:(NSString *)name
      {
          if (self = [super init]) {
              self.name = name;
          }
          return self;
      }
      @end
      
    2. 同样转化为c++代码查看其内部结构

      
      int age_ = 10;
      struct __Person__test_block_impl_0 {
        struct __block_impl impl;
        struct __Person__test_block_desc_0* Desc;
        Person *self;
        __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
          impl.isa = &_NSConcreteStackBlock;
          impl.Flags = flags;
          impl.FuncPtr = fp;
          Desc = desc;
        }
      };
      static void __Person__test_block_func_0(struct __Person__test_block_impl_0 *__cself) {
        Person *self = __cself->self; // bound by copy
      
              NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_Person_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
          }
      static void __Person__test_block_copy_0(struct __Person__test_block_impl_0*dst, struct __Person__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
      
      static void __Person__test_block_dispose_0(struct __Person__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}
      
      static struct __Person__test_block_desc_0 {
        size_t reserved;
        size_t Block_size;
        void (*copy)(struct __Person__test_block_impl_0*, struct __Person__test_block_impl_0*);
        void (*dispose)(struct __Person__test_block_impl_0*);
      } __Person__test_block_desc_0_DATA = { 0, sizeof(struct __Person__test_block_impl_0), __Person__test_block_copy_0, __Person__test_block_dispose_0};
      
      static void _I_Person_test(Person * self, SEL _cmd) {
          void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
          ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
      }
      
      
      static instancetype _I_Person_initWithName_(Person * self, SEL _cmd, NSString *name) {
          if (self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"))) {
              ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
          }
          return self;
      }
      
      static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
      extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
      
      static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }
      // @end
      
      struct _prop_t {
          const char *name;
         	    const char *attributes;
      };
      
    3. 可以发现,self同样被block捕获,接着我们找到test方法可以发现,test方法默认传递了两个参数self和_cmd。

    4. 同理得,类方法也同样默认传递了类对象self和方法选择器_cmd。

    5. 不论对象方法还是类方法都会默认将self作为参数传递给方法内部,既然是作为参数传入,那么self肯定是局部变量。上面讲到局部变量肯定会被block捕获。

    6. 在block内部使用name成员变量或者调用实例的属性

      - (void)test
      {
          void(^block)(void) = ^{
              NSLog(@"%@",self.name);
              NSLog(@"%@",_name);
          };
          block();
      }
      

    7. 得到结论:在block中使用的是实例对象的属性,block中捕获的仍然是实例对象,并通过实例对象通过不同的方式去获取使用到的属性。

    4. block的类型

    1.类型分析

    1. 通过源码分析得到,block中的isa指针指向的是_NSConcreteStackBlock类对象地址。那么block是否就是_NSConcreteStackBlock类型的呢?

    2. 我们通过代码用class方法或者isa指针查看具体类型。

      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
              void (^block)(void) = ^{
                  NSLog(@"Hello");
              };
      
              NSLog(@"%@", [block class]);
              NSLog(@"%@", [[block class] superclass]);
              NSLog(@"%@", [[[block class] superclass] superclass]);
              NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
          }
          return 0;
      }
      // log 打印结果  __NSGlobalBlock__
      // log 打印结果  __NSGlobalBlock
      // log 打印结果  NSBlock
      // log 打印结果  NSObjcet
      
    3. 从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

    2.类型分类

    1. block有3中类型

      • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
      • __NSStackBlock__ ( _NSConcreteStackBlock )
      • __NSMallocBlock__ ( _NSConcreteMallocBlock )
    2. 通过代码查看一下block在什么情况下其类型会各不相同

      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // 1. 内部没有调用外部变量的block
              void (^block1)(void) = ^{
              };
              // 2. 内部调用外部变量的block
              int a = 10;
              void (^block2)(void) = ^{
                  NSLog(@"log :%d",a);
              };
             // 3. 直接调用的block的class
              NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
                  NSLog(@"%d",a);
              } class]);
          }
          return 0;
      }
      // 最后一行 Log :打印结果 __NSGlobalBlock__, __NSStackBlock__ ,__NSMallocBlock__
      
    3. 上述代码转化为c++代码查看源码时却发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。

    4. 我们可以推测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

    5. block在内存中的存储

    1. 通过下面一张图看一下不同block的存放区域

    2. 上图中可以发现,根据block的类型不同,block存放在不同的区域中。
      数据段中的__NSGlobalBlock__直到程序结束才会被回收,不过我们很少使用到__NSGlobalBlock__类型的block,因为这样使用block并没有什么意义。

    3. __NSStackBlock__类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。

    4. __NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

    5. block是如何定义其类型

    6. 接着我们使用代码验证上述问题,首先关闭ARC回到MRC环境下,因为ARC会帮助我们做很多事情,可能会影响我们的观察。

      // MRC环境!!!
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // Global:没有访问auto变量:__NSGlobalBlock__
              void (^block1)(void) = ^{
                  NSLog(@"block1---------");
              };
              // Stack:访问了auto变量: __NSStackBlock__
              int a = 10;
              void (^block2)(void) = ^{
                  NSLog(@"block2---------%d", a);
              };
              NSLog(@"%@ %@", [block1 class], [block2 class]);
              // __NSStackBlock__调用copy : __NSMallocBlock__
              NSLog(@"%@", [[block2 copy] class]);
          }
          return 0;
      }
      // Log 打印信息 --> __NSGlobalBlock__ ,__NSStackBlock__ ,__NSMallocBlock__
      
    7. 通过打印的内容可以验证上图中所示的正确性。

      • 没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。
      • 访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。
      • __NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。
    8. 上面提到过__NSGlobalBlock__类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。

    9. 但是__NSStackBlock__访问了aotu变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会发生问题,通过下面代码可以证实这个问题。MRC 环境下的。

      void (^block)(void);
      void test()
      {
          // __NSStackBlock__
          int a = 10;
          block = ^{
              NSLog(@"block---------%d", a);
          };
      }
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              test();
              block();
          }
          return 0;
      }
      // Log 打印信息 :MRC 环境下 : block---------272632424
      // Log 打印信息 :ARC 环境下 :  block---------10
      
      • 如果执行copy操作打印结果为10
      void (^block)(void);
      void test()
      {
          // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
          int age = 10;
          block = [^{
              NSLog(@"block---------%d", age);
          } copy];
          [block release];
      }
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // insert code here...
              test();
          
              block();
      //     Log 打印信息 : block---------10
          }
          return 0;
      }
      
    10. 可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?因为上述代码中创建的block是__NSStackBlock__类型的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。查看其c++代码可以更清楚的理解。

    11. 为了避免这种情况发生,可以通过copy将__NSStackBlock__类型的block转化为__NSMallocBlock__类型的block,将block存储在堆中,以下是修改后的代码。

      void (^block)(void);
      void test()
      {
          // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
          int age = 10;
          block = [^{
              NSLog(@"block---------%d", age);
          } copy];
          [block release];
      }
      // Log 打印信息 : block---------10
      
    12. 那么其他类型的block调用copy会改变block类型吗?下面表格已经展示的很清晰了。

    13. 所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下回系统会自动copy,是block不会被销毁。

    6. ARC环境下的block

    • 在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。

    • 会自动将block进行一次copy操作的情况。

    1. block作为函数返回值时

      typedef void (^Block)(void);
      Block myblock()
      {
          int a = 10;
          // 上文提到过,block中访问了auto变量,此时block类型应为__NSStackBlock__
          Block block = ^{
              NSLog(@"---------%d", a);
          };
          return block;
      }
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              Block block = myblock();
              block();
             // 打印block类型为 __NSMallocBlock__
              NSLog(@"%@",[block class]);
          }
          return 0;
      }
      Log  打印信息 :---------10
      Log  打印信息 :__NSMallocBlock__
      
      • 上文提到过,如果在block中访问了auto变量时,block的类型为__NSStackBlock__,上面打印内容发现blcok为__NSMallocBlock__类型的,并且可以正常打印出a的值,说明block内存并没有被销毁。
      • 上面提到过,block进行copy操作会转化为__NSMallocBlock__类型,来讲block复制到堆中,那么说明RAC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。
    2. 将block赋值给__strong指针时

      • block被强指针引用时,ARC也会自动对block进行一次copy操作。
       int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // block内没有访问auto变量
              Block block = ^{
                  NSLog(@"block---------");
              };
              NSLog(@"%@",[block class]);
              int a = 10;
              // block内访问了auto变量,但没有赋值给__strong指针
              NSLog(@"%@",[^{
                  NSLog(@"block1---------%d", a);
              } class]);
              // block赋值给__strong指针
              Block block2 = ^{
                NSLog(@"block2---------%d", a);
              };
              NSLog(@"%@",[block1 class]);
          }
          return 0;
      }
      Log  打印信息 :__NSGlobalBlock__
      Log  打印信息 :__NSStackBlock__
      Log  打印信息 :__NSMallocBlock__
      
    3. block作为Cocoa API中方法名含有usingBlock的方法参数时

      • 例如:遍历数组的block方法,将block作为参数的时候。
      NSArray *array = @[];
      [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
      
      }];
      
    4. block作为GCD API的方法参数时

      • 例如:GDC的一次性函数或延迟执行的函数,执行完block操作之后系统才会对block进行release操作。
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
      
      });        
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      
      });
      

    7. block声明写法

    • 通过上面对MRC及ARC环境下block的不同类型的分析,总结出不同环境下block属性建议写法。
    1. MRC下block属性的建议写法
      @property (copy, nonatomic) void (^block)(void);

    2. ARC下block属性的建议写法
      @property (strong, nonatomic) void (^block)(void);
      @property (copy, nonatomic) void (^block)(void);

  • 相关阅读:
    宁波工程学院2020新生校赛C
    宁波工程学院2020新生校赛B
    宁波工程学院2020新生校赛A -恭喜小梁成为了宝可梦训练家~(水题)
    POJ 1611
    牛客算法周周练11E
    牛客算法周周练11C
    牛客算法周周练11A
    CodeForces 1176C
    CodeForces 445B
    UVALive 3027
  • 原文地址:https://www.cnblogs.com/ShaoYinling/p/10246096.html
Copyright © 2020-2023  润新知