CoderHann

block那些事儿

block简介

block我们经常称之为代码块语法,是Objective-C对闭包的实现,如果你不知道闭包的概念可以参考百科,用于代码块的存储并在适当场景执行保存的代码。正是由于block能够存储代码以及捕获变量的特点,在开发中实现回调就变得特别的简单。通常应用场景如:事件监听回调、网络异步回调等等,在提供简单回调的同时也将业务代码变得更加的独立、灵活。但是如果你不是很了解block的用法的话可能造成一些逻辑上的错判或者比较严重的问题如循环引用,本章着重讲述block的基本使用以及开发中要避免的一些问题。

block的使用

我们在了解block是什么以及它在开发中主要作用之后,我们要对block的使用做一个详细的介绍。这个章节主要围绕block的使用,通过语法分析、分类和其它用法对其进行讲述。

创建

如果你学过C语言的函数指针,那么block变量的声明跟函数指针极其相似我们来看下block的语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//返回类型 block变量名 入参列表,无参数使用() 入参列表,无参数可省略()
returnType(^blockName)(parameterTypes) = ^(parameters) {
// 代码
};
// 示例
- (void)blockDeclare {
//返回类型和入参都为空的情况
void (^printBlock)() = ^{
NSLog(@"printBlock");
};
printBlock();
// 声明一个sumBlock变量名的block对象,它的返回值类型是int,接收两个int类型的参数,封装代码功能求给出两个参数之和
int(^sumBlock)(int a,int b) = ^(int a,int b) {
return a+b;
};
int sum = sumBlock(1,2);
NSLog(@"sum is:%zd",sum);
}
2017-04-06 14:14:41.570 KnowBlock[1857:139693] printBlock
2017-04-06 14:14:41.570 KnowBlock[1857:139693] sum is:3

上面演示的就是block最基础的使用,其实block的使用并不难只是表现形式上和其它类型不太一样导致我们跟它比较生疏,如果多用用的话自然用着也方便了。

既然block作为一种类型存在我们的代码中,那我们就可以将其看做普通类型来使用,正好带着大家再熟悉下他的用法。

自定义block类型:
如果我们定义的一个block类型需要在很多地方使用,那我们可以将其使用typedef进行自定义类型化,苹果官方文档也推荐这种方式。除此之外使用自定义block类型代码,读取上就类似普通的类型增强了代码可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef int (^MyBlock)(int a,int b);
- (void)blockForTypedef {
MyBlock aBlock = ^int(int a,int b) {
return a + b;
};
int sum = aBlock(3,3);
NSLog(@"MyBlock sum:%zd",sum);
MyBlock anotherBlock = ^int(int n,int m) {
return n + m;
};
int sum2 = anotherBlock(1,1);
NSLog(@"MyBlock sum2:%zd",sum2);
}

block属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 属性声明部分
@property (nonatomic,copy)MyBlock blockOne;//MyBlock使用上面typedef定义的
@property (nonatomic,copy)int (^blockTwo)(int a,int b);
- (void)blockForProperty {
_blockOne = ^int(int a,int b) {
return a+b;
};
_blockTwo = ^int(int a,int b) {
return a+b;
};
_blockOne(1,1);
_blockTwo(2,2);
}

block做为形参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 带有block形参的对象方法
- (void)printBlock:(void (^)())sumBlock {
// (其它代码,执行完所产生的数据用于回调)
// 回调
if (sumBlock) {
// 如果block不为空就执行block
sumBlock();
}
}
- (void)blockForParams {
// 匿名block演示
[self printBlock:^{
NSLog(@"匿名block调用啦!");
}];
// 显式声明的block变量用于参数传递
void (^printBlock)() = ^{
NSLog(@"实名printBlock调用啦!");
};
[self printBlock:printBlock];
}
2017-04-06 15:06:36.497 KnowBlock[2068:179310] 匿名block调用啦!
2017-04-06 15:06:36.497 KnowBlock[2068:179310] 实名printBlock调用啦!

上面演示block作为形参传递给其它方法,这种使用方法一般用于耗时/阻塞操作回调的场景。比如网络请求所产生的数据需要给到调用者,这时就可以使用block将产生的数据反馈回去。当这种方法有多个参数,并且有block参数时经常将block的参数放到方法尾部,形成尾部代码块不会影响方法的可读性。

分类

这里要讲的block分类是根据block所存储的区域有关,存储在静态区的为静态block__NSConcreteGlobalBlock,存储在栈内存区的为栈block__NSConcreteStackBlock,而存储在堆内存区的为堆block__NSConcreteMallocBlock。而这三种类型又跟项目所使用的内存管理方式ARC、MRC有关。下面我们来具体分析下这几种类型的表现:

静态block:
当block代码块中没有对任何的局部变量的引用时(引用静态变量不受影响),那么这个block将会存储在静态区域。MRC和ARC所达成的条件一致符合以上所述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)blockCategoryForGlobal {
void (^globalBlock)() = ^{
NSLog(@"global block executed!");
};
NSLog(@"globalBlock:%@",globalBlock);
static int a = 11;
void (^globalBlock2)() = ^{
NSLog(@"globalBlock2 executed!a=%zd",a);
};
NSLog(@"globalBlock2:%@",globalBlock2);
}
2017-04-07 00:19:57.188 KnowBlock[1806:126350] globalBlock:<__NSGlobalBlock__: 0x1062fe0c0>
2017-04-07 00:19:57.189 KnowBlock[1806:126350] globalBlock2:<__NSGlobalBlock__: 0x1062fe100>

栈block:
在MRC下当block内访问了局部变量或者是成员变量时,block的存储区域放到了栈中即栈block。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)blockCategoryForStack {
int a = 11;
void (^stackBlock)() = ^{
NSLog(@"stackBlock executed!a=%zd",a);
};
NSLog(@"stackBlock:%@",stackBlock);
// _aNumber是成员变量
void (^stackBlock2)() = ^{
NSLog(@"stackBlock2 executed!_aNumber=%zd",_aNumber);
};
NSLog(@"stackBlock2:%@",stackBlock2);
}
2017-04-07 09:39:09.025 KnowBlock[888:38708] stackBlock:<__NSStackBlock__: 0x7fff5019d698>
2017-04-07 09:39:09.026 KnowBlock[888:38708] stackBlock2:<__NSStackBlock__: 0x7fff5019d668>

这个效果是将项目设置为MRC才能看到的,ARC下这种创建的block默认的放到堆内存了。

堆block:
在MRC下,当栈内存的block发生了copy的操作是,新copy出来的block对象内存分配在堆中,即堆block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// MRC环境下
- (void)blockCategoryForMRCHeap {
int a = 11;
void (^stackBlock)() = ^{
NSLog(@"stackBlock executed!a=%zd",a);
};
NSLog(@"stackBlock:%@",stackBlock);
void (^heapBlock)() = [stackBlock copy];
NSLog(@"heapBlock:%@",heapBlock);
}
2017-04-07 09:47:54.127 KnowBlock[974:46200] stackBlock:<__NSStackBlock__: 0x7fff5f6d66a8>
2017-04-07 09:47:54.128 KnowBlock[974:46200] heapBlock:<__NSMallocBlock__: 0x600000057d90>

在ARC下,除了全局的block形式,其它的方式声明的block内存分配在堆上面。访问局部变量和成员变量的这些block都是堆block了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ARC环境下
- (void)blockCategoryForARCHeap {
int a = 11;
void (^heapBlock)() = ^{
NSLog(@"heapBlock executed!a=%zd",a);
};
NSLog(@"heapBlock:%@",heapBlock);
void (^heapBlock2)() = ^{
NSLog(@"heapBlock2 executed!_aNumber=%zd",_aNumber);
};
NSLog(@"heapBlock2:%@",heapBlock2);
void (^heapBlock3)() = [heapBlock copy];
NSLog(@"heapBlock3:%@",heapBlock3);
}
2017-04-07 09:55:04.040 KnowBlock[1027:52949] heapBlock:<__NSMallocBlock__: 0x60800005adc0>
2017-04-07 09:55:04.041 KnowBlock[1027:52949] heapBlock2:<__NSMallocBlock__: 0x60800005ad00>
2017-04-07 09:55:04.041 KnowBlock[1027:52949] heapBlock3:<__NSMallocBlock__: 0x60800005adc0>

捕获值处理

block除了具有代码保持能力之外还有捕获块内访问的变量,对于不同类型的变量其捕获处理也不一样。
栈局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 演示捕获存储在栈区的变量
- (void)captureStackVar {
int j = 10;
void (^aBlock)() = ^{
NSLog(@"aBlock j=%zd",j);
};
j++;
aBlock();
}
2017-04-07 11:22:07.775 KnowBlock[1605:117309] aBlock j=10

很显然我们aBlock是在j++之后调用的,按照我们的意思应该打印11。而log打印所示j=10,这里我们解释下:block在捕获局部变量的时候是将这个变量复制了一份放到了block捕获列表中,所以j还是一开始捕获的数据,没有被改变过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 演示尝试在block中修改j报错
- (void)captureStackVar2 {
int j = 10;
void (^bBlock)() = ^{
// 这里编译不通过,不能在block中修改j变量
j++;
NSLog(@"bBlock j=%zd",j);
};
bBlock();
}
// 使用__block修饰j就可以修改了
- (void)captureStackVar3 {
__block int j = 10;
void (^aBlock)() = ^{
NSLog(@"aBlock j=%zd",j);
};
j++;
aBlock();
void (^bBlock)() = ^{
j++;
NSLog(@"bBlock j=%zd",j);
};
bBlock();
}
2017-04-07 13:51:12.358 KnowBlock[1722:136487] aBlock j=11
2017-04-07 13:51:12.359 KnowBlock[1722:136487] bBlock j=12

上面代码主要演示了block对捕获的局部变量进行修改情况,进而引出block不可对捕获的局部变量修改的问题。那为什么不能直接修改呢?我们想个问题:通常block是用于回调,也就是说存在回调回来了而我的局部变量都已经释放掉了,那我再更改这个变量的时候就会发生野指针等问题,所以block不能直接修改局部变量。

__block关键字表示你要在block中对这个变量进行修改,一旦发现block代码引用了这个变量会将这个变量拷贝一份到堆内存上,block拿到的是堆内存变量的地址,这样block没有释放它,它就能够被正常更改。下面是我们对captureStackVar2方法修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (void)captureStackVar2 {
// 此时j地址指向栈区内存
__block int j = 10;
NSLog(@"j = %p",&j);
void (^aBlock)() = ^{
NSLog(@"aBlock j=%zd",j);
NSLog(@"j = %p",&j);
};
// 上面的aBlock引用了j,使得j copy到堆内存,并更改j现在指向为堆内存
j++;
NSLog(@"j = %p",&j);
aBlock();
void (^bBlock)() = ^{
j++;
NSLog(@"bBlock j=%zd",j);
NSLog(@"j = %p",&j);
};
bBlock();
}
2017-04-07 15:02:20.401 KnowBlock[8658:205960] j = 0x7fff58c686c8
2017-04-07 15:02:20.402 KnowBlock[8658:205960] j = 0x600000030138
2017-04-07 15:02:20.402 KnowBlock[8658:205960] aBlock j=11
2017-04-07 15:02:20.402 KnowBlock[8658:205960] j = 0x600000030138
2017-04-07 15:02:20.402 KnowBlock[8658:205960] bBlock j=12
2017-04-07 15:02:20.402 KnowBlock[8658:205960] j = 0x600000030138

请观察下打印日志,j的指针发生了变化。其实在block的捕获对象中,局部变量的处理是最麻烦的了。涉及到捕获变量的复制或者__block关键字的相关底层原理,所以这里还是推荐大家多看几遍局部变量的处理以及代码演示。

全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 全局变量i
int i = 10;
- (void)captureGlobalVar {
void (^aBlock)() = ^{
NSLog(@"aBlock i=%zd",i);
};
i++;
aBlock();
void (^bBlock)() = ^{
i++;
NSLog(@"bBlock i=%zd",i);
};
bBlock();
2017-04-07 15:34:49.310 KnowBlock[8847:222829] aBlock i=11
2017-04-07 15:34:49.311 KnowBlock[8847:222829] bBlock i=12
}

捕获的全局变量是可以在block直接修改的,毕竟全局变量不会面临被销毁的情况,block内部是对它有特殊处理的。
静态变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)captureStaticVar {
static int i = 10;
void (^aBlock)() = ^{
NSLog(@"aBlock i=%zd",i);
};
i++;
aBlock();
void (^bBlock)() = ^{
i++;
NSLog(@"bBlock i=%zd",i);
};
bBlock();
2017-04-07 15:36:25.156 KnowBlock[9023:237530] aBlock i=11
2017-04-07 15:36:25.157 KnowBlock[9023:237530] bBlock i=12
}

静态变量的捕获和全局变量捕获表现一致。

成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@property (nonatomic,assign)int aNumber;
- (void)capturePropertyVar {
_aNumber = 10;
void (^aBlock)() = ^{
NSLog(@"aBlock _aNumber=%zd",_aNumber);
};
_aNumber++;
aBlock();
void (^bBlock)() = ^{
_aNumber++;
NSLog(@"bBlock _aNumber=%zd",_aNumber);
};
bBlock();
2017-04-07 15:44:52.453 KnowBlock[9081:245736] aBlock _aNumber=11
2017-04-07 15:44:52.453 KnowBlock[9081:245736] bBlock _aNumber=12
}

表面上我们捕获了self内部的成员变量,而实质上捕获成员变量的同时也捕获了self,这种用法要注意避免循环引用的情况。当我们尝试在block内部修改成员变量时其实是通过self->_aNumber进行了值的更新。

循环引用

循环引用在block编程中也算是经常遇到的问题了,这里专门讲述下造成循环引用的原因以及对这种问题的解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// self的成员变量sumBlock
@property (nonatomic,copy)void (^sumBlock)(int a,int b);
- (void)blockForCycleReferenceProblem {
_sumBlock = ^(int a,int b) {
// 这里编译器给出了警告:capturing 'self' strongly in this block is likely to lead to a retain cycle
self.aNumber = a+b;
NSLog(@"self.aNumber:%zd",self.aNumber);
};
_sumBlock(1,1);
}

上面代码存在着循环引用,在self中存在的这个_sumBlock为self所拥有,而在声明_sumBlock的代码块中明显捕获到了self,并将self放到了捕获列表中即_sumBlock拥有self,所以导致了循环引用!

知道问题了该怎么解决呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)blockForCycleReferenceSolution {
// weak化self
__weak typeof(self) weakSelf = self;
_sumBlock = ^(int a,int b) {
weakSelf.aNumber = a+b;
NSLog(@"self.aNumber:%zd",weakSelf.aNumber);
};
_sumBlock(1,1);
}

解决办法如上所示,其实循环引用这种问题打断这个闭环就能够解决,我们这面是在block捕获self时做了处理,block捕获的是weak类型的self不会增加self实例对象的引用计数,即block能够使用self的前提是self引用计数不为0,当self引用计数为0时block内部的self则变为nil,实质上block并没有真正拥有self才打破了循环引用。

小结

本文算是对block的使用整体讲述了一遍,其中涉及的很多的代码示例建议读者尽量都去读一遍。在日常开发中也建议大家多使用block,增强对它的认识,避免编程陷阱,毕竟block的回调是那么的优雅、整洁、高效。该博客内容都是凭着自己的理解和debug调试得出的,如果本文留了什么坑还请大家帮忙提醒我纠正下。我再总结block的时候也看了别人对block的认识,也发现了一些好文,这里如果有想对block进行深究的话可以看看唐巧的一篇文章