Block学习笔记

一、概述

闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」;

Block实质是Objective-C对闭包的对象实现,简单说来,Block就是对象。

二、Block的声明

1.有参数有返回值

1
2
3
int (^CustomBlock1)(int) = ^int (int a) {
return a + 1;
};

2.有参数无返回值

1
2
3
4
5
6
7
8
void (^CustomBlock)(int) = ^void (int a) {
NSLog(@"-有参数无返回值--参数:%d", a);
};

// 也可以简写
void (^CustomBlock1)(int) = ^(int a) {
NSLog(@"-有参数无返回值--参数:%d", a);
};

3. 无参数有返回值

1
2
3
4
5
6
7
int (^CustomBlock)(void) = ^int(void) {
return 1;
};
// 也可以简写
int (^CustomBlock1)(void) = ^int {
return 1;
};

4. 无参数无返回值

1
2
3
4
5
6
7
8
void (^CustomBlock)(void) = ^void (void) {
NSLog(@"-无参数无返回值--");
};
// 也可以简写
void (^CustomBlock1)(void) = ^(void){
NSLog(@"-无参数无返回值--");
};

5. 利用 typedef 声明block

1
2
3
4
5
6
7
8
// 利用 typedef 声明block
typedef return_type (^BlockTypeName)(var_type);

// 例子1:作属性
@property (nonatomic, copy) BlockTypeName blockName;

// 例子2:作方法参数
- (void)requestForSomething:(Model)model handle:(BlockTypeName)handle;

三、Block捕获变量及对象

1、变量的定义

  • 全局变量
    • 函数外面声明
    • 可以跨文件访问
    • 可以在声明时赋上初始值。如果没有赋初始值,系统自动赋值为0
    • 存储位置:既非堆,也非栈,而是专门的【全局(静态)存储区static】!
  • 静态变量
    • 函数外面或内部声明(即可修饰原全局变量亦可修饰原局部变量)
    • 仅声明该变量的文件可以访问
    • 可以在声明时赋上初始值。如果没有赋初始值,系统自动赋值为0
    • 存储位置:既非堆,也非栈,而是专门的【全局(静态)存储区static】!
  • 局部变量(自动变量)
    • 函数内部声明
    • 仅当函数执行时存在
    • 仅在本文件本函数内可访问
    • 存储位置:自动保存在函数的每次执行的【栈帧】中,并随着函数结束后自动释放,另外,函数每次执行则保存在【栈】中

2、Block捕获变量

将Objective-C 转 C++的方法

1、在OC源文件block.m写好代码。

2、打开终端,cd到block.m所在文件夹。

3、输入clang -rewrite-objc block.m,就会在当前文件夹内自动生成对应的block.cpp文件。

OC代码:

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
int global_val = 10; // 全局变量
static int static_global_val = 20; // 全局静态变量

int main() {
typedef void (^MyBlock)(void);

static int static_val = 30; // 静态变量
int val = 40; // 局部变量
int val_unuse = 50; // 未使用的局部变量

MyBlock block = ^{
// 捕获局部变量
NSLog(@"val------------------%d", val);
// 修改局部变量 -> 代码编译不通过
//val = 4000;
// 全局变量
global_val *= 10;
// 全局静态变量
static_global_val *= 10;
// 静态变量
static_val *= 10;
};
val *= 10;
block();
NSLog(@"global_val-----------%d", global_val);
NSLog(@"static_global_val----%d", static_global_val);
NSLog(@"static_val-----------%d", static_val);
}

---输出结果:---
局部变量: val------------------40
全局变量: global_val-----------100
全局静态变量: static_global_val----200
静态变量: static_val-----------300

C++代码:

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
int global_val = 10;
static int static_global_val = 20;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val; // 静态变量 --> 指针
int val; // 局部变量 --> 值

// 在构造函数中,也可以看到 static_val、val被传入
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
int val = __cself->val; // bound by copy

global_val *= 10;
static_global_val *= 10;
(*static_val) *= 10;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8k_cgm28r0d0bz94xnnrr606rf40000gn_T_block_75d081_mi_0, val);
}

// 纪录了block结构体大小等信息
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() {
...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

对于 block 外的变量引用,block默认是将其复制到其数据结构中来实现访问的。也就是说block的自动变量截获只针对block内部使用的自动变量,不使用则不截获,因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大(__main_block_desc_0)。特别要注意的是默认情况下block只能访问不能修改局部变量的值。

捕获局部变量

在Block内部使用了其外部变量,这些变量就会被Block保存。自动变量val虽然被捕获进来了,但是是用 __cself->val来访问的。Block仅仅捕获了val的值,并没有捕获val的内存地址。所以,我们在block外部修改了val的值,在block内部并没有效果。

修改局部变量

代码编译不通过。默认情况下block只能访问不能修改局部变量的值。因为Block仅仅捕获了val的值,并没有捕获val的内存地址,block内部修改值并不会对外部的val生效。可能基于此原因,O这种写法直接编译错误。

修改全局变量&修改全局静态变量

可以直接访问。全局变量和全局静态变量没有被截获到block里面,它们的访问是不经过block的(见__main_block_func_0)

修改静态变量

通过指针访问。访问静态变量(static_val)时,将静态变量的 指针 传递给__main_block_impl_0结构体的构造函数并保存。修改静态变量时,是指针操作,所以可以修改其值。

总结: 由上述Block的变量捕获机制,可以总结出下图:

变量类型 是否捕获到Block内部 传递方式
局部变量 值传递
局部staic变量 指针传递
全局变量 直接访问

3、Block捕获对象

OC代码:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
typedef void (^MyBlock)(void);

NSMutableArray *arr = [[NSMutableArray alloc]init];

MyBlock block = ^{
[arr addObject:@1];
};

block();
NSLog(@"arr.count------------%d", (int)arr.count);
}

C++代码:

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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *arr; // 数组对象 --> 指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, int flags=0) : arr(_arr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *arr = __cself->arr; // bound by copy

((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1));
}
// 相当于retain操作,将对象赋值在对象类型的结构体成员变量中
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 当于release操作
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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};
int main() {
...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

捕获对象

在block中可以修改对象的值,因为捕获对象时,在__main_block_impl_0中可以看到捕获的是指针。

我们可以看到在捕获对象的源码中多了两个函数 __main_block_copy_0 和 __main_block_dispose_0。
这两个函数涉及到Block的存储域及copy操作,在下一节中会说明。

四、三种不同类型的Block

  • 全局Block(_NSConcreteGlobalBlock):存在于全局内存中, 生命周期从创建到应用程序结束,相当于单例。
  • 栈Block(_NSConcreteStackBlock):存在于栈内存中, 超出其作用域则马上被销毁
  • 堆Block(_NSConcreteMallocBlock):存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

1、怎么确定Block的类型?

在上述的源码中,可以看到Block的构造函数__main_block_impl_0中的isa指针指向的是&_NSConcreteStackBlock,它表示当前的Block位于栈区中。

Block类型|是否捕获到Block内部
|:–|:–:|–
_NSConcreteGlobalBlock|没有用到外界变量或只用到全局变量、静态变量
_NSConcreteStackBlock|只用到外部局部变量、成员属性变量,且没有强指针引用
_NSConcreteMallocBlock|有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为堆Block

2、全局Block(_NSConcreteGlobalBlock)

全局Block的生成条件:

  • 定义全局变量的地方有block语法时
1
2
3
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
  • Block不截获的自动变量时
1
2
3
4
int(^block)(int count) = ^(int count) {
return count;
};
block(2);

3、栈Block(_NSConcreteStackBlock)

在生成Block以后,如果这个Block不是全局Block,那么它就是为_NSConcreteStackBlock对象,但是如果其所属的变量作用域名结束,该block就被废弃。在栈上的__block变量也是如此。

4、堆Block(_NSConcreteMallocBlock)

为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把Block复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将Block从栈复制到堆。

Block的复制操作执行的是copy实例方法。不同类型的Block使用copy方法的效果如下表:

Block类型 存储位置 复制效果
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 栈区 从栈区复制到堆区
_NSConcreteMallocBlock 堆区 引用计数加一

5、copy和dispose

C结构体里不能含有被__strong修饰的变量,因为编译器不知道应该何时初始化和废弃C结构体。但是OC的运行时库能够准确把握Block从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过__main_block_copy_0函数和__main_block_dispose_0函数进行的

函数|调用时机
|:–|:–:|–:
copy|栈上的 Block 复制到堆时
dispose|堆上的 Block 被废弃(释放)时

那么什么时候栈上的Block会被复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 将方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

五、Block循环引用

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

1
2
3
4
5
// self 持有 someBlock 对象
self.someBlock = ^(Type var){
// 在Block内部,持有self
[self dosomething];
};

解决方式:

  • 使用 __weak
1
2
3
4
5
6
7
8
9
// weakSelf 对 self进行弱引用
__weak typeof(self) weakSelf = self;

// self 持有 someBlock 对象
self.someBlock = ^(Type var){

// 在Block内部,持有weakSelf
[weakSelf dosomething];
};
  • 使用__block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (instancetype)init {
self = [super init];

__block id blockSelf = self; // blockSelf 持有 self

//self持有someBlock
someBlock = ^{
NSLog(@"self = %@",blockSelf); //someBlock持有blockSelf
blockSelf = nil;
};
return self;
}

- (void)doSomething() {
someBlock();
}

此时,blockSelf 持有 self, self 持有someBlock, 而someBlock持有blockSelf。此时,三者形成了一个循环。如果doSomething不执行,blockSelf不能置为nil,则无法打破这个循环。

一旦执行了doSomething,则循环被打破,对象也就可以被释放。