block是一个OC对象, 它的功能是保存代码片段, 预先准备好代码, 并在需要的时候执行.

目录

  1. Block语法
  2. Block类型变量
  3. __block说明符号
  4. Block实现原理
    1. Block结构
  5. Block的存储域
  6. Block复制
  7. 使用__block发生了什么
  8. Block的执行函数
  9. Block的循环引用

Block语法

Block表达式语法:
^ 返回值类型 (参数列表) {表达式}

1
2
3
4
5
6
7
^ int (int a) {
return a + 1;
};
^{
NSLog(@"最简模式Block");
};

Block类型变量

声明Block类型变量语法:
返回值类型 (^变量名)(参数列表) = Block表达式

1
2
3
4
5
int (^myBlock)(int,int) = ^ int (int a,int b) {
return a+b;
};
int c = myBlock(2,3);
NSLog(@"c=%d",c);

当Block类型变量作为函数的参数时,写作:
返回值类型 (^)(参数列表) block变量名

1
2
3
4
5
6
7
[self testParmWithBlock:^int(int a, int b) {
return a+b;
}];
- (void)testParmWithBlock:(int(^)(int a,int b))block {
block(2,3);
NSLog(@"%@",[block class]);
}

借助typedef可简写:
typedef <#returnType#>(^<#name#>)(<#arguments#>);

1
2
3
4
5
6
7
typedef int(^TypedefBlock)(int,int);
[self testParmWithTypedefBlock:^int(int a, int b) {
return a+b;
}];
- (void)testParmWithTypedefBlock: (TypedefBlock)myTypedefBlock{
myTypedefBlock(2,3);
}

截获自动变量值
Block表达式可截获所使用的自动变量的值。
截获:保存自动变量的瞬间值。
因为是“瞬间值”,所以声明Block之后,即便在Block外修改自动变量的值,也不会对Block内截获的自动变量值产生影响。
例如:

1
2
3
4
5
6
7
int i=10;
void(^myAutomaticVariable)(void) = ^{
NSLog(@"In block, i = %d", i);
};
i = 20;//Block外修改变量i,也不影响Block内的自动变量
myAutomaticVariable();//i修改为20后才执行,打印: In block, i = 10
NSLog(@"i = %d", i);//打印:i = 20

__block说明符号

自动变量截获的值为Block声明时刻的瞬间值,保存后就不能改写该值,如需对自动变量进行重新赋值,需要在变量声明前附加__block说明符,这时该变量称为__block变量。

自动变量值为一个对象情况
当自动变量为一个类的对象,且没有使用__block修饰时,虽然不可以在Block内对该变量进行重新赋值,但可以修改该对象的属性。
如果该对象是个Mutable的对象,例如NSMutableArray,则还可以在Block内对NSMutableArray进行元素的增删:

1
2
3
4
5
6
7
8
9
NSMutableArray *varMuArr = [@[@"1",@"2"] mutableCopy];
NSLog(@"varMuArr Count:%ld", varMuArr.count);//打印varMuArr Count:2
void (^muArrBlock)(NSString *) = ^(NSString *str) {
[varMuArr addObject:str]; //可以修改属性
//varMuArr = [@[@"5"] mutableCopy]; 不能赋值 Variable is not assignable (missing __block type specifier)
};
muArrBlock(@"3");
NSLog(@"varMuArr Count:%ld", varMuArr.count);//打印varMuArr Count:3
NSLog(@"%@",[muArrBlock class]);//打印:__NSMallocBlock__

Block实现原理

使用Clang
Block实际上是作为极普通的C语言源码来处理的:含有Block语法的源码首先被转换成C语言编译器能处理的源码,再作为普通的C源代码进行编译。
使用LLVM编译器的clang命令可将含有Block的Objective-C代码转换成C++的源代码,以探查其具体实现方式:
clang -rewrite-objc 源码文件名

Block结构

使用Block的时候,编译器对Block语法进行了怎样的转换?
如上所示的最简单的Block使用代码,经clang转换后,可得到以下几个部分(有代码删减和注释添加):
__ViewController__viewDidLoad_block_impl_6即为viewDidLoad()函数栈上的Block结构体
将上面的代码段clang,发现Block的结构体__ViewController__viewDidLoad_block_impl_6结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
struct __ViewController__viewDidLoad_block_impl_6 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_6* Desc;
NSMutableArray *varMuArr;
__ViewController__viewDidLoad_block_impl_6(void *fp, struct __ViewController__viewDidLoad_block_desc_6 *desc, NSMutableArray *_varMuArr, int flags=0) : varMuArr(_varMuArr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

Block有三种类(即__block_impl的isa指针指向的值,isa说明参考《Objective-C isa 指针 与 runtime 机制》),根据Block对象创建时所处数据区不同而进行区别:

_NSConcreteStackBlock:在栈上创建的Block对象
_NSConcreteMallocBlock:在堆上创建的Block对象 出了作用域后引用会复制到堆区
_NSConcreteGlobalBlock:全局数据区的Block对象 不引用外部变量
观察上节代码中__ViewController__viewDidLoad_block_impl_6结构体(viewDidLoad栈上Block的结构体)的构造函数可以看到,栈上的变量_varMuArr以参数的形式传入到了这个构造函数中,此处即为变量的自动截获。变量被保存在Block的结构体实例中。

所以在muArrBlock()执行之前,栈上简单数据类型的_varMuArr无论发生什么变化,都不会影响到Block以参数形式传入而捕获的值。但这个变量是指向对象的指针时,是可以修改这个对象的属性的,只是不能为变量重新赋值。

Block的存储域

上文已提到,根据Block创建的位置不同,Block有三种类型,创建的Block对象分别会存储到栈、堆、全局数据区域。

1
2
3
4
5
void (^globalBlock)(void) = ^{
NSLog(@"Global Block");
};
globalBlock();
NSLog(@"%@",[globalBlock class]);//打印:__NSGlobalBlock__

像上面代码块中的全局globalBlock自然是存储在全局数据区,但注意在函数栈上创建的blk,如果没有截获自动变量,Block的结构实例还是会被设置在程序的全局数据区,而非栈上:

1
2
3
4
5
6
int s = 1;
void (^stackBlock)(void) = ^{
NSLog(@"s=%d",s);
};
stackBlock();
NSLog(@"%@",[stackBlock class]);//打印:__NSMallocBlock__

可以看到截获了自动变量的Block打印的类是__NSGlobalBlock__,表示存储在全局数据区。
但为什么捕获自动变量的Block打印的类却是设置在堆上的__NSMallocBlock__,而非栈上的__NSStackBlock__?这个问题稍后解释。

Block复制

配置在栈上的Block,如果其所属的栈作用域结束,该Block就会被废弃,对于超出Block作用域仍需使用Block的情况,Block提供了将Block从栈上复制到堆上的方法来解决这种问题,即便Block栈作用域已结束,但被拷贝到堆上的Block还可以继续存在。
复制到堆上的Block,将_NSConcreteMallocBlock类对象写入Block结构体实例的成员变量isa:
impl.isa = &_NSConcreteMallocBlock;

在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码(或者直接在堆上创建Block对象),以下几种情况栈上的Block会自动复制到堆上:

调用Block的copy方法
将Block作为函数返回值时(MRC时此条无效,需手动调用copy) 这个有问题
将Block赋值给__strong修改的变量时(MRC时此条无效)
向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时

其它时候向方法的参数中传递Block时,需要手动调用copy方法复制Block。
上一节的栈上截获了自动变量i的Block之所以在栈上创建,却是_NSMallocBlock__类,就是因为这个Block对象赋值给了__strong修饰的变量**captureBlk(_strong是ARC下对象的默认修饰符)。
因为上面四条规则,在ARC下其实很少见到_NSConcreteStackBlock类的Block,大多数情况编译器都保证了Block是在堆上创建的,如下代码所示,仅最后一行代码直接使用一个不赋值给变量的Block,它的类才是__NSStackBlock__

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
typedef int(^TypedefBlock)(int,int);
typedef NSString *(^TypedefStringBlock)(NSString *,NSString *);
int m=3;
int n=4;
TypedefBlock rblock = [self testReturnBlock];
int rVar = rblock(m,n);
NSLog(@"%@",[rblock class]);//打印:__NSGlobalBlock__
NSLog(@"rVar=%d",rVar);
TypedefStringBlock strBlock = [self testReturnStrBlock];
NSString *strVar = strBlock(@"String",@"Block");
NSLog(@"%@",[strBlock class]);//打印:__NSGlobalBlock__
NSLog(@"str=%@",strVar);
int count = 0;
NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);
- (TypedefBlock)testReturnBlock {
int (^returnBlock)(int,int) = ^(int a,int b){
return a+b;
};
//globalBlock();
return returnBlock;
}
- (TypedefStringBlock)testReturnStrBlock {
NSString * (^returnBlock)(NSString *,NSString *) = ^(NSString * a,NSString * b){
return [a stringByAppendingString:b];
};
//globalBlock();
return returnBlock;
}

使用__block发生了什么

Block捕获的自动变量添加__block说明符,就可在Block内读和写该变量,也可以在原来的栈上读写该变量。
自动变量的截获保证了栈上的自动变量被销毁后,Block内仍可使用该变量。
__block保证了栈上和Block内(通常在堆上)可以访问和修改“同一个变量”,__block是如何实现这一功能的?
__block发挥作用的原理:将栈上用__block修饰的自动变量封装成一个结构体,让其在堆上创建,以方便从栈上或堆上访问和修改同一份数据。

现在对刚才的代码段,加上__block说明符,并在block内外读写变量ivar。

1
2
3
4
5
6
7
8
__block int ivar=10;
void(^myChangeBlockVar)(void) = ^{
ivar = 20;
NSLog(@"In block, ivar = %d", ivar);//打印:In block, ivar = 20
};
ivar++;
NSLog(@"Out Block: ivar = %d", ivar);//Out Block: ivar = 11
myChangeBlockVar();
1
2
3
4
5
6
7
8
9
10
11
struct __ViewController__viewDidLoad_block_impl_10 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_10* Desc;
__Block_byref_ivar_0 *ivar; // by ref
__ViewController__viewDidLoad_block_impl_10(void *fp, struct __ViewController__viewDidLoad_block_desc_10 *desc, __Block_byref_ivar_0 *_ivar, int flags=0) : ivar(_ivar->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

最大的变化就是ivar变量不再是int类型了,ivar变成了一个指向__Block_byref_ivar_0结构体的指针,__Block_byref_ivar_0结构如下:

1
2
3
4
5
6
7
struct __Block_byref_ivar_0 {
void *__isa;
__Block_byref_ivar_0 *__forwarding;
int __flags;
int __size;
int ivar;
};

它保存了int ivar变量,还有一个指向__Block_byref_ivar_0实例的指针__forwarding,通过下面两段代码__forwarding指针的用法可以知道,该指针其实指向的是对象自身:

Block的执行函数

1
2
3
4
5
6
static void __ViewController__viewDidLoad_block_func_10(struct __ViewController__viewDidLoad_block_impl_10 *__cself) {
__Block_byref_ivar_0 *ivar = __cself->ivar; // bound by ref
(ivar->__forwarding->ivar) = 20; //对应count = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_mb_gqftsnzd5694x3wr5glk3sfm0000gn_T_ViewController_609dca_mi_22, (ivar->__forwarding->ivar));
}
1
2
3
4
5
6
7
8
9
__attribute__((__blocks__(byref))) __Block_byref_ivar_0 ivar = {(void*)0,(__Block_byref_ivar_0 *)&ivar, 0, sizeof(__Block_byref_ivar_0), 10};
void(*myChangeBlockVar)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_10((void *)__ViewController__viewDidLoad_block_func_10, &__ViewController__viewDidLoad_block_desc_10_DATA, (__Block_byref_ivar_0 *)&ivar, 570425344));
(ivar.__forwarding->ivar)++; //对应count ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_mb_gqftsnzd5694x3wr5glk3sfm0000gn_T_ViewController_609dca_mi_23, (ivar.__forwarding->ivar));
((void (*)(__block_impl *))((__block_impl *)myChangeBlockVar)->FuncPtr)((__block_impl *)myChangeBlockVar);

为什么要通过__forwarding指针完成对ivar变量的读写修改?
为了保证无论是在栈上还是在堆上,都能通过都__forwarding指针找到在堆上创建的ivar这个__ViewController__viewDidLoad_block_func_10结构体,以完成对ivar->ivar(第一个ivar是__ViewController__viewDidLoad_block_func_10对象,第二个ivar是int类型变量)的访问和修改。

Block的循环引用

Block的循环引用原理和解决方法大家都比较熟悉,此处将结合上文的介绍,介绍一种不常用的解决Block循环引用的方法和一种借助Block参数解决该问题的方法。
Block循环引用原因:一个对象A有Block类型的属性,从而持有这个Block,如果Block的代码块中使用到这个对象A,或者仅仅是用用到A对象的属性,会使Block也持有A对象,导致两者互相持有,不能在作用域结束后正常释放。
解决原理:对象A照常持有Block,但Block不能强引用持有对象A以打破循环。
解决方法:
方法一: 对Block内要使用的对象A使用__weak进行修饰,Block对对象A弱引用打破循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__block ViewController* weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
使用__weak typeof(self)
__weak typeof(self) weakSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",weakSelf);
};
使用宏
#define Weakify(x) typeof(x) __weak weak##x = x;
#define Strongfiy(x) typeof(x) __strong x = weak##x;
@Weakify(self);
self.blk = ^{
@Strongfiy(self);
NSLog(@"In Block : %@",self);
};

方法二:对Block内要使用的对象A使用__block进行修饰,并在代码块内,使用完__block变量后将其设为nil,并且该block必须至少执行一次。

1
2
3
4
5
__block XXController *blkSelf = self;
self.blk = ^{
NSLog(@"In Block : %@",blkSelf);
blkSelf = nil;//不能省略
};

在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:
XXController对象持有Block对象blk
blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
__block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环

第二种使用__block打破循环的方法,优点是:
可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题

其缺点也明显:
必须手动保证__block变量最后设置为nil
block必须执行一次,否则__block不为nil循环应用仍存在
self.blk();//该block必须执行一次,否则还是内存泄露
因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用。

方法三:将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露。

1
2
3
4
self.blk = ^(UIViewController *vc) {
NSLog(@"Use Property:%@", vc.name);
};
self.blk(self);

优点:
简化了两行代码,更优雅
更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环