iOS Block(1)-底层原理探索、block的类型和copy

1. block的本质

我们通过一个简单的demo,解析一下block的底层原理.
定义一个简单的block并调用:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ^(){
            NSLog(@"Hello world, I'm block!");
        }();
    }
    return 0;
}## 1\. block的本质

我们通过一个简单的demo,解析一下block的底层原理.
定义一个简单的block并调用:

import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
^(){
NSLog(@"Hello world, I'm block!");
}();
}
return 0;
}


将OC代码转换成C++代码

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;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);

    }

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;

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

}
return 0;

}


//我们OC下的block相关代码
^(){
NSLog(@"Hello world, I'm block!");
}();

装换成的C++代码就是:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();


block的调用实际上就是`__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_0`结构体内部有一个与结构体同名的`__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)`函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的`- (instancetype)init;`方法。`__main_block_impl_0`函数携带三个参数,最后一个参数为可选的,默认值为0。再看结构体`__main_block_impl_0`,发现其第一个成员imp也是个结构体,结构体类型为`__block_impl`。

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


通过结构体`__block_impl`实现代码中的isa指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。结构体的第二个成员仍然是个`__main_block_desc_0`类型的结构体.

static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}


该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size则代表了该block的大小。
接下来回到block的调用函数`__main_block_impl_0`,`((void (*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA))();`该函数就是结构体`__main_block_impl_0`的构造函数`__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)`,它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的`__main_block_desc_0`,那么第一个参数函数指针fp到底是什么?在这个demo的C++实现代码中,fp指向的函数为`__main_block_func_0`,`__main_block_func_0`的函数实现代码如下:

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


由于在OC代码中,我们block体内打印了一个字符串,与这个`__main_block_func_0`函数内的代码完全一致。研究发现`__main_block_func_0`这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成`__main_block_func_0`这个函数。与我们OC中的代码`NSLog(@"Hello world, I'm block!");`相对应的就是`NSLog((NSString*)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);`.
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。

## 2\. block的变量捕获(capture)

#### 一. 局部变量之auto变量

什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:

  • (void)example{
    int a = 5; //等价于auto int a = 5;
    NSString *name = @"Block"; //等价于 auto NSString *name = @"Block";
    static int b = 10; //这个b就不是auto变量
    }

常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制之后block变成什么样子?:

//demoA

import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {

    int a = 10;
    Block block = ^(){
        NSLog(@"你好世界!a = : %d ;",a);
    };

    a = 20;
    block();

// 2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;

}
return 0;

}


上面demo,block内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,将上边的代码转换成C++代码:

typedef void(*Block)(void);

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

        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,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, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

    int a = 10;

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

    a = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

}
return 0;

}


对比之前没有访问任何变量的block结构体,此时的block所对应的结构体`__main_block_impl_0`里面多了一个成员`int a`,并且结构体的构造函数`__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)`多出了一个参数`_a`,(知识点:后面的: `a(_a)`为C++的语法,意为将参数`_a`赋值给成员`a`)。
在实现block的时候,对应的C++代码为`Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA, a));`可见,系统将`a`作为函数`__main_block_impl_0`的参数传递进去,所以block所对应的结构体中`int a;`这个成员所对应的值`a = 10;`后面我们修改了`a`的值为20,并使用`block();`调用block 打印`a`的值,这个时候调用了函数`__main_block_func_0(struct __main_block_impl_0 *__cself)`,实现如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
}


其内部访问变量`a`的方式为:`int a = __cself->a;__cself`为block所对应的结构体对象,所以这个`a`也就是之前结构体`__main_block_impl_0`中保存的成员变量`a`的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时`main()`函数中的`a`变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量`a`,外面怎么修改不会影响到block结构体内部成员`a`的值。

#### 二. 局部变量之static变量

根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:

//demoB

import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
@autoreleasepool {

    auto NSString *name = @"Block";
    int a = 10;
    static int b = 10;
    Block block = ^(){
        NSLog(@"你好世界!a = : %d ;",a);
        NSLog(@"你好世界!b = : %d ;",b);
    };

    a = 20;
    b = 20;
    block();

// 2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
// 2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;

}
return 0;

}


发现局部静态变量b修改之后,block内部打印的结果也变了!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量b的捕获过程:

typedef void(*Block)(void);

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *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 = &_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
int *b = __cself->b; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*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)};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

    int a = 10;
    static int b = 10;
    Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

    a = 20;
    b = 20;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

}
return 0;

}


通过C++代码发现,局部自动变量`a`与静态变量`b`的捕获方式不同,block结构体中,`a`为`int`变量,`b`为`int *`变量,也就是指针。在定义block的时候,`Block block = ((void (*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, a, &b))`,传递的也是`b`变量的指针,调用block的时候,`__main_block_func_0`中获取`b`也是通过block的结构体`__main_block_impl_0`访问内部成员变量`b`,与结构体外部变量`b`指向的是同一块内存地址,所以只要有地方修改`b`,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。

#### 三. 全局变量

\demoB

import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
@autoreleasepool {

    int a = 10;
    static int b = 10;
    Block block = ^(){
        NSLog(@"你好世界!a = : %d ;",a);
        NSLog(@"你好世界!b = : %d ;",b);
        NSLog(@"你好世界!age_ = : %d ;",age_);
    };

    a = 20;
    b = 20;
    age_ = 26;
    block();

// 2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
// 2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
// 2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;

}
return 0;

}


block内部访问全局变量`age_`,其变化同静态局部变量一样。同样转换成C++代码分析:

typedef void(*Block)(void);

int age_ = 25;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *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 = &_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
int *b = __cself->b; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,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 a = 10;
    static int b = 10;
    Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

    a = 20;
    b = 20;
    age_ = 26;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;

}


较demoA、demoB不同的是,block结构体内部没有定义`age_`变量,block内部访问`age_`变量的时候,传入的也是全局的`age_`,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:

![image](//upload-images.jianshu.io/upload_images/6323922-d14764dd87e401c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/800/format/webp)

分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。

## 3\. block的类型

> block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型.
> `__NSGlobalBlock__ ( _NSConcreteGlobalBlock )`
> `__NSStackBlock__ ( _NSConcreteStackBlock )`
> `__NSMallocBlock__ ( _NSConcreteMallocBlock )`

我们通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:

![image](//upload-images.jianshu.io/upload_images/6323922-3cd2f60c31f3c48b.png?imageMogr2/auto-orient/strip|imageView2/2/w/835/format/webp)

> 程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
> 数据区也就全局区,存放着程序中使用到的全局变量。
> 堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
> 栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。
> `补充的知识`
> 堆是动态分配内存的,是程序员管理的,自己申请内存,自己管理内存.
> 栈是系统会自动分配内存,自己释放内存.

由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:

![image](//upload-images.jianshu.io/upload_images/6323922-1a92da1ae183263e.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/900/format/webp)

在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:

import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
@autoreleasepool {

    int a = 10;
    static int b = 10;
    Block block = ^(){
        NSLog(@"你好世界!a = : %d ;",a);
    };

    Block block1 = ^(){
        NSLog(@"你好世界");
    };
    NSLog(@"%@  %@  %@",[block class],[block1 class],[[block copy] class]);

// NSStackBlock NSGlobalBlock NSMallocBlock

}
return 0;

}


访问了auto变量的block在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。

| block类型 | 环境 |
| --- | --- |
| **NSGlobalBlock** | 没有访问auto变量 |
| **NSStackBlock** | 访问了auto变量 |
| **NSMallocBlock** | **NSStackBlock**调用了copy |

stackBlock为什么要copy到堆上,因为我们想把block存储的东西保存下来.栈上的block会在函数结束的时候释放,block保存的东西不确定,我们一般都是将block保存下来,在恰当的时候调用.

![image](//upload-images.jianshu.io/upload_images/6323922-a7837ac562e8388d.png?imageMogr2/auto-orient/strip|imageView2/2/w/600/format/webp)

## 4\. block的copy

> 在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,有以下情况:
> 
> 1.  block作为函数返回值的时.
> 2.  将block赋值给_strong指针时.
> 3.  block作为Cocoa API中方法名含有usingBlock的方法参数时.(例如:[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }];)
> 4.  block作为GCD API的方法参数时

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

那么问题来了,为什么要对block进行copy操作?
假如在MRC环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:

![image](//upload-images.jianshu.io/upload_images/6323922-bd591b829e96796c.png?imageMogr2/auto-orient/strip|imageView2/2/w/693/format/webp)

调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个`NSStackBlock`类型的block,该block对应的结构体分配在栈内存上,等到`test()`函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量`a`,`a`所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用`copy`方法将block从栈区`copy`到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用`copy` 或`strong`关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的`copy`方法了。但是系统底层还是帮我们对block做了`copy`操作。

![image](//upload-images.jianshu.io/upload_images/6323922-54180f146c543bf6.png?imageMogr2/auto-orient/strip|imageView2/2/w/597/format/webp)

"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了`void(^block)(void)`,这个变量默认是`__strong`修饰的,满足编译器会根据情况自动将栈上的block复制到堆上的条件2,即"将block赋值给`__strong`指针时"。

作者:周灬
链接:https://www.jianshu.com/p/9441de6295a0
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


将OC代码转换成C++代码

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;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);

        }

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; 

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

    }
    return 0;
}

//我们OC下的block相关代码
 ^(){
        NSLog(@"Hello world, I'm block!");
    }();

装换成的C++代码就是:
   ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();

block的调用实际上就是__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_0结构体内部有一个与结构体同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的- (instancetype)init;方法。__main_block_impl_0函数携带三个参数,最后一个参数为可选的,默认值为0。再看结构体__main_block_impl_0,发现其第一个成员imp也是个结构体,结构体类型为__block_impl

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

通过结构体__block_impl实现代码中的isa指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。结构体的第二个成员仍然是个__main_block_desc_0类型的结构体.

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size则代表了该block的大小。
接下来回到block的调用函数__main_block_impl_0,((void (*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA))();该函数就是结构体__main_block_impl_0的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0),它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的__main_block_desc_0,那么第一个参数函数指针fp到底是什么?在这个demo的C++实现代码中,fp指向的函数为__main_block_func_0__main_block_func_0的函数实现代码如下:

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

由于在OC代码中,我们block体内打印了一个字符串,与这个__main_block_func_0函数内的代码完全一致。研究发现__main_block_func_0这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成__main_block_func_0这个函数。与我们OC中的代码NSLog(@"Hello world, I'm block!");相对应的就是NSLog((NSString*)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);.
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。

2. block的变量捕获(capture)

一. 局部变量之auto变量

什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:

- (void)example{
   int a = 5;  //等价于auto int a = 5;
   NSString *name = @"Block"; //等价于 auto NSString *name = @"Block";
   static int b = 10;  //这个b就不是auto变量
}

常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。
为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制之后block变成什么样子?:

//demoA
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };

        a = 20;
        block();
//        2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;

    }
    return 0;
}

上面demo,block内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,将上边的代码转换成C++代码:

typedef void(*Block)(void);

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

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,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, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;

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

        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

对比之前没有访问任何变量的block结构体,此时的block所对应的结构体__main_block_impl_0里面多了一个成员int a,并且结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)多出了一个参数_a,(知识点:后面的: a(_a)为C++的语法,意为将参数_a赋值给成员a)。
在实现block的时候,对应的C++代码为Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,&__main_block_desc_0_DATA, a));可见,系统将a作为函数__main_block_impl_0的参数传递进去,所以block所对应的结构体中int a;这个成员所对应的值a = 10;后面我们修改了a的值为20,并使用block();调用block 打印a的值,这个时候调用了函数__main_block_func_0(struct __main_block_impl_0 *__cself),实现如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
 }

其内部访问变量a的方式为:int a = __cself->a;__cself为block所对应的结构体对象,所以这个a也就是之前结构体__main_block_impl_0中保存的成员变量a的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时main()函数中的a变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量a,外面怎么修改不会影响到block结构体内部成员a的值。

二. 局部变量之static变量

根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:

//demoB
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        auto NSString *name = @"Block";
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
        };

        a = 20;
        b = 20;
        block();
//        2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
//        2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;

    }
    return 0;
}

发现局部静态变量b修改之后,block内部打印的结果也变了!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量b的捕获过程:

typedef void(*Block)(void);

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *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 = &_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
  int *b = __cself->b; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*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)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

通过C++代码发现,局部自动变量a与静态变量b的捕获方式不同,block结构体中,aint变量,bint *变量,也就是指针。在定义block的时候,Block block = ((void (*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, a, &b)),传递的也是b变量的指针,调用block的时候,__main_block_func_0中获取b也是通过block的结构体__main_block_impl_0访问内部成员变量b,与结构体外部变量b指向的是同一块内存地址,所以只要有地方修改b,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。

三. 全局变量

\\demoB
#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
            NSLog(@"你好世界!age_ = : %d ;",age_);
        };

        a = 20;
        b = 20;
        age_ = 26;
        block();
//        2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
//        2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
//        2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;

    }
    return 0;
}

block内部访问全局变量age_,其变化同静态局部变量一样。同样转换成C++代码分析:

typedef void(*Block)(void);

int age_ = 25;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *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 = &_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
  int *b = __cself->b; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,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 a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        age_ = 26;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

较demoA、demoB不同的是,block结构体内部没有定义age_变量,block内部访问age_变量的时候,传入的也是全局的age_,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:

image

分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。

3. block的类型

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型.
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

我们通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:

image

程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
数据区也就全局区,存放着程序中使用到的全局变量。
堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。
补充的知识
堆是动态分配内存的,是程序员管理的,自己申请内存,自己管理内存.
栈是系统会自动分配内存,自己释放内存.

由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:

image

在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:

#import <Foundation/Foundation.h>

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };

        Block block1 = ^(){
            NSLog(@"你好世界");
        };
        NSLog(@"%@  %@  %@",[block class],[block1 class],[[block copy] class]);

//         __NSStackBlock__  __NSGlobalBlock__  __NSMallocBlock__

    }
    return 0;
}

访问了auto变量的block在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。

block类型 环境
NSGlobalBlock 没有访问auto变量
NSStackBlock 访问了auto变量
NSMallocBlock NSStackBlock调用了copy

stackBlock为什么要copy到堆上,因为我们想把block存储的东西保存下来.栈上的block会在函数结束的时候释放,block保存的东西不确定,我们一般都是将block保存下来,在恰当的时候调用.

image

4. block的copy

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,有以下情况:

  1. block作为函数返回值的时.
  2. 将block赋值给_strong指针时.
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时.(例如:[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }];)
  4. block作为GCD API的方法参数时
  • MRC下block属性的建议写法
    @property (copy, nonatomic) void (^block)(void);
  • ARC下block属性的建议写法
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);

那么问题来了,为什么要对block进行copy操作?
假如在MRC环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:

image

调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个NSStackBlock类型的block,该block对应的结构体分配在栈内存上,等到test()函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量a,a所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用copy方法将block从栈区copy到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用copystrong关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的copy方法了。但是系统底层还是帮我们对block做了copy操作。

image

"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了void(^block)(void),这个变量默认是__strong修饰的,满足编译器会根据情况自动将栈上的block复制到堆上的条件2,即"将block赋值给__strong指针时"。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容