Block系列
# block 本质
Block 本质上是一个 Objective-C 的对象,它内部也有一个 isa 指针,它是一个封装了函数及函数调用环境的 Objective-C 对象,可以添加到 NSArray 及 NSDictionary 等集合中,它是基于 C 语言及运行时特性,有点类似标准的 C 函数。但除了可执行代码以外,另外包含了变量同堆或栈的自动绑定。
# Block 类型
# NSGlobalBlock 全局block
void (^exampleBlock)(void) = ^{
// block
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
2
3
4
打印日志:exampleBlock is: NSGlobalBlock
如果一个 block 没有访问外部局部变量,或者访问的是全局变量,或者静态局部变量,此时的 block 就是一个全局 block ,并且数据存储在全局区。
# NSStackBlock 栈block
int temp = 100;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"exampleBlock is: %d", temp);
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]);
2
3
4
5
6
7
打印日志:exampleBlock is: NSMallocBlock???不是说好的 NSStackBlock 的吗?为什么打印的是__NSMallocBlock__ 呢?这里是因为我们使用了 ARC ,Xcode 默认帮我们做了很多事情。
我们可以去 Build Settings 里面,找到 Objective-C Automatic Reference Counting ,并将其设置为 No ,然后再 Run 一次代码。你会看到打印日志是:exampleBlock is: NSStackBlock
如果 block 访问了外部局部变量,此时的 block 就是一个栈 block ,并且存储在栈区。由于栈区的释放是由系统控制,因此栈中的代码在作用域结束之后内存就会销毁,如果此时再调用 block 就会发生问题,( 注: 此代码运行在 MRC 下)如:
void (^simpleBlock)(void);
void callFunc() {
int age = 10;
simpleBlock = ^{
NSLog(@"simpleBlock-----%d", age);
};
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
callFunc();
simpleBlock();
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
打印日志:simpleBlock--------41044160
# NSMallocBlock
当一个 NSStackBlock 类型 block 做 copy 操作后,就会将这个 block 从栈上复制到堆上,而堆上的这个 block 类型就是 NSMallocBlock 类型。在 ARC 环境下,编译器会根据情况,自动将 block 从栈上 copy 到堆上。具体会进行 copy 的情况有如下 4 种:
block 作为函数的返回值时; block 赋值给 __strong 指针,或者赋值给 block 类型的成员变量时; block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时; block 作为 GCD API 的方法参数时;
# 总结
MRC下block类型
| 类型 | 环境 |
|---|---|
| _NSConcreteGlobalBlock | 只访问了静态变量(包括全局静态变量和局部静态变量)和全局变量 |
| _NSConcreteStackBlock | 没访问静态变量和全局变量 |
| _NSConcreteMallocBlock | NSStackBlock调用了copy |
ARC下block类型
| 类型 | 环境 |
|---|---|
| _NSConcreteGlobalBlock | 只访问了静态变量(包括全局静态变量和局部静态变量)和全局变量 |
| _NSConcreteMallocBlock | 没访问静态变量和全局变量 |
点击查看
__weak typeof(self)weakSelf = self;
static int a = 0;
void (^block1)(void) = ^{
a = 1;
b = 1; //b为全局变量
};
__block int c = 0;
void (^block2)(void) = ^{
NSLog(@"age:%d", weakSelf.age);
c = 1;
};
NSLog(@"block1.class = %@", [block1 class]);
NSLog(@"block2.class = %@", [block2 class]);
NSLog(@"block2 copy.class = %@", [[block2 copy] class]);
运行结果如下:
2020-11-14 22:45:54.457496+0800 BlockTestDemo[13178:426318] block1.class = __NSGlobalBlock__
2020-11-14 22:45:54.457616+0800 BlockTestDemo[13178:426318] block2.class = __NSStackBlock__
2020-11-14 22:45:54.457720+0800 BlockTestDemo[13178:426318] block2 copy.class = __NSMallocBlock__
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# block中关键字的使用
# __block 的作用
简单来说,__block 作用是允许 block 内部访问和修改外部变量,在 ARC 环境下还可以用来防止循环引用;
__block int age = 10;
void (^exampleBlock)(void) = ^{
// block
NSLog(@"1.age is: %d", age);
age = 16;
NSLog(@"2.age is: %d", age);
};
exampleBlock();
NSLog(@"3.age is: %d", age);
2
3
4
5
6
7
8
9
__block 主要用来解决 block 内部无法修改 auto 变量值的问题,为什么加上 __block 修饰之后,auto 变量值就能修改了呢?
这是因为,加上 __block 修饰之后,编译器会将 __block 变量包装成一个结构体 __Block_byref_age_0 ,结构体内部 *__forwarding 是指向自身的指针,并且结构体内部还存储着外部 auto 变量。
struct __Block_byref_val_0 {
void *__isa; // isa指针
__Block_byref_val_0 *__forwarding;
int __flags;
int __size; // Block结构体大小
int age; // 捕获到的变量
}
2
3
4
5
6
7

从上图可以看到,如果 block 是在栈上,那么这个 __forwarding 指针就是指向它自己,当这个 block 从栈上复制到堆上后,栈上的 __forwarding 指针指向的是复制到堆上的 __block 结构体。堆上的 __block 结构体中的 __forwarding 指向的还是它自己,即 age->__forwarding 获取到堆上的 __block 结构体,age->__forwarding->age 会把堆上的 age 赋值为 16 。因此不管是栈上还是堆上的 __block 结构体,最终使用到的都是堆上的 __block 结构体里面的数据。
对于是否可以修改外部变量,我们可以主要集中于这个自加的操作,如果__forwarding永远指向自身那么直接通过i取到i对应的值就可以了为什么中间加一个__forwarding呢?
我们知道, ARC环境下,一旦Block赋值就会触发copy,__block修饰的变量也就会copy到堆上,Block的类型也就变成了__NSMallocBlock。
堆上的Block会持有对象。我们把Block通过copy到了堆上,堆上也会重新复制一份Block,并且该Block也会继续持有该__block修饰的对象。当Block释放的时候,__block修饰的对象因为没有被任何对象引用,也会被释放销毁
__forwarding指针这里的作用就是针对堆的Block,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。然后堆上的变量的__forwarding再指向自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。
block创建的时候是在栈上的,对block进行赋值操作之后会将block拷贝到堆上。同时也会将block中使用的对象拷贝到堆上。然后将栈上的__block修饰对象的__forwarding指针指向堆上的拷贝之后的对象。这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的__forwarding指针将堆和栈的对象链接起来。因此达到了修改的目的。
# __weak 的作用
简单来说是为了防止循环引用。

self 本身会对 block 进行强引用,block 也会对 self 形成强引用,这样就会造成循环引用的问题。我们可以通过使用 __weak 打破循环,使 block 对象对 self 弱引用。
此时我们注意,由于 block 对 self 的引用为 weak 引用,因此有可能在执行 block 时,self 对象本身已经释放,那么我们如何保证 self 对象不在 block 内部释放呢?这就引出了下面__strong 的作用。
# __strong 的作用
简单来说,是防止 block 内部引用的外部 weak 变量被提前释放,进而在 block 内部无法获取 weak 变量以继续使用的情况;
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf exampleFunc];
};
2
3
4
5
这样就保证了在 block 作用域结束之前,block 内部都持有一个 strongSelf 对象可供使用。
但是,即便如此,依然有一个场景,就是执行 __strong __typeof(weakSelf) strongSelf = weakSelf; 之前,weakSelf 对象已经释放,这时如果给 self 对象发送消息,这没有问题,Objective-C 的消息发送机制允许我们给一个 nil 对象发送消息,这不会出现问题。但如果有额外的一些操作,比如说将 self 添加到数组,这时因为 self 为 nil,程序就会 Crash。
我们可以增加一层安全保护来解决这个问题,如:
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// Add operation here
}
};
2
3
4
5
6
7
# 拓展知识
# 思考题
Block 内修改外部 NSMutableString 、NSMutableArray 、NSMutableDictionary 对象,是否需要添加 __block 修饰?
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"1"];
void (^exampleBlock)(void) = ^{
// block
[mutableArray addObject:@"2"];
};
exampleBlock();
NSLog(@"mutableArray: %@", mutableArray);
2
3
4
5
6
7
8
打印日志:mutableArray: ( 1, 2 )
答案是:不需要,因为在 block 内部,我们只是使用了对象 mutableArray 的内存地址,往其中添加内容。并没有修改其内存地址,因此不需要使用 __block 也可以正确执行。当我们只是使用局部变量的内存地址,而不是对其内存地址进行修改时,我们无需对其添加 __block ,如果添加了 __block 系统会自动创建相应的结构体,这种情况冗余且低效。
# Block 数据结构
Block 内部数据结构图如下:

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Block_layout 结构体成员含义如下:
提示
isa: 指向所属类的指针,也就是 block 的类型
flags: 按 bit 位表示一些 block 的附加信息,比如判断 block 类型、判断 block 引用计数、判断 block 是否需要执行辅助函数等;
reserved: 保留变量;
invoke: block 函数指针,指向具体的 block 实现的函数调用地址,block 内部的执行代码都在这个函数中;
descriptor: 结构体 Block_descriptor,block 的附加描述信息,包含 copy/dispose 函数,block 的大小,保留变量;
variables: 因为 block 有闭包性,所以可以访问 block 外部的局部变量。这些 variables 就是复制到结构体中的外部局部变量或变量的地址;
Block_descriptor 结构体成员含义如下:
提示
reserved: 保留变量;
size: block 的大小;
copy: 函数用于捕获变量并持有引用;
dispose: 析构函数,用来释放捕获的资源;
# 总结
使用 Block 过程中需要我们关注的重点有 4 个:
block 的三种类型; block 避免引起循环引用; block 对 auto 变量的 copy 操作; __block、__weak、__strong 的作用;
# block面试题
# _block和__weak修饰符的区别?
- __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
- __weak只能在ARC模式下使用,也只能修饰对象,不能修饰基本数据类型。
- __block对象可以在block中被重新赋值,__weak不可以。
# block是否能修改外部变量的值
Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
# Block在ARC跟MRC中的行为和用法有什么区别?
相同点
- block的本质一样, 都是函数指针
- 使用__block都可以解决在block中修改外部变量的问题
不同点
解决循环引用的方式不同 MRC中使用__block ARC中使用 __weak
block用什么属性修饰,为什么?
在MRC中, 定义Block属性时, 应该用copy修饰
在ARC中, 定义Block属性时, 系统会自动将其copy, 即复制到堆上. 但习惯上还是会用copy修饰
用copy修饰的原因
block创建时默认是创建在栈上的, 超过作用域后就会被销毁, 只有使用copy才会生成一个堆block, 在作用域外被访问
# block是类吗,有哪些类型?
block也算是个类,因为它有isa指针,block.isa的类型包括
_NSConcreteGlobalBlock 跟全局变量一样,设置在程序的数据区域(.data)中 _NSConcreteStackBlock栈上(前面讲的都是栈上的 block) _NSConcreteMallocBlock 堆上
这个isa可以按位运算
# 一个int变量被 __block 修饰与否的区别?block的变量截获
被__block 修饰与否的区别 用一段示例代码来解答这个问题吧:
__block int a = 10;
int b = 20;
PrintTwoIntBlock block = ^(){
a -= 10;
printf("%d, %d\n",a,b);
};
block();//0 20
a += 20;
b += 30;
printf("%d, %d\n",a,b);//20 50
block();/10 20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
通过__block修饰int a,block体中对这个变量的引用是指针拷贝,它会作为block结构体构造参数传入到结构体中且复制这个变量的指针引用,从而达到可以修改变量的作用.
int b没有被__block修饰,block内部对b是值copy.所以在block内部修改b不影响外部b的变化.
# 什么时候栈上的Block会被复制到堆呢?
- 调用block的copy函数时。
- Block作为函数返回值返回时。
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时。
- 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。
什么时候Block被废弃呢?
堆上的Block被释放后,谁都不再持有Block时调用dispose函数。
# block在修改NSMutableArray,需不需要添加__block
如修改NSMutableArray的存储内容的话,是不需要添加__block修饰的。
如修改NSMutableArray对象的本身,那必须添加__block修饰。
# block怎么进行内存管理的?
在上面Block的构造函数__TestClass__testMethods_block_impl_0中的isa指针指向的是&_NSConcreteStackBlock,它表示当前的Block位于栈区中.
| block内存操作 | 存储域/存储位置 | copy操作的影响 |
|---|---|---|
| _NSConcreteGlobalBlock | 程序的数据区域 | 什么也不做 |
| _NSConcreteStackBlock | 栈 | 从栈拷贝到堆 |
| _NSConcreteMallocBlock | 堆 | 引用计数增加 |
全局Block:_NSConcreteGlobalBlock的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针来访问,它的产生条件:
记述全局变量的地方有block语法时.
block不截获的自动变量.
以上两个条件只要满足一个就可以产生全局Block. 参考
栈Block:
_NSConcreteStackBlock在生成Block以后,如果这个Block不是全局Block,那它就是栈Block,生命周期在其所属的变量作用域内.(也就是说如果销毁取决于所属的变量作用域).如果Block变量和__block变量复制到了堆上以后,则不再会受到变量作用域结束的影响了,因为它变成了
- 堆Block.
_NSConcreteMallocBlock将栈block复制到堆以后,block结构体的isa成员变量变成了_NSConcreteMallocBlock。
# block可以用strong修饰吗?
在ARC中可以,因为在ARC环境中的block只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆中的操作.
在MRC中不行,因为要有拷贝过程.如果执行copy用strong的话会crash, strong是ARC中引入的关键字.如果使用retain相当于忽视了block的copy过程.
# 解决循环引用时为什么要用__strong、__weak修饰?
首先因为block捕获变量的时候 结构体构造时传入了self,造成了默认的引用关系,所以一般在block外部对操作对象会加上__weak,在Block内部使用__strong修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有,但是持有的是我们上面加了__weak所以行程了比消此长的链条,刚好能解决block延迟销毁的时候对外部对象生命周期造成的影响.如果不这样做很容易造成循环引用.
# block发生copy时机?
在ARC中,编译器将创建在栈中的block会自动拷贝到堆内存中,而block作为方法或函数的参数传递时,编译器不会做copy操作.
- 调用block的copy函数时。
- Block作为函数返回值返回时。
- 将Block赋值给附有__strong修饰符id类型的类或者Block类型成员变量时。
- 方法中含有usingBlock的Cocoa框架方法或者GCD的API中传递Block时。
# Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?
ARC下会对这个对象强引用,MRC下不会
# block 的变量捕获机制
block 的变量捕获机制,是为了保证 block 内部能够正常访问外部的变量。
| 变量类型 | 是否捕获到 block 内部 | 访问方式 |
|---|---|---|
| 全局变量 | 否 | 直接访问 |
| 局部变量(auto 类型) | 是 | 值传递 |
| 局部变量(static 类型) | 是 | 指针传递 |
对于全局变量,不会捕获到 block 内部,访问方式为直接访问。作用域的原因,全局变量哪里都可以直接访问,所以不用捕获。而对于局部变量,外部不能直接访问,所以需要捕获。下面我们来看一下 block 对于局部变量的具体捕获机制。
# auto 类型的局部变量
auto 类型的局部变量(我们定义出来的变量,默认都是 auto 类型,只是省略了),block 内部会自动生成一个同类型成员变量,用来存储这个变量的值,访问方式为值传递。auto 类型的局部变量可能会销毁,其内存会消失,block 将来执行代码的时候不可能再去访问那块内存,所以捕获其值。由于是值传递,我们修改 block 外部被捕获变量的值,不会影响到 block 内部捕获的变量值。
//局部变量截获 是值截获
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
//这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。
//同样,在block里如果修改变量num,也是无效的,甚至编译器会报错
NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
void(^block)(void) = ^{
NSLog(@"%@",arr);//局部变量
[arr addObject:@"4"];
};
[arr addObject:@"3"];
arr = nil;
block();
打印为1,2,3
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置为nil,对block没有影响,而该对象调用方法会影响
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# static 类型的局部变量
static 类型的局部变量,block 内部会自动生成一个同类型成员变量,用来存储这个变量的地址,访问方式为指针传递。static 变量会一直保存在内存中, 所以捕获其地址即可。相反,由于是指针传递,我们修改 block 外部被捕获变量的值,会影响到 block 内部捕获的变量值。
//局部静态变量截获 是指针截获。
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。
2
3
4
5
6
7
8
9
//全局变量,静态全局变量截获:不截获,直接取值。
/我们同样用clang编译看下结果。
static NSInteger num3 = 300;
NSInteger num4 = 3000;
- (void)blockTest
{
NSInteger num = 30;
static NSInteger num2 = 3;
__block NSInteger num5 = 30000;
void(^block)(void) = ^{
NSLog(@"%zd",num);//局部变量
NSLog(@"%zd",num2);//静态变量
NSLog(@"%zd",num3);//全局变量
NSLog(@"%zd",num4);//全局静态变量
NSLog(@"%zd",num5);//__block修饰变量
};
block();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 对象类型的局部变量
对于对象类型的局部变量,block 会连同它的所有权修饰符一起捕获。
- 如果 block 是在栈上,将不会对对象产生强引用
- 如果 block 被拷贝到堆上,将会调用 block 内部的 copy(__funcName_block_copy_num)函数,copy 函数内部又会调用 assign(_Block_object_assign)函数,assign 函数将会根据变量的所有权修饰符做出相应的操作,形成强引用(retain)或者弱引用。
- 如果 block 从堆上移除,也就是被释放的时候,会调用 block 内部的 dispose(_Block_object_dispose)函数,dispose 函数会自动释放引用的变量(release)。
总结1:
MRC 中block 没有引用外部变量, block为 NSGlobalBlock 类型,存储在全局数据区.
MRC 中block 引用外部变量,block为NSStackBlock 类型,存储在栈内存中.
所以, 在block所属的栈作用域外使用block时, 需要将调用copy方法将该block存储在堆区.
总结2:
ARC 中 没有引用外部变量, block为 NSGlobalBlock 类型,存储在全局数据区.
ARC 中 引用外部变量, block为 autoreleased NSMallocBlock 类型,存储在堆内存中.