Block的解析

来源:互联网 发布:three.js skybox 编辑:程序博客网 时间:2024/06/03 20:26

一,block是什么?

*带有自动变量的匿名函数。
————引自《iOS与OS X多线程和内存管理》*
1.匿名函数
首先blocks是c语言的扩充功能
c语言中函数是这个样子的:

void func() {    printf("hello world");}

那么,block是一个什么样子的呢?

^void () {    printf("hello world");}

这种不带名的函数就是所谓的匿名函数
2.带自动变量
还是要从c语言函数说起。

int a = 10;void func (int b) {    printf("%d + %d = %d",a,b,a+b);}int main(int argc, char * argv[]) {    func(5);    return 0;}

上述的代码主要想说明一件事,C语言函数中,函数体中使用的函数外部变量只有两种:函数参数即全局变量。
那么block是怎么使用的呢?

int main(int argc, char * argv[]) {    int a = 10;    void(^block)(int) = ^(int b) {        printf("%d + %d = %d\n",a,b,a+b);    };    block(5);    return 0;}

我们看到,在此例中a已经不是全局变量了,而是一个局部变量,也就是自动变量。然而block却可以正常使用,为什么呢?因为block内部维护了一个变量a的值,所以执行正确。这里你先不用纠结,下面会有源码。由上我们就知道了什么叫做带自动变量了。

二,block的实质

想要看Block的实质我们还是Block的实现过程。我们还是要借助clang。

#include <stdio.h>int main(int argc, char * argv[]) {    int a = 10;    void(^block)(int) = ^(int b) {        printf("%d + %d = %d\n",a,b,a+b);    };    block(5);    return 0;}

还是这个简单的函数,我们借助clang来转换一下。这里为了稍后方便,我们尽量删除其他无用头文件,引入必要头文件。

clang -rewrite-objc main.m

转换完成后我们会发现main.m同文件夹下多了一个main.cpp的文件。打开这个文件,现在command + L跳转到第62行,复制62行至67行,command + 下调至文件底部粘贴,再跳至510行,command + shift + 上选中上面所有代码,delete删除后就剩下干货了,大概是这个样子的:
block实现
在block结构体中,三个成员变量,一个构造函数。
第一个成员变量是__block_impl的结构体,其中有block实现函数的函数指针。第二个成员变量是__main_block_desc_0,用来负责block的内存管理。第三个int型成员变量是a。
int a这个成员变量就是上面提到的带有的自动变量。因为block内部引用了外部的自动变量,所以在block结构体中多了一个同类型同名的成员变量。同样,如果没有引入外部的自动变量的话此处block结构体中也不会有这第三个成员变量。
现在将目光集中到main函数中。可以看到,第一行声明了一个局部变量,第二行调用了block的构造函数,将block对应的函数指针Desc以及局部变量传给了函数指针指向的函数
然后我们看到,第三行调用block结构体的函数指针指向的函数,并把block自身及参数传给了函数指针指向的函数。
转过来看block指向的函数,函数首先从block自身中提取捕获的自动变量a赋值给一个临时变量,同时执行原本block的函数体
至此就完成了一次block函数的调用过程。
这里我们要注意下捕获的自动变量:
所谓捕获的自动变量我们可以从两方面来理解:
1.我们看到在生成block的瞬间就将自动变量的值赋给了block。所以此时外界计时修改局部变量的值并不影响block中的值。
2.block中我们是不能对捕获的变量进行赋值操作的,只要这么做编译器就会警告。为什么苹果会做出这样的限制呢?因为在block里对捕获的自动变量复制其实是有歧义的。因为通过看__main_block_func_0内部的实现我们知道,block内部使用的都是block捕获到自动变量,当然这个自动变量是我们转换代码之前完全不知道的一个概念。也就是在编码过程中我们在block中使用的变量与实际代码运行过程中block内部操作的变量本就是两个变量,所以在这里修改block捕获的自动变量的值事实上跟开发者预期的结果完全是两个结果。所以苹果干脆在此就给出个警告来避免未知的错误。
3.虽说不能对捕获的自动变量进行赋值操作,但这并不影响我们使用他,否则的话这个自动变量捕获到也没有什么用了。这点很好理解,没什么好解释的。

三,关于Block对外部的赋值操作

上文说过,Block不能对其捕获的局部(非静态)变量的值进行赋值操作。既然有这些限制,那么一定有可以Block中可以做赋值操作的变量,他们都有谁呢?

  • 静态变量
  • 全局变量
  • __block说明符修饰的变量

还是针对带有自动变量的匿名函数这句话来讲。这一节我们来探讨一下Block是如何使用外部变量的。我们知道Block截获变量的意义在于想要使用Block作用于内无法使用的变量,所以他要截获变量。接下来围绕着这句话从各种变量类型做深入的展开。
1.仅使用参数的Block

int main(int argc, char * argv[]) {    void(^block)(int) = ^(int a) {        a = 10;        printf("block : a = %d\n",a);    };    int a = 5;    block(a);    printf("a = %d",a);    return 0;}///clang转换后的形式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, int a) {    a = 10;    printf("block : a = %d\n",a);}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, char * argv[]) {    void(*block)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));    int a = 5;    ((void (*)(__block_impl *, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, a);    printf("a = %d",a);    return 0;}

由于使用的是函数的参数,是在Block作用域内可以使用的,所以Block没有对变量进行截获。这个Block基本就是最简单的函数。
2.使用局部变量(非静态)

int main(int argc, char * argv[]) {    int a = 10;    void(^block)() = ^() {        printf("n = %d",a);    };    block();    return 0;}///clang转换后struct __main_block_impl_0 {    struct __block_impl impl;    struct __main_block_desc_0* Desc;    int a;    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {        impl.isa = &_NSConcreteStackBlock;        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    printf("n = %d",a);}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, char * argv[]) {    int a = 10;    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);    return 0;}

我们看到,这里截获了这个局部变量,具体原因在上述内容中有讲到过,此处不再赘述。
3.局部静态变量
我们知道,静态变量存储在静态区,只创建一次,随后使用的同名变量均应指向同一地址。由静态变量的特性我们应该知道,如果Block截获了一个静态局域变量,并在Block中对其值进行了更改,这个操作应该是有效的,他应该改变该变量的值。我们看下他是如何实现的?

int main(int argc, char * argv[]) {    static int a = 10;    void(^block)() = ^() {        a = 20;    };    block();    printf("a = %d",a);    return 0;}///clang转换后struct __main_block_impl_0 {    struct __block_impl impl;    struct __main_block_desc_0* Desc;    int *a;    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {        impl.isa = &_NSConcreteStackBlock;        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    (*a) = 20;}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, char * argv[]) {    static int a = 10;    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);    printf("a = %d",a);    return 0;}

我们看到了,Block截获的是局部静态变量的指针。这个思路跟C语言中函数一样。C语言中我们想更改实参的值时也是通过传址的方式实现的。形如:

void mySwap(int * a,int * b);int main(int argc, char * argv[]) {    int a = 1;    int b = 2;    mySwap(&a, &b);    printf("a = %d",a);    return 0;}void mySwap(int * a,int * b) {    int temp = *a;    *a = *b;    *b = temp;}

4.全局变量(静态与非静态)
上面说过,Block捕获变量是为了在Block中使用其作用域外的变量,那么全局变量本身作用在区域,Block可以使用,故不需要对全局变量进行捕获。以下以全局静态变量为例。

static int a = 10;int main(int argc, char * argv[]) {    void(^block)() =  ^{        a = 20;    };    block();    printf("a = %d",a);    return 0;}///clang 转换后static int a = 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 = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};static void __main_block_func_0(struct __main_block_impl_0 *__cself) {        a = 20;}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, char * argv[]) {    void(*block)() = ((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);    printf("a = %d",a);    return 0;}

5.__block修饰的变量
我们知道,被__block修饰的局部变量,在Block内部对其进行赋值操作是可以的,那么他是如何实现的呢?

int main(int argc, char * argv[]) {    __block int a = 10;    void(^block)() =  ^{        a = 20;    };    block();    printf("%d",a);    return 0;}///clang 转换后struct __Block_byref_a_0 {    void *__isa;    __Block_byref_a_0 *__forwarding;    int __flags;    int __size;    int a;};struct __main_block_impl_0 {    struct __block_impl impl;    struct __main_block_desc_0* Desc;    __Block_byref_a_0 *a; // by ref    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref    (a->__forwarding->a) = 20;}static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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, char * argv[]) {    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};    void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);    printf("%d",(a.__forwarding->a));    return 0;}

我们看到__block修饰的变量会自动为其生成一个结构体,并在之后对变量的操作使用的都是结构体中持有的变量a。而后Block捕获了结构体,Block中对变量的复制也映射成了对结构体内部变量的赋值。我们可以发现,在上面的例子中,不仅生成了一个__block变量的结构体,还多了__main_block_copy_0__main_block_dispose_0两个函数,的具体作用我们稍后再表。

四,Block类型及其存储域

首先应该了解一下Block的三种类型:

  • _NSConcreteStackBlock///栈区Block
  • _NSConcreteMallocBlock///堆区Block
  • _NSConcreteGlobalBlock///全局Block

我们设想这样一种情况,上述的例子中,我们看到我们都是在main函数中声明的Block,也就是说他其实block对象其实是一个局域变量,那么他一定会被存储在栈上。也就是说当出了变量的作用于,也就是main函数结束,block对象就会被销毁。这时我们的Block即为_NSConcreteStackBlock。我们可以从__block_impl结构体中的isa指针看到上述例子中的Block均为_NSConcreteStackBlock类型。

但是平时我们使用block的时候还有这么一种情况,即并不是在声明的地方立即使用,而是在等待某个时机从而进行回调。而此时一般是已经出了block对象的作用域,如果跟之前一样是栈区block的话显然block已经被销毁,此时进行回调只会引起crash。这时我们就需要_NSConcreteMallocBlock区的block了,即堆区Block。回想我们持有block的时候使用什么修饰符呢?copy对吧,而block对象执行copy操作就是将其按需复制到堆区。

#import <Foundation/Foundation.h>int globalVar = 100;void(^globalBlock)() = ^{    NSLog(@"global block beyond main");};int main(int argc, char * argv[]) {    int a = 10;    NSLog(@"stack block: %@",^{NSLog(@"here a = %d",a);});    NSLog(@"malloc block: %@",[^{NSLog(@"here a = %d",a);} copy]);    NSLog(@"global block: %@",^{NSLog(@"I use nothing");});    NSLog(@"global block beyong main: %@",globalBlock);    NSLog(@"global block use globalVar: %@",^{NSLog(@"%d",globalVar);});    return 0;}///输出:stack block: <__NSStackBlock__: 0x7fff5d7cd578>malloc block: <__NSMallocBlock__: 0x60000004ef70>global block: <__NSGlobalBlock__: 0x1024320d0>global block beyong main: <__NSGlobalBlock__: 0x102432050>global block use globalVar: <__NSGlobalBlock__: 0x102432110>

此处我们可以看到三种block类型。从源码我们可以知道默认生成的block均为_NSConcreteStackBlock类型,而后执行了copy操作的block为_NSConcreteMallocBlock类型,后面三个均为_NSConcreteGlobalBlock类型。
这里我们先说_NSConcreteGlobalBlock类型的Block。在全局范围内声明的Block即为全局Block,并且没有引入自动变量的也为全局Block。
现在我们知道了,调用过copy方法的block会被复制到堆区,堆区的Block均为_NSConcreteMallocBlock类型。那么什么情况下block会执行copy方法呢?

其实我们可以从上述的分析中猜到,当block需要在其作用域外使用的我们应该将其复制到堆区。例如block作为函数返回值的时候,这时候编译器会按需调用copy方法:

typedef void(^voidBlock)();voidBlock func();int main(int argc, char * argv[]) {    NSLog(@"the return value of func:%@",func());    return 0;}voidBlock func() {    int a = 10;    NSLog(@"the block in func:%@",^{NSLog(@"block in func : a = %d",a);});    return ^{        NSLog(@"block in func : a = %d",a);    };}///输出:the block in func:<__NSStackBlock__: 0x7fff56877558>the return value of func:<__NSMallocBlock__: 0x6080000486a0>

此例中我们看到函数体中,输出了一个Block其为栈区Block,但是当将同样的Block作为返回值返回到main函数中的时候,他变成了堆区Block。
同学们不要说我这不是一个Block,我应该生成一个Block将其赋值,Log一下,在返回出去。这个真不是我不赋值,我不能啊,因为在ARC中赋值的时候如果不附加修饰符的话默认认为生成的变量是以__strong修饰符修饰的,而编译器遇到__strong修饰符会自动copy。。。我怎么给你做例子啊。。。反正这么写虽然不是同一个block,但是应该是同一类型block,足以说明问题。另外说过,编译器会按需调用copy方法。也就是说栈区block会出作用域销毁,全局block并不会,所以如果返回值是一个全局block的话,则不会调用copy方法。
此外以下两种情况也会由系统为我们调用copy方法:

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

还有就是显示调用copy方法的时候,另外如果将其赋值给有copy修饰符修饰的属性的话也会调用copy方法。
然而什么时候应该调用copy方法呢?我们先来看下不同类型block调用copy方法会有什么行为。

Block类型 副本源的配置存储域 复制效果
_NSConcreteStackBlock 栈 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteMallocBlock 堆 引用计数增加 不管Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可。

————引自《iOS与OS X多线程和内存管理》

但是在我们确定的时候,还是要根据需要调用copy方法,不要盲目调用copy方法,毕竟这个方法是十分占用CPU资源的。

五, __block说明符

上文中,已经讲述了block对象在调用copy方法时候的行为。然而__block说明符修饰的变量与block对象基本一致。

这里写图片描述
正如在上文中提到的,被__block说明符的变量会自动生成一个结构体。
值得一提的是三个地方:
这里写图片描述
只有被block说明符修饰的变量,今后使用的均为其结构体中维护的同名成员变量,不过从源码中我们看到,并不是简单地使用了成员变量,而是a.forwarding->a这样一个引用方式,这是因为什么呢?
首先从__Block_byref_a_0中我们可以看到forwarding是一个Block_byref_a_0类型的结构体指针。
从main函数中第一行block变量生成的代码我们看出,在本例中生成__block变量a的同时将a的__forwarding指向了a自身。这样a.forwarding->a最终还是指向了__block变量a结构体中的成员变量a。
既然这样,就一定存在__forwarding并不指向block变量自身的情况,故此才需要__forwarding存在来保证时刻能取到一个正确的值。而上文中提到的调用copy方法的时候,就会对__forwarding指针进行操作。
这里写图片描述
由上图我们可以看到,当调用copy方法后,__forwarding指针指向堆中的__block变量。而堆中的__block变量的__forwarding指针则指向自身。

同时我们知道,block其实是对c语言的扩充,然而OC中我们使用的是引用计数来管理对象生命周期,而不是GC。所以事实上Block需要自行管理内存。那么当我们的Block捕获了一个对象时,他又是如何管理其引用计数的呢?
上文中有提到过__main_block_copy和__main_block_dispose两个函数。当Block结构体中捕获到的对象需要retain的时候则调用__main_block_copy方法增加引用计数,当其需要释放的时候则调用__main_block_dispose释放对象。所以当block从栈上复制到堆的时候会调用copy函数,而对上的block被释放时调用dispose函数。

六,关于block的引起的循环引用

一直以来,Block引起的循环引用都让不少初级工程师,甚至包括一些中级工程师(索性就叫他中级吧。。。)谈虎色变。他们不知道Block是如何引起循环引用的,只知道__weak可以避免循环引用。知其然不知其所以然,闹出一些笑话也是让人无语。

首先说一下什么是循环引用?

引用计数机制不做展开,我们只需要知道,在OC中对象是在引用计数为0的时候进行销毁的。一个对对象的强引用会造成一次引用计数的加一。释放一个强引用会造成引用计数的减一。
Block对内部使用的自动变量造成一个强引用,而如果这个自动变量恰好对Block也有强引用的话就会造成循环引用。
既然知道了循环引用的起因,那么我们只要打破引用的闭环就可以轻松解决。两个思路,一个是从最开始就不让强引用成为闭环,使用弱引用。另一个思路是找到一个合适的时机主动释放一个强引用,打破闭环。
1.弱引用

__weak typeof(self)weakSelf = self;self.block = ^{    NSLog(@"%@",weakSelf);};self.block();

上述代码中,使用__weak生成一个弱引用变量weakSelf,保持对self的弱引用。然后Block捕获到weakSelf,对weakSelf也是弱引用,然而却没有造成闭环。故避免了循环引用。
这里写图片描述
主动释放

__block id blockSelf = self;self.block = ^{    NSLog(@"%@",blockSelf);    blockSelf = nil;};NSLog(@"%@",self.block);self.block();

上述代码中,使用__block生成一个block对象blockSelf,保持对self的强引用。然后Block捕获到blockSelf,强引用blockSelf,由于self对block还有一个强引用,此时形成了一个闭环。但当block调用的时候,内部最后将blockSelf对象置为nil。由于blockSelf置为nil,__block对象失去强引用被销毁,同时释放对self的强引用,从而打破闭环。

这里写图片描述
不过两种避免循环引用的方式都有各自的缺点。

__weak 的弱引用形式的缺点在于,当block执行的时候,由于对self是弱引用,不能保证self对象是否已经被销毁。事实上block执行前self被销毁还好,顶多是不执行。但是如果在block执行过程中,self被销毁就会造成不可预估的后果。所以当使用__weak的时候我们通常会看到如下结构:

__weak typeof(self)weakSelf = self;self.block = ^{    __strong typeof(weakSelf)strongSelf = weakSelf;    NSLog(@"%@",strongSelf);};self.block();

这样的结构可以保证在block执行过程中,不会因为self释放引起问题,然而如果block执行前self被释放后block也就没有机会执行了,也算是对代码的保护。
__weak有这样的缺点,为什么不适用__block等方式呢?

事实上__block同样有着自己的烦恼,就是一定要在block体中对__block对象置为nil,且block一定要执行才可以解决循环引用。所以开发者要根据具体情况合理的选择解决循环引用的方式。

原创粉丝点击