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修饰的局部变量之间的差异html

    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++代码看一下内部结构中两个参数的区别。ios

    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++代码查看全局变量调用方式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中捕获的仍然是实例对象,并经过实例对象经过不一样的方式去获取使用到的属性。atom

4. block的类型

1.类型分析

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

  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);

相关文章
相关标签/搜索