美文网首页
探寻block的本质

探寻block的本质

作者: 叫我小黑 | 来源:发表于2019-06-27 17:46 被阅读0次

    先对block有一个基本的认识
    block本质上也是一个oc对象,他内部也有一个isa指针。block是封装了函数调用以及函数调用环境的OC对象。

    block的本质

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block)(void) =  ^{
                NSLog(@"this is a block!");
            };
            block();
        }
        return 0;
    }
    

    通过命令行将代码转换成 c++ 代码

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

    main 函数转化成的 C++ 代码如下

    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    将c++中block的声明和调用分别取出来查看其内部实现。

    定义block变量

    void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    

    可以发现,block定义中调用了__main_block_impl_0函数,并且将__main_block_impl_0函数的地址赋值给了 block。那么我们来看一下__main_block_impl_0函数内部结构。

    __main_block_imp_0结构体

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

    __main_block_imp_0结构体内有一个同名构造函数__main_block_imp_0,构造函数中对一些变量进行了赋值最终会返回一个结构体。也就是说最终将一个__main_block_imp_0结构体的地址赋值给了block变量
    __main_block_impl_0结构体内可以发现__main_block_impl_0构造函数中传入了三个参数。(void *)__main_block_func_0&__main_block_desc_0_DATAflags。其中flags有默认值 0,也就说flags参数在调用的时候可以省略不传。

    __main_block_func_0

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
          NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_958c22_mi_0);
    }
    

    发现代码是一个 NSLog 打印,恰恰是我们在block块中写下的代码。
    那么 __main_block_func_0 函数中其实存储着我们 block 中写下的代码。而__main_block_impl_0函数中传入的是(void *)__main_block_func_0,也就说将我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。

    &__main_block_desc_0_DATA

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

    可以看到__main_block_desc_0中存储着两个参数,reservedBlock_size,并且 reserved 赋值为0,而 Block_size 则存储着__main_block_impl_0占用空间大小。最终将__main_block_desc_0结构体的地址传入__main_block_func_0中赋值给Desc。

    回过头来查看__main_block_impl_0结构体

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

    第一个变量就是__block_impl结构体

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    

    可以发现__block_impl结构体内部就有一个isa指针,因此可以证明block本质上就是一个oc对象。而在构造函数中将函数中传入的值分别存储在__main_block_impl_0结构体实例中,最终将结构体的地址赋值给block。

    通过上面对__main_block_impl_0结构体构造函数的参数的分析可得结论:

    • __block_impl 结构体中isa指针存储着 &_NSConcreteStackBlock 地址,可以暂时理解为其类对象地址,block就是 _NSConcreteStackBlock 类型的。
    • block 代码块中的代码被封装成 __main_block_func_0 函数,FuncPtr 则存储着 __main_block_func_0 函数的地址。
    • Desc 指向 __main_block_desc_0 结构体对象,其中存储__main_block_impl_0 结构体所占用的内存。

    调用block执行内部代码

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    

    可以发现调用 block 是通过 block 找到 FunPtr 直接调用,通过上面分析我们知道block指向的是__main_block_impl_0 类型结构体,但是我们发现 __main_block_impl_0 结构体中并不直接就可以找到FunPtr,而FunPtr 是存储在 __block_impl 中的,为什么block可以直接调用__block_impl 中的 FunPtr 呢?

    重新查看上述源代码可以发现,(__block_impl *)blockblock 强制转化为 __block_impl 类型的,因为 __block_impl__main_block_impl_0结构体的第一个成员,那么 __block_impl 的内存地址就是 __main_block_impl_0结构体的内存地址。所以可以转化成功,并找到 FunPtr 成员。
    FunPtr 中存储着通过代码块封装的函数地址,那么调用此函数,也就是会执行代码块中的代码。

    验证block的本质 __main_block_impl_0 结构体类型。

    struct __main_block_desc_0 {
        size_t reserved;
        size_t Block_size;
    };
    
    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;
    };
    
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0* Desc;
        int age;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 20;
            void (^block)(void) =  ^{
                NSLog(@"this is a block! -- %d", age);
            };
            
            struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
            
            block();
        }
        return 0;
    }
    

    通过打断点查看自定义的结构体是否被赋值成功,以及里面的值。


    __main_block_impl_0

    接下来断点来到block代码块中,看一下堆栈信息中的函数调用地址。Debuf workflow -> always show Disassembly


    image.png

    可以看到地址确实和FuncPtr中的代码块地址一样。

    block底层的数据结构可以通过一张图来展示


    block底层的数据结构

    block的变量捕获

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

    局部变量

    auto变量
    auto自动变量,离开作用域就销毁,局部变量前面自动添加auto关键字。自动变量会捕获到block内部,也就是说block内部会专门新增加一个参数来存储变量的值。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 20;
            void (^block)(void) =  ^{
                NSLog(@"this is a block! -- %d", age);
            }; 
            block();
        }
        return 0;
    }
    

    通过命令行将代码转换成 c++ 代码

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

    查看生成的C++代码

    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_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_933af9_mi_0, age);
    }
    
    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)};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            int age = 20;
            void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    

    由代码可知捕获auto变量确实是值传递。

    static变量
    static 修饰的变量为指针传递,同样会被block捕获。
    分别添加aotu修饰的局部变量和static修饰的局部变量,通过源码来看一下他们之间的差别

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            static int height = 10;
            int age = 20;
            void (^block)(void) =  ^{
                NSLog(@"this is a block! -- %d --%d",height, age);
            };
            
    //        struct __main_block_impl_0 *blockStruct = (__bridge struct __main_block_impl_0 *)block;
            
            block();
        }
        return 0;
    }
    

    通过命令行将代码转换成 c++ 代码

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

    查看生成的C++代码

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *height;
      int age;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_height, int _age, int flags=0) : height(_height), 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 *height = __cself->height; // bound by copy
      int age = __cself->age; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_11af52_mi_0,(*height), age);
     }
    
    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)};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            static int height = 10;
            int age = 20;
            void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &height, age));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    

    从上述源码中可以看出,height, age 两个变量都有捕获到block内部。但是age传入的是值,而height传入的则是地址。

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

    全局变量

    int width = 10;
    static int length = 11;
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            static int height = 10;
            int age = 20;
            void (^block)(void) =  ^{
                NSLog(@"this is a block! -- %d --%d  -- %d --%d",height, age, width, length);
            };
            block();
        }
        return 0;
    }
    

    通过命令行将代码转换成 c++ 代码

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

    查看生成的C++代码

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      int *height;
      int age;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_height, int _age, int flags=0) : height(_height), 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 *height = __cself->height; // bound by copy
      int age = __cself->age; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_4a2f6a_mi_0,(*height), age, width, length);
     }
    
    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)};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            static int height = 10;
            int age = 20;
            void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &height, age));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
    

    通过上述代码可以发现,__main_block_imp_0 并没有添加全局变量 width 和 length,因此block不需要捕获全局变量,因为全局变量无论在哪里都可以访问。

    最后做一个总结


    block 变量捕获

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

    思考:以下代码中block是否会捕获变量呢?

    @interface Person : NSObject
    @property(nonatomic, copy) NSString *name;
    - (void)test;
    @end
    
    @implementation Person
    - (void)test
    {
        void(^block)(void) = ^{
            NSLog(@"%@",self);
        };
        block();
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.name = @"jack";
            [person test];
        }
        return 0;
    }
    

    转化为c++代码查看其内部结构

    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_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_a3f23a_mi_0,self);
    }
    
    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);
    }
    

    可以发现,self同样被block捕获,test方法默认传递了两个参数self和_cmd,那么self肯定是局部变量。上面讲到局部变量肯定会被block捕获。

    如果 test 方法中代码如下

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

    转化为c++代码查看其内部结构

    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_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_0d2dd0_mi_0,((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_0d2dd0_mi_1,(*(NSString **)((char *)self + OBJC_IVAR_$_Person$_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);
    }
    

    可以发现,即使block中使用的是实例对象的属性,block中捕获的仍然是实例对象,self.name 调用了 get 方法 objc_msgSend)(self, sel_registerName("name")),而 _name 直接通过地址获取 (char *)self + OBJC_IVAR_$_Person$_name

    block的类型

    block有3种类型:
    __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    __NSStackBlock__ ( _NSConcreteStackBlock )
    __NSMallocBlock__ ( _NSConcreteMallocBlock )

    可以通过调用class方法或者isa指针查看具体类型

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            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;
    }
    

    打印内容


    打印内容

    从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

    通过代码查看一下block在什么情况下其类型会各不相同

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block1)(void) = ^{
                NSLog(@"Hello");
            };
            
            int age = 10;
            void (^block2)(void) = ^{
                NSLog(@"Hello - %d", age);
            };
            
            NSLog(@"\n %@ \n %@ \n %@", [block1 class], [block2 class], [^{
                NSLog(@"%d", age);
            } class]);
        }
        return 0;
    }
    

    打印结果


    image.png

    将代码转化成 C++ 代码

    // __main_block_impl_0
    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;
      }
    };
    
    // __main_block_impl_1
    struct __main_block_impl_1 {
      struct __block_impl impl;
      struct __main_block_desc_1* Desc;
      int age;
      __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    // __main_block_impl_2
    struct __main_block_impl_2 {
      struct __block_impl impl;
      struct __main_block_desc_2* Desc;
      int age;
      __main_block_impl_2(void *fp, struct __main_block_desc_2 *desc, int _age, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            void (*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
            int age = 10;
            void (*block2)(void) = ((void (*)())&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA, age));
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_aeebac_mi_4, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block1, sel_registerName("class")), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block2, sel_registerName("class")), ((Class (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)())&__main_block_impl_2((void *)__main_block_func_2, &__main_block_desc_2_DATA, age)), sel_registerName("class")));
        }
        return 0;
    }
    

    查看转化的c++代码发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。
    猜测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

    block在内存中的存储

    block在内存中的存储

    上图中可以发现,根据block的类型不同,block存放在不同的区域中。
    数据段中的NSGlobalBlock直到程序结束才会被回收,很少使用到NSGlobalBlock类型的block,因为这样使用block并没有什么意义。
    NSStackBlock类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。
    NSMallocBlock是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

    block是如何定义其类型
    我们先关闭ARC回到MRC环境下

    ARC -> MRC
    看一下如下代码的打印结果如何
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            void (^block1)(void) = ^{
                NSLog(@"Hello");
            };
            
            int age = 10;
            void (^block2)(void) = ^{
                NSLog(@"Hello - %d", age);
            };
            
            NSLog(@"\n %@ \n %@ \n %@", [block1 class], [block2 class], [[block2 copy] class]);
        }
        return 0;
    }
    

    打印结果

    打印结果
    通过打印的内容可以发现正如下图中所示。
    image.png
    没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。
    访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。
    __NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。
    上面提到过__NSGlobalBlock__类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。
    但是__NSStackBlock__访问了auto变量,并且是存放在栈中的。

    栈中的代码在作用域结束之后内存就会被销毁,那么很有可能block内存销毁之后才去调用它,那样就会发生问题

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

    打印结果

    image.png
    可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?因为上述代码中创建的block是NSStackBlock类型的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。
    为了避免这种情况发生,可以通过copy将NSStackBlock类型的block转化为NSMallocBlock类型的block,将block存储在堆中,以下是修改后的代码。
    void (^block)(void);
    void test()
    {
        // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
        int age = 10;
        block = [^{
            NSLog(@"block---------%d", age);
        } copy];
    }
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            test();
            block();
            [block release];
        }
        return 0;
    }
    

    此时在打印就会发现数据正确


    打印结果

    每一种类型的block调用copy后的结果如下所示

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

    ARC 帮我们做了什么?

    在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况
    block作为函数返回值时

    typedef void(^Block)(void);
    Block myBlock()
    {
        // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
        int age = 10;
        Block block = ^{
            NSLog(@"block---------%d", age);
        };
        return block;
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block = myBlock();
            block();
        }
        return 0;
    }
    

    打印内容:

    打印内容
    在block中访问了auto变量时,block的类型为__NSStackBlock__,上面打印内容发现blcok为__NSMallocBlock__类型的,并且可以正常打印出a的值,说明block内存并没有被销毁,被复制到堆中,说明RAC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作。

    将block赋值给__strong指针时

    typedef void(^Block)(void);
    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(@"%@",[block2 class]);
        }
        return 0;
    }
    
    打印结果

    从打印内容可以看出,当block被赋值给__strong指针时,RAC会自动进行一次copy操作。

    block作为Cocoa API中方法名含有usingBlock的方法参数时

    NSArray *array = @[];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                
    }];
    

    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(), ^{
                
    });
    
    blockblock属性的建议写法

    MRC下block属性的建议写法

    @property (copy, nonatomic) void (^block)(void);
    

    ARC下block属性的建议写法

    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

    block对对象变量的捕获

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                Person *person = [[Person alloc] init];
                person.name = @"jack";
                
                block = ^{
                    NSLog(@"------block内部%@",person.name);
                };
            } // 执行完毕,person没有被释放
            NSLog(@"--------");
        }
        return 0;
    }
    

    上面大括号的代码块执行完毕之后,person 没有被释放,person为auto变量,将会被 block 捕获,即block有一个强引用引用person,所以block不被销毁的话,person也不会销毁。

    查看对应的 C++ 代码可发现 block 持有 person 的引用

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Person *person;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_person, int flags=0) : person(_person) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    将代码的运行环境转为 MRC

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                Person *person = [[Person alloc] init];
                person.name = @"jack";
                
                block = ^{
                    NSLog(@"------block内部%@",person.name);
                };
                [person release];
            } // 执行完毕,person没有被释放
            NSLog(@"--------");
        }
        return 0;
    }
    

    运行发现大括号的代码块执行完毕之后,person 就被释放,这是因为MRC环境下block在栈空间,栈空间对外面的person不会进行强引用。
    block调用copy操作之后,person不会被释放

    block = [^{
          NSLog(@"------block内部%@",person.name);
     } copy];
    

    只对栈空间的block进行一次copy操作,将栈空间的block拷贝到堆中,person就不会被释放,说明堆空间的block可能会对person进行一次retain操作,以保证person不会被销毁。堆空间的block自己销毁之后也会对持有的对象进行release操作。
    也就是说栈空间上的block不会对对象强引用,堆空间的block有能力持有外部调用的对象,即对对象进行强引用或去除强引用的操作。

    __weak
    __weak添加之后,person在大括号代码块执行完毕之后就被销毁了。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Block block;
            {
                Person *person = [[Person alloc] init];
                person.name = @"jack";
                
                __weak typeof(person) weakPerson = person;
                block = ^{
                    NSLog(@"------block内部%@",weakPerson.name);
                };
            } // 执行完毕,person没有被释放
            NSLog(@"--------");
        }
        return 0;
    }
    

    将代码转化为c++来看一下,__weak修饰变量,需要告知编译器使用ARC环境及版本号否则会报错,添加说明-fobjc-arc -fobjc-runtime=ios-8.0.0

    xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      Person *__weak weakPerson;
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    

    __weak修饰的变量,在生成的__main_block_impl_0中也是用__weak修饰的。

    __main_block_copy_0 和 __main_block_dispose_0
    当block中捕获对象类型的变量时,我们发现block结构体__main_block_impl_0的描述结构体__main_block_desc_0中多了两个参数copydispose函数,查看源码:

    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    

    发现 copy 和 dispose 函数中传入的都是 __main_block_impl_0 结构体本身。
    copy 本质就是 __main_block_copy_0函数, __main_block_copy_0函数内部调用 _Block_object_assign函数,_Block_object_assign 中传入的是 person 对象的地址,person 对象,以及3。
    dispose 本质就是 __main_block_dispose_0 函数, __main_block_dispose_0 函数内部调用 _Block_object_dispose 函数,_Block_object_dispose 函数传入的参数是 person 对象,以及3。

    _Block_object_assign函数调用时机及作用
    当block进行copy操作的时候就会自动调用 __main_block_desc_0 内部的 __main_block_copy_0 函数, __main_block_copy_0 函数内部会调用 _Block_object_assign 函数。
    _Block_object_assign 函数会自动根据 __main_block_impl_0 结构体内部的person是什么类型的指针,对 person 对象产生强引用或者弱引用。可以理解为 _Block_object_assign 函数内部会对person 进行引用计数器的操作,如果 __main_block_impl_0 结构体内 person 指针是 __strong 类型,则为强引用,引用计数+1,如果是 __weak 类型,则为弱引用,引用计数不变。

    _Block_object_dispose函数调用时机及作用
    当block从堆中移除时就会自动调用 __main_block_desc_0 中的 __main_block_dispose_0 函数,__main_block_dispose_0 函数内部会调用 _Block_object_dispose 函数。
    _Block_object_dispose 会对 person 对象做释放操作,类似于release,也就是断开对 person 对象的引用。

    总结

    • 当block内部访问了对象类型的auto变量时,如果block是在栈上,将不会对auto变量产生强引用
    • 如果block被拷贝到堆上,会调用block内部的copy函数,copy函数内部会调用 _Block_object_assign 函,_Block_object_assign 函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
    • 如果block从堆上移除,会调用block内部的dispose函数,dispose函数内部会调用 _Block_object_dispose 函数
      _Block_object_dispose 函数会自动释放引用的auto变量(release)
    • 用图总结一下后两条


      copy 和 dispose 调用的时机

    block内修改变量的值

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int age = 10;
            Block block = ^ {
                // age = 20; // 无法修改
                NSLog(@"%d",age);
            };
            block();
        }
        return 0;
    }
    

    默认情况下block不能修改外部的局部变量,通过之前的源码分析可得原因如下:

    age是在 main 函数内部声明的,说明 age 的内存存在于main函数的栈空间内部,但是block内部的代码 __main_block_func_0 函数内部。__main_block_func_0 函数内部无法访问age变量的内存空间,两个函数的栈空间不一样,__main_block_func_0 内部拿到的age是block结构体内部的age,因此无法在 __main_block_func_0 函数内部去修改main函数内部的变量。

    那应该如何做才能在block内修改变量的值
    方式一:age使用static修饰
    block内部捕获到的是 static 修饰的 age 变量的指针,所以在__main_block_func_0 函数内部就可以拿到 age 变量的内存地址,因此就可以在block内部修改age的值。

    方式二:__block
    __block用于解决block内部不能修改auto变量值的问题,__block不能修饰静态变量(static) 和全局变量

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block int age = 10;
            Block block = ^ {
               age = 20; // 无法修改
                NSLog(@"%d",age);
            };
            block();
        }
        return 0;
    }
    

    查看底层 C++ 源码

    typedef void(*Block)(void);
    
    struct __Block_byref_age_0 {
      void *__isa;   //isa 指针
    __Block_byref_age_0 *__forwarding; //存储结构体自己的地址
     int __flags;
     int __size; // 变量占用的空间
     int age; // age 变量
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_age_0 *age; //  __block 修饰的变量,变成了 __Block_byref_age_0 结构体指针
      __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; // 拿到 age 的结构体
      (age->__forwarding->age) = 20; // 通过结构体拿到 age 变量并赋值
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_85eda0_mi_0,(age->__forwarding->age));
    }
    
    //block copy 到堆时会调用
    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*/);}
    
    // block 销毁时会调用
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
            Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    首先被 __block 修饰的age变量声明变为名为age的__Block_byref_age_0 结构体,也就是说加上 __block 修饰的话捕获到的block内的变量为 __Block_byref_age_0 类型的结构体。

    分析 __Block_byref_age_0结构体内存储哪些元素

    • __isa指针__Block_byref_age_0 中也有isa指针也就是说__Block_byref_age_0 本质也一个对象。
    • __forwarding__forwarding__Block_byref_age_0 结构体类型的,并且__forwarding 存储的值为(__Block_byref_age_0 *)&age,即结构体自己的内存地址。
    • __flags :0
    • __size :sizeof(__Block_byref_age_0)即__Block_byref_age_0所占用的内存空间。
    • age :真正存储变量的地方,这里存储局部变量10。

    由c++源码可看到调用block时,首先取出 __main_block_impl_0中的age,通过age结构体拿到 __forwarding指针,上面提到过__forwarding中保存的就是__Block_byref_age_0结构体本身,在通过__forwarding拿到结构体中的 age 变量并修改其值。

    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      __Block_byref_age_0 *age = __cself->age; // 拿到 age 的结构体
      (age->__forwarding->age) = 20; // 通过结构体拿到 age 变量并赋值
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_85eda0_mi_0,(age->__forwarding->age));
    }
    

    NSLog 中使用 age 时也通过同样的方式获取 age 的值。

    __block修饰对象类型

    @interface Person : NSObject
    @property(nonatomic, copy) NSString *name;
    @end
    
    @implementation Person
    @end
    
    typedef void(^Block)(void);
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            Block block = ^{
                person = [[Person alloc] init];
                NSLog(@"%@",person);
            };
            block();
        }
        return 0;
    }
    

    生成c++源码查看

    struct __Block_byref_person_0 {
      void *__isa;
    __Block_byref_person_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__strong person;
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_person_0 *person; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_person_0 *_person, int flags=0) : person(_person->__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_person_0 *person = __cself->person; // bound by ref
     (person->__forwarding->person) = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_xj_ybnbwbrx1tn7s5wnhqsx7lbw0000gn_T_main_5dabb7_mi_0,(person->__forwarding->person));
    }
    static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);}
    
    static struct __main_block_desc_0 {
      size_t reserved;
      size_t Block_size;
      void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
      void (*dispose)(struct __main_block_impl_0*);
    } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
    
    int main(int argc, const char * argv[]) {
        /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
            __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {(void*)0,(__Block_byref_person_0 *)&person, 33554432, sizeof(__Block_byref_person_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
            Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_person_0 *)&person, 570425344));
            ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        }
        return 0;
    }
    

    通过源码查看,对象被包装在一个新的结构体中,结构体内部会有一个person对象,不一样的地方是结构体内部添加了内存管理的两个函数 __Block_byref_id_object_copy__Block_byref_id_object_dispose

    #define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    __Block_byref_id_object_copy__Block_byref_id_object_dispose函数的调用时机及作用在__block内存管理部分详细分析。

    思考:__block修饰的age变量在编译时会被封装为结构体,那么当在外部使用age变量的时候,使用的__Block_byref_age_0结构体呢?还是__Block_byref_age_0结构体内的age变量呢?
    答:外部使用 age 变量是使用__Block_byref_age_0结构体内的age变量,apple为了隐藏__Block_byref_age_0结构体的实现,打印age变量的地址可以发现其实是__Block_byref_age_0结构体内age变量的地址。

    __block内存管理

    当block中捕获对象类型的变量时,block中的__main_block_desc_0 结构体内部会自动添加copydispose函数对捕获的变量进行内存管理。

    当block内部捕获__block修饰的对象类型的变量时,__Block_byref_person_0结构体内部也会自动添加__Block_byref_id_object_copy__Block_byref_id_object_dispose 对被__block包装成结构体中的对象进行内存管理。
    当block内存在栈上时,并不会对__block变量产生内存管理。当blcok被copy到堆上时
    会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(相当于retain)

    首先通过一张图看一下block复制到堆上时内存变化


    block复制到堆上时内存变化

    blockcopy到堆上时,block内部引用的__block变量也会被复制到堆上,并且持有变量,如果block复制到堆上的同时,__block变量已经存在堆上了,则不会复制。

    block从堆中移除的话,就会调用dispose函数,也就是__main_block_dispose_0函数,__main_block_dispose_0函数内部会调用_Block_object_dispose函数,会自动释放引用的__block变量。

    block从堆中移除时内存变化

    block内部决定什么时候将变量复制到堆中,什么时候对变量做引用计数的操作。

    __block修饰的变量在 block 结构体中一直都是强引用,而其他类型的是由传入的对象指针类型决定。

    接下来我们看一下 __block 修饰的变量生成的结构体有什么不同?

    struct __Block_byref_person_0 {
      void *__isa;
    __Block_byref_person_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__strong person;
    };
    struct __Block_byref_age_1 {
      void *__isa;
    __Block_byref_age_1 *__forwarding;
     int __flags;
     int __size;
     int age;
    };
    struct __Block_byref_weakPerson_2 {
      void *__isa;
    __Block_byref_weakPerson_2 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__weak weakPerson;
    };
    

    可以发现 __block修饰对象类型的变量生成的结构体内部多了__Block_byref_id_object_copy__Block_byref_id_object_dispose两个函数,用来对对象类型的变量进行内存管理的操作。而结构体对对象的引用类型,则取决于block捕获的对象类型的变量。weakPerson是弱指针,所以__Block_byref_weakPerson_2对weakPerson就是弱引用,person是强指针,所以__Block_byref_person_0对person就是强引用。

    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*/);
    _Block_object_assign((void*)&dst->person, (void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->person2, (void*)src->person2, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    __main_block_copy_0函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3

    static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->weakPerson, 8/*BLOCK_FIELD_IS_BYREF*/)
    ;_Block_object_dispose((void*)src->person2, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    

    同理 __main_block_dispose_0 根据变量有没有被__block修饰做出不同的处理,被__block修饰的变量最后的参数传入的是8,没有被__block修饰的变量最后的参数传入的是3

    __forwarding指针
    我们知道 __forwarding 指针指向的是结构体自己。当使用变量的时候,通过结构体找到 __forwarding 指针,在通过__forwarding 指针找到相应的变量。这样设计的目的是为了方便内存管理。通过上面对__block变量的内存管理分析我们知道,block被复制到堆上时,会将block中引用的变量也复制到堆中。

    当block在栈中时,__Block_byref_age_0结构体内的__forwarding指针指向结构体自己。
    而当block被复制到堆中时,栈中的__Block_byref_age_0结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0结构体中的__forwarding指针指向的就是堆中的__Block_byref_age_0结构体,堆中__Block_byref_age_0结构体内的__forwarding指针依然指向自己。
    通过__forwarding指针巧妙的将修改的变量赋值在堆中的__Block_byref_age_0中。

    __forwarding 指针

    被__block修饰的对象类型的内存管理

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            Block block = ^ {
                NSLog(@"%p", person);
            };
            block();
        }
        return 0;
    }
    

    生成c++代码查看__Block_byref_person_0结构体及其声明

    //__Block_byref_person_0结构体
    struct __Block_byref_person_0 {
      void *__isa; // 8
    __Block_byref_person_0 *__forwarding; //8
     int __flags; //4
     int __size; //4
     void (*__Block_byref_id_object_copy)(void*, void*); //8
     void (*__Block_byref_id_object_dispose)(void*); //8
     Person *__strong person; //8
    };
    // 8 + 8 + 4 + 4 + 8 + 8 + 8 = 48
    
    // __Block_byref_person_0结构体声明
     __attribute__((__blocks__(byref))) __Block_byref_person_0 person = {
    (void*)0,(__Block_byref_person_0 *)&person,
     33554432,
     sizeof(__Block_byref_person_0), 
    __Block_byref_id_object_copy_131, 
    __Block_byref_id_object_dispose_131, 
    ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"))};
    

    __block修饰的对象类型生成的结构体中新增加了两个函数 void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);。这两个函数为__block修饰的对象提供了内存管理的操作。
    可以看出为void (*__Block_byref_id_object_copy)(void*, void*);void (*__Block_byref_id_object_dispose)(void*);赋值的分别为__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131 找到这两个函数

    static void __Block_byref_id_object_copy_131(void *dst, void *src) {
     _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
    }
    static void __Block_byref_id_object_dispose_131(void *src) {
     _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
    }
    

    上述源码中可以发现__Block_byref_id_object_copy_131函数中同样调用了_Block_object_assign函数,而_Block_object_assign函数内部拿到dst指针即block对象自己的地址值加上40个字节。并且_Block_object_assign最后传入的参数是131,同block直接对对象进行内存管理传入的参数3,8都不同。可以猜想_Block_object_assign内部根据传入的参数不同进行不同的操作的。
    通过对上面__Block_byref_person_0结构体占用空间计算发现__Block_byref_person_0结构体占用的空间为48个字节。而加40恰好指向的就为person指针。
    也就是说copy函数会将person地址传入_Block_object_assign函数,_Block_object_assign中对Person对象进行强引用或者弱引用。

    如果使用__weak修饰变量查看一下其中的源码

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            __block __weak Person *weakPerson = person;
            Block block = ^ {
                NSLog(@"%p", weakPerson);
            };
            block();
        }
        return 0;
    }
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_weakPerson_0 *weakPerson; // by ref
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_weakPerson_0 *_weakPerson, int flags=0) : weakPerson(_weakPerson->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
    struct __Block_byref_weakPerson_0 {
      void *__isa;
    __Block_byref_weakPerson_0 *__forwarding;
     int __flags;
     int __size;
     void (*__Block_byref_id_object_copy)(void*, void*);
     void (*__Block_byref_id_object_dispose)(void*);
     Person *__weak weakPerson;
    };
    

    __main_block_impl_0中没有任何变化,__main_block_impl_0对weakPerson依然是强引用,但是__Block_byref_weakPerson_0中对weakPerson变为了__weak指针。
    也就是说无论如何block内部中对__block修饰变量生成的结构体都是强引用,结构体内部对外部变量的引用取决于传入block内部的变量是强引用还是弱引用。

    当block从堆中移除的时候。会调用dispose函数,block块中去除对__Block_byref_person_0 *person;的引用,__Block_byref_person_0结构体中也会调用dispose操作去除对Person *person;的引用。以保证结构体和结构体内部的对象可以正常释放。

    MRC环境下

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            Block block = [^ {
                NSLog(@"%p", person);
            } copy];
            [person release];
            block();
            [block release];
        }
        return 0;
    }
    

    上述代码打印结果

    2019-06-27 17:07:48.657846+0800 block[2383:142138] person - dealloc
    2019-06-27 17:07:48.658156+0800 block[2383:142138] 0x1020001c0

    尽管调用了copy操作,__block结构体不会对person产生强引用,依然是弱引用。

    循环引用

    循环引用会导致内存泄漏

    @interface Person : NSObject
    @property(nonatomic, copy) NSString *name;
    @property(nonatomic, copy) void(^block)(void);
    @end
    
    @implementation Person
    - (void)dealloc
    {
        NSLog(@"Person - dealloc");
    }
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *person = [[Person alloc] init];
            person.name = @"jack";
            person.block = ^{
                NSLog(@"%@",person.name);
            };
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    

    运行代码打印结果为

    2019-06-27 17:15:57.764411+0800 block[2407:144423] 大括号结束啦

    可以发现大括号结束之后,person依然没有被释放,产生了循环引用。
    通过一张图看一下他们之间的内存结构


    循环引用

    Person对象和block对象相互之间产生了强引用,导致双方都不会被释放,进而造成内存泄漏。

    解决循环引用问题 - ARC
    首先为了能随时执行block,肯定希望person对block对强引用,而block内部对person的引用为弱引用最好。

    使用__weak__unsafe_unretained修饰符可以解决循环引用的问题
    __weak会使block内部将指针变为弱指针。block对person对象为弱指针的话,也就不会出现相互引用而导致不会被释放了。

    使用__weak和__unsafe_unretained修饰

    __weak 和 __unsafe_unretained的区别。

    • __weak不会产生强引用,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。
    • __unsafe_unretained不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。

    使用__block也可以解决循环引用的问题。

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            __block Person *person = [[Person alloc] init];
            person.name = @"jack";
            person.block = ^{
                NSLog(@"%@",person.name);
                person = nil;
            };
        }
        NSLog(@"大括号结束啦");
        return 0;
    }
    
    使用__block也可以解决循环引用
    在block内部使用变量使用的其实是__block修饰的变量生成的结构体__Block_byref_person_0内部的person对象,那么当person对象置为nil也就断开了结构体对person的强引用,那么三角的循环引用就自动断开。该释放的时候就会释放了。但是有弊端,必须执行block,并且在block内部将person对象置为nil。也就是说在block执行之前代码是因为循环引用导致内存泄漏的。

    解决循环引用问题 - MRC
    使用__unsafe_unretained解决。在MRC环境下不支持使用__weak,使用原理同ARC环境下相同。
    使用__block也能解决循环引用的问题。因为在__block内存管理中提到过,MRC环境下,尽管调用了copy操作,__block结构体不会对person产生强引用,依然是弱引用。因此同样可以解决循环引用的问题。

    __strong 和 __weak

    __weak typeof(self) weakSelf = self;
    person.block = ^{
        __strong typeof(weakSelf) myself = weakSelf;
        NSLog(@"age is %d", myself->_age);
    };
    

    在block内部重新使用__strong修饰self变量是为了在block内部有一个强指针指向weakSelf避免在block调用的时候weakSelf已经被销毁。

    相关文章

      网友评论

          本文标题:探寻block的本质

          本文链接:https://www.haomeiwen.com/subject/bojhcctx.html