OC Blocks 学习笔记

什么是Blocks

从语法上看Blocks是带有自动变量(局部变量)的匿名函数,从底层结构上看Blocks本质上也是封装了函数调用以及函数调用环境的OC对象。

匿名函数

1
2
3
void (^blockName)(void) = ^(){
NSLog(@"I am block");
};

这是一个简单的Block,void (^blockName)(void) 声明了Block的返回值/名称/参数,^(){NSLog(@"I am block");};是Block的具体实现,可以看作是一个没有名称的函数,只是比函数多了一个^

截获自动变量值

Block中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写Block中使用的自动变量的值也不会影响Block执行时自动变量的值。

1
2
3
4
5
6
7
8
int value = 10;
void (^blockName)(void) = ^(){
NSLog(@"value = %d",value);
};
value = 20;
blockName();
// 打印结果是10,而非20

带有自动变量(局部变量)的匿名函数”的定义中,带有自动变量是指Block拥有捕获外部变量的功能,在Block中访问一个外部的自动变量时,Block会持用它的临时状态,自动捕获变量值,外部自动变量的变化不会影响它的的状态,所以打印结果是10,而非20。

C语言中当我们声明一个局部变量不添加任何修饰符时,会默认变量为自动变量,相当于添加了auto关键字。这里需要区分自动变量和静态局部变量,静态变量会使用static做修饰。但只要是局部变量就会被截获到Block内部,但访问的方式不同(方式不同具体介绍可见下文)。

自动变量属于动态存储类型,是在动态存储区内分配存储单元的,函数调用结束后存储单元即被释放。而静态局部变量是在静态存储区内分配内存单元,在程序的整个运行期间内都不释放空间。

所以我们在重用UITableViewCell时,常常用静态局部变量修饰CellIdentifier

需要注意的是,不能直接在Block的实现中,给自动变量赋值,不然会报以下错误:

1
2
// 变量不可分配(缺少__block类型说明符)
Variable is not assignable (missing __block type specifier)

可以用__block说明符修饰自动变量,这时自动变量将会变Block内部的成员变量,并且进行内存管理,即可对其赋值,关于__block说明符具体介绍可见下文。

Blocks底层结构

我们可以使用clang(LLVM译码器)转换成C++源码,以此来窥探Block的实质。

在命令行工具(我这里用的是iTerm2)来到指定目录,执行

1
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-11.0.0 main.m

译成iOS 11 arm64 架构下的C++源码。

1
2
3
4
5
// 以最简单了Block为例
void (^myBlock)(void) = ^(){
NSLog(@"I am Block");
};
myBlock();
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
37
38
39
40
41
// 最终转换成的 C++ 代码
// 已删除一些强制转换代码,Block无关代码转换成伪代码
struct __block_impl { // Block结构体
void *isa; // isa 指针
int Flags; // 默认为0
int Reserved; // 保留字段,暂时没用,可能以后会用到
void *FuncPtr; // 执行函数指针
};
struct __main_block_impl_0 { // myBlock 结构体,有意思的发现命名是从0开始递增,0、1、2...
struct __block_impl impl; // C++ 特有语法,表示__main_block_impl_0拥有__block_impl4个成员变量
struct __main_block_desc_0* Desc; // myBlock 的描述信息
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { // 构造函数
impl.isa = &_NSConcreteStackBlock; // isa 指针赋值
impl.Flags = flags; // 默认为0
impl.FuncPtr = fp; // 执行函数指针
Desc = desc; // 描述信息
}
};
// myBlock的执行函数,__cself相当于C++中的this,OC中的self,就想OC方法会自带self和_cmd
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog(@"I am Block"); // 源码并不是这样,但和文本讨论内容无关,所以可以理解为伪代码
}
static struct __main_block_desc_0 { // myBlock的描述
size_t reserved; // 保留字段
size_t Block_size; // myBlock的内存大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
// myBlock的声明指向 __main_block_impl_0 结构体的内存地址
// __main_block_impl_0通过构造函数创建了一个myBlock对象
void (*myBlock)(void) = &__main_block_impl_0(
__main_block_func_0, // myBlock的执行函数地址
&__main_block_desc_0_DATA // myBlock的描述信息
);
// 调用时通过 myBlock 指针找到 __main_block_impl_0 中的执行函数指针,
// 执行函数指针找到函数的实现,再调用该函数,并且把自己作为参数传到函数内部
myBlock->FuncPtr(myBlock);

有源码可以总结到Block实则为以下结构体

1
2
3
4
5
6
7
8
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
size_t reserved;
size_t Block_size;
};

__main_block_impl_0结构体相当于基于objc_object结构体的OC类对象(OC对象内存学习笔记可查看OC类对象内存结构)。就此,可以理解“Blocks本质上也是封装了函数调用以及函数调用环境的OC对象”定义中,Blocks本质是OC对象的说法了。void *FuncPtr;成员变量了Block执行函数的调用地址,所以封装了函数调用也不言而喻了。

截获变量值的本质

上文中说到Block会截获所使用的自动变量的值,现在来具体看一下截获的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int height = 178; // 全局变量
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10; // 自动局部变量
static int number = 10; // 静态局部变量
void (^myBlock)(void) = ^(){
NSLog(@"my age = %d", age); // 访问外部自动局部变量
NSLog(@"my number = %d", number); // 访问外部静态局部变量
NSLog(@"my height = %d", height); // 访问外部全局变量
};
myBlock();
}
return 0;
}
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
// 最终转换成的 C++ 代码
// 已删除一些强制转换代码,Block无关代码转换成伪代码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age; // 保存外部的自动局部变量的值
int *number; // 保存外部的静态局部变量的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_number, int flags=0) : age(_age), number(_number) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
auto int age = 10;
static int number = 10;
void (*myBlock)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
age,
&number
);
myBlock->FuncPtr(myBlock);

首先我们注意到,Block语法表达式中使用的自动局部变量被作为成员变量追加到了__main_block_impl_0结构体中,而全局变量没有。并且自动局部变量保存的是值,而静态局部变量保存的是指针,想一想,静态局部变量并不会被销毁使用只需要保存地址,而自动局部变量有可能会在Block调用前已经销毁掉,所以需要保存其值。

截获变量值总结如下

变量类型 截获到Block内部 访问方式
自动局部变量(auto) YES 值传递
静态局部变量(static) YES 指针传递
全局变量 NO 直接访问

__block说明符

上文提到在Block中修改自动局部变量的值时,编译器就报警告,要求用__block说明符。

首先来看一下允许Block中可以改写值的:

  • 局部静态变量
  • 静态全局变量
  • 全局变量

静态全局变量和全局变量的访问和赋值,与Block外完全相同。但局部静态变量是截获其内存地址,通过内存地址来访问和赋值。

由于自动局部变量会被截获到Block内部,并且Block结构体的就有一个成员变量来存储自动局部变量的值。我们先来看一下用__block修饰的自动局部变量被截获到内部后会怎么样。

1
2
3
4
5
6
__block auto int age = 10; // __block 修饰的自动局部变量
void (^myBlock)(void) = ^(){
int value = age; // 访问外部自动局部变量
age = 30; // 给外部自动局部变量赋值
};
myBlock();
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 最终转换成的 C++ 代码
// 已删除一些强制转换代码,Block无关代码转换成伪代码
struct __Block_byref_age_0 {
void *__isa; // 指针 为0
__Block_byref_age_0 *__forwarding; // __Block_byref_age_0实例的内存地址
int __flags; // 0
int __size; // 内存大小
int age; // age相当于原自动局部变量的成员变量
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // 该指针指向 __block变量的__Block_byref_age_0结构体实例
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // 取出Block的age指针
int value = (age->__forwarding->age); // 通过 __Block_byref_age_0 实例的__forwarding访问age
(age->__forwarding->age) = 30; // 因为 void *__isa 存的是0
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 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};
// 加了__block 后 age 成了结构体实例
// 该实例保存了 age的地址、初始值以及内存大小
__attribute__((__blocks__(byref))) auto __Block_byref_age_0 age = {0,
&age,
0,
sizeof(__Block_byref_age_0),
10};
// Block内部将__Block_byref_age_0结构体实例作为成员变量保存
void (*myBlock)(void) = &__main_block_impl_0(__main_block_func_0,
&__main_block_desc_0_DATA,
&age,
570425344));
myBlock->FuncPtr(myBlock);

Block_byref_age_0 结构体实例的成员变量forwarding持有指向该实例自身的指针,可以实现无论block变量配置在栈上还是堆上都能正确地访问block变量,也就是说forwarding是指向自身的。

怎么实现的?

  • 最初,block变量在栈上时,它的成员变量forwarding指向栈上的__block变量结构体实例。
  • block被复制到堆上时,会将forwarding的值替换为堆上的目标block变量用结构体实例的地址。而在堆上的目标block变量自己的__forwarding的值就指向它自己。

Block类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
// 运行结果如下:
// __NSGlobalBlock__
// __NSGlobalBlock
// NSBlock
// NSObject

首先,可见Block即成自NSObject,这边可以再次验证了Block本质是对象。(之前从Block结构体已经确认)

__NSGlobalBlock又是什么鬼?

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

1
2
3
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )// 内存:数据区域 .data 区
__NSStackBlock__ ( _NSConcreteStackBlock ) // 内存:栈区
__NSMallocBlock__ ( _NSConcreteMallocBlock )// 内存:堆区

堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存

之前在 __main_block_impl_0 结构体中isa赋值时

1
2
3
4
5
6
7
8
9
10
11
void (^myBlock)(void) = ^(){
NSLog(@"I am Block");
};
myBlock();
__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;
}

可见myBlock为__NSGlobalBlock__类型的Block。

三种类型生成的环境

block类型 环境
__NSGlobalBlock__ 没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__调用了copy

每一种类型的block调用copy后的结果如下所示

block的类 副本源的配置存储域 复制效果
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteMallocBlock 引用计数增加

而大多数情况下,编译器会进行判断,自动将block从栈上复制到堆:

  • block作为函数值返回的时候
  • 部分情况下向方法或函数中传递block的时候
    • Cocoa框架的方法而且方法名中含有usingBlock等时。
    • Grand Central Dispatch 的API。

除了这两种情况,基本都需要我们手动复制block。

那么__block变量在Block执行copy操作后会发生什么呢?

  1. 任何一个block被复制到堆上时,__block变量也会一并从栈复制到堆上,并被该Block持有。
  2. 如果接着有其他Block被复制到堆上的话,被复制的Block会持有block变量,并增加block的引用计数,反过来如果Block被废弃,它所持有的__block也就被释放(不再有block引用它)。

copy和dispose

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

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

被__block说明符修饰后,会把自动变量包装成对象处理,同时多了两个函数

1
2
3
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

如果block被拷贝到堆上

  • 会调用block内部的copy函数

  • copy函数内部会调用_Block_object_assign函数

  • _Block_object_assign函数会根据auto变量的修饰符(__strong__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用​

如果block从堆上移除

  • 会调用block内部的dispose函数
  • dispose函数内部会调用_Block_object_dispose函数
  • _Block_object_dispose函数会自动释放引用的auto变量(release)

Block循环引用

如果在Block内部使用__strong修饰符的对象类型的自动变量,那么当Block从栈复制到堆的时候,该对象就会被Block所持有。

所以如果这个对象还同时持有Block的话,就容易发生循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef void(^blk_t)(void);
@interface Person : NSObject {
blk_t blk_;
}
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"self = %@",self);
};
}
return self;
}
@end

Block blk_t持有self,而self也同时持有作为成员变量的blk_t

__weak修饰符

1
2
3
4
5
6
7
8
9
10
- (instancetype)init {
self = [super init];
if (self) {
id __weak weakSelf = self;
blk_ = ^{
NSLog(@"self = %@",weakSelf);
};
}
return self;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef void(^blk_t)(void);
@interface Person : NSObject {
blk_t blk_;
id obj_;
}
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"obj_ = %@",obj_);//循环引用警告
};
}
return self;
}

Block语法内的obj截获了self,因为ojb是self的成员变量,因此,block如果想持有obj_,就必须引用先引用self,所以同样会造成循环引用。就好比你如果想去某个商场里的咖啡厅,就需要先知道商场在哪里一样。

如果某个属性用的是weak关键字呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface Person()
@property (nonatomic, weak) NSArray *array;
@end
@implementation Person
- (instancetype)init {
self = [super init];
if (self) {
blk_ = ^{
NSLog(@"array = %@",_array);//循环引用警告
};
}
return self;
}

还是会有循环引用的警告提示,因为循环引用的是self和block之间的事情,这个被Block持有的成员变量是strong还是weak都没有关系,而且即使是基本类型(assign)也是一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Person()
@property (nonatomic, assign) NSInteger index;
@end
@implementation Person
- (instancetype)init
{
self = [super init];
blk_ = ^{
NSLog(@"index = %ld",_index);//循环引用警告
};
return self;
}

__block修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (instancetype)init
{
self = [super init];
__block id temp = self;//temp持有self
//self持有blk_
blk_ = ^{
NSLog(@"self = %@",temp);//blk_持有temp
temp = nil;
};
return self;
}
- (void)execBlc
{
blk_();
}

所以如果不执行blk_(将temp设为nil),则无法打破这个循环。

一旦执行了blk_,就只有

  • self持有blk_
  • blk_持有temp

使用__block 避免循环比较有什么特点呢?

  • 通过__block可以控制对象的持有时间。
  • 为了避免循环引用必须执行block,否则循环引用一直存在。

所以我们应该根据实际情况,根据当前Block的用途来决定到底用block,还是weak或__unsafe_unretained。

Blocks语法

  • returnType 表示返回的对象/关键字等(可以是void,并省略)
  • blockName 表示block的名称
  • parameterTypes 表示参数的类型(可以是void,并省略)
  • parameters 表示参数

作为局部变量

1
2
3
4
5
6
7
// 返回值类型(^名称)(参数的类型) = ^返回值类型(参数) {...}
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
// 省略返回值类型 & 参数的类型
void (^blockName)(void) = ^void(void) {...};
// 也可以省略block实现中的void,更加精简的写法
void (^blockName)(void) = ^() {...};

For example

1
2
3
4
5
6
7
// 声明并实现一个局部变量Block
int (^addBlock)(int, int) = ^(int a, int b){
return a + b;
};
// 调用局部变量Block
int results = addBlock(1,1);

作为属性

1
2
3
// 语法相当于作为局部变量时的声明,作为属性后升级为全局变量
// 返回值类型(^名称)(参数的类型)
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);

注意:这里使用了copy修饰符,why?

For example

1
2
3
4
5
6
7
8
9
10
// 将上一个例子中的局部变量升级为全局变量
@property (nonatomic, copy, nullable) int (^addBlock)(int, int);
// 实现全局变量
_addBlock = ^int (int a, int b) {
return a + b;
};
// 调用局部变量Block
int results = addBlock(1,1);

作为方法参数/方法调用的参数

1
2
3
4
5
// 返回值类型(^)(参数的类型)
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
// ^返回值类型(参数)
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];

For example

1
2
3
4
5
6
7
8
9
10
// 上一个例子中的属性,会自动生成一个Set方法
- (void)setAddBlock:(int (^)(int, int))addBlock;
// 调用方法
[self setAddBlock:^int(int a, int b) {
return a + b;
}];
// 可见在定义方法时,Block作为形参
// 在调用方法时,Block作为实参

作为返回值

1
2
// 返回值类型(^)(参数的类型)
- (returnType(^nullability)(parameterTypes))methodName;

For example

1
2
3
4
5
6
7
// 依旧是之前作为属性的Block
@property (nonatomic, copy) int(^addBlock)(int, int);
// 该属性的 Get 方法
- (int (^)(int, int))addBlock {
return _addBlock;
}

在链式编程时,核心就是将Block作为返回值,达到链式调用的效果。

typedef

typedef是C中的关键字,它的主要作用是给一个数据类型定义一个新的名称

1
2
3
4
5
// typedef 返回值类型 (^类型名称)(参数类型);
typedef returnType (^TypeName)(parameterTypes);
// 类型名称 block名称 = ^返回值类型(参数) {...};
TypeName blockName = ^returnType(parameters) {...};

在Xcode中键入typedefBlock,即可快速生成语法

For example

1
2
3
4
5
6
7
8
9
10
// 定义一个 Block 类型
typedef int(^RYJAddBlock)(int a, int b);
// 声明 Block 属性
@property (nonatomic, copy) RYJAddBlock addBlock;
// 给_addBlock成员变量赋值
_addBlock = ^(int a, int b) {
return a + b;
};

⚠️在声明 Block 时往往只声明参数类型,但在一些知名第三方,以及Apple官方的一些写法中,会声明参数名称,个人认为这样会使代码更加便于阅读,所以建议在声明Block时声明完整的参数类型及其名称。

Block应用场景

事件回调 & 数据传递

时间回调和数据传递通常会一起使用,For example

1
2
3
4
5
- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure

AFNetworking框架中Get请求的回调,同时传递请求到的数据。

我们也可以单独使用事件回调

1
2
3
4
5
6
7
8
9
10
11
12
typedef void(^RYJWeekDaysUpdateBlock)(void);
// 声明一个Block
@property (nonatomic, copy) RYJWeekDaysUpdateBlock weekDaysUpdate;
// 在恰当时间回调
if (_weekDaysUpdate) {
_weekDaysUpdate();
}
// 实现Block
weekDaysUpdate = ^{
// DO SOMETHING...
};

所以是事件回调还是数据传递,需要根据需求判断。

链式语法

链式编程思想:核心思想为将block作为方法的返回值,且返回值的类型为调用者本身,并将该方法以setter的形式返回,这样就可以实现了连续调用,即为链式编程。

Masonry的一个典型的链式编程用法如下:

1
2
3
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.mas_left).mas_offset(110);
}];

参考资料

《Objective-C 高级编程 iOS与OSX多线程和内存管理》

《Blocks Programming Topics》

How Do I Declare A Block in Objective-C?