iOS开发-消息传递方式-Block

说过了target-action

说过了KVO

说过了NotificationCenter

这次我们来说一个一对一的消息传递方式,Block

Block是什么

Block,很多语言中翻译做闭包,用《Objective-C高级编程》中的话说:

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的额匿名函数。

所以,Block就是一个带有自动变量匿名函数

匿名函数

顾名思义,匿名函数就是没有名称的函数,C语言是不允许出现没有函数名的函数的,但是因为实际上调用函数也是调用指向函数的函数指针,但是没有函数名,就没办法获取到函数的指针。那么block具体是怎么来实现的呢?我们先往下看。

自动变量

在栈上声明一个变量如果不是静态变量或全局变量,是不可以在这个栈内声明的匿名函数中使用的,但是在block中却可以。

Block结构

在Xcode里,我们敲入快捷键inlineBlock就会看到有这样一个block的样式提供给我们。

1
2
3
4
//增加了returnType,可省略
returnType(^blockName)(parameterTypes) = ^returnType(parameters) {
statements
};

其中第二个returnType是我加上去的,为了能看的明显一点。

但是一下子看到一个这样的东西还是有点乱,我们把它拆分成声明部分和实现部分来看就会清楚很多了。

声明Block

1
returnType(^blockName)(parameterTypes)

声明block中包括了返回类型、^、block名称、参数列表。

实现Block

1
2
3
^returnType(parameters) {
statements
};

实现block中包括了返回类型(可省略)、参数列表(可省略)、实现代码。

其中返回值类型可以省略。

1
2
3
^(parameters) {
statements
};

参数列表也可以省略。

1
^{statements};

Block的使用

因为block的传入参数和返回值都可以为空,所以Block的使用可以分为4中模式:

  • 1.无参数、无返回值。
  • 2.有参数、无返回值。
  • 3.无参数、有返回值。
  • 4.有参数、有返回值。

接下来我们就来举例子看看这几种方式的使用。

无参数、无返回值

1
2
3
4
5
6
7
8
9
//无参数、无返回值
- (void)blockWithoutParameterAndWithoutReturn
{
void(^noParameterNoReturn)(void) = ^(void){
NSLog(@"无参数、无返回值");
};
noParameterNoReturn();
}

有参数、无返回值

1
2
3
4
5
6
7
8
9
//有参数、无返回值
- (void)blockWithParameterAndWithoutReturn
{
void(^parameterNoReturn)(NSInteger number) = ^(NSInteger number){
NSLog(@"有参数、无返回值,参数是%lu",number);
};
parameterNoReturn(10);
}

无参数、有返回值

1
2
3
4
5
6
7
8
9
10
11
12
//无参数、有返回值
- (void)blockWithoutParameterAndWithReturn
{
NSInteger(^noParameterReturn)(void) = ^{
NSInteger number = 20;
NSLog(@"无参数、有返回值,返回值是%lu",number);
return number;
};
NSInteger number = noParameterReturn();
NSLog(@"返回值是%lu",number);
}

有参数、有返回值

1
2
3
4
5
6
7
8
9
10
11
//有参数、有返回值
- (void)blockWithParameterAndWithReturn
{
NSInteger(^parameterAndReturn)(NSInteger numberA, NSInteger numberB) = ^(NSInteger numberA, NSInteger numberB){
NSLog(@"有参数、有返回值,参数是%lu、%lu,返回值是%lu",numberA,numberB,numberA+numberB);
return numberA+numberB;
};
NSInteger numberSum = parameterAndReturn(30,40);
NSLog(@"返回值是%lu",numberSum);
}

使用typedef定义

除了上边的常规操作之外,block还可以作为OC中的一个参数,这时候可以用到typedef来定义一个block,然后在函数调用时进行参数传递。

比如先定义一个block参数:

1
2
//number作为参数,无返回值
typedef void(^typedefBlock)(NSInteger number);

然后声明一个函数中带有此变量

1
2
3
4
5
6
7
//typedef block
- (void)testTypedefBlockWith:(typedefBlock)testTypedefBlock
{
NSLog(@"开始使用typedef block");
testTypedefBlock(12);
NSLog(@"结束使用typedef block");
}

这时候调用此方法,在回调的方法中就可以获取到传递过来的值。

1
2
3
4
5
[self testTypedefBlockWith:^(NSInteger number) {
NSLog(@"回调 typedef block number %lu",number);
}];

Block与外界变量

默认情况

通常情况下,对于block外的变量引用,block默认是将其复制到block的数据结构中实现访问的,也就是说只有block中用到的变量,block才会把他自动截获进来,而且因为截取的是瞬时值,所以之后在外部改变变量的值也不会改变值得大小。因为截获自动变量会存储在block内部,所以会导致block体积变大。

另外需要注意的一点就是block内部只能调用getter方法,不可以调用setter方法,所以是没办法修改外部变量的值的。

logo

比如:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)autoParamterTest
{
NSInteger number = 100;
void(^autoParamter)(void) = ^(void){
NSLog(@"%lu",number); //输出100
};
number = 200;
autoParamter();
}

这段代码最后会输出100,因为在定义block时,他已经把number的值复制到block中了,所以再改变他,对block中的值也不会有影响。

另外,在block中对number赋值时,编译器会直接报错。

logo

__block

对于这种情况,OC提供了block(两个下划线_)来修饰外部变量,使用了block修饰的外部变量,block内部是复制其引用地址来实现访问数据的,所以block内部可以修改block外部的变量值。

logo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)autoBlockParameterTest
{
__block NSInteger number = 100;
void(^autoBlockParamter)(void) = ^(void){
NSLog(@"%lu",number); //输出200
number = 300;
NSLog(@"%lu",number); //输出300
};
number = 200;
autoBlockParamter();
}

那为什么在加了 __block修饰符之后就可以访问了呢?后边我们会详细说明,我们先往下看。

Block的循环引用

Block是很好用,但是用不好的时候就容易出现循环引用,比如在某各类将block作为自己的变量,然后又在这个block的方法中使用了这个类自己的东西,这时候两者互相持有就会发生循环引用,引起内存泄漏的问题。比如如下代码:

1
2
3
4
5
6
- (void)blockCircularReference
{
self.circleBlock = ^(NSInteger number) {
[self autoParamterTest];
};
}
logo

但是苹果也给出了相应的解决方案来处理block下的循环引用。

__weak修饰

可以直接用__weak(有两个下划线)来修饰,来打破block中的循环,使用__weak修饰解决循环引用一共有三种实现的方式。

  1. 使用__weak ClassName
1
2
3
4
5
6
7
- (void)blockCircularReference
{
__weak MPBlockViewController *weakSelf = self;
self.circleBlock = ^(NSInteger number) {
[weakSelf autoParamterTest];
};
}
  1. 使用__weak typeof(self)
1
2
3
4
5
6
7
- (void)blockCircularReference
{
__weak typeof (self) weakSelf = self;
self.circleBlock = ^(NSInteger number) {
[weakSelf autoParamterTest];
};
}
  1. 使用Reactive Cocoa中的@weakify和@strongify
1
2
3
4
5
6
7
8
- (void)blockCircularReference
{
@weakify(self);
self.circleBlock = ^(NSInteger number) {
@strongify(self);
[self autoParamterTest];
};
}

@weakify, @strongify的具体使用可以看这里

__block

在MRC下,可以直接使用__block进行修饰。

也可以先用__block修饰,然后在block方法中使用完将其设为nil,但是要注意就是block必须要被调用一次。

1
2
3
4
5
6
7
8
9
10
- (void)blockCircularReference
{
__block MPBlockViewController *blockSelf = self;
self.circleBlock = ^(NSInteger number) {
[blockSelf autoParamterTest];
blockSelf = nil; //必须设为nil
};
self.circleBlock(10); //必须至少调用一次
}

将self作为参数传递

也可以直接将self作为一个参数传递到block中。

1
2
3
4
5
6
- (void)blockCircularReference
{
self.circleBlock = ^(MPBlockViewController *vc) {
[vc autoParamterTest];
};
}

Block的实现

block实际上是用C语言源码来处理的,含有block的源码首先被转换成C语言编译器能够处理的源码,再作为C进行编译。

Clang

使用LLVM编译器的clang可以将OC的代码翻译成C++的源代码,说是C++的代码,但是实际上也就是C语言的源代码。

使用的方式就是打开Terminal,cd到源代码文件目录,输入:

1
clang -rewrite-objc 源代码文件名

比如这样一段代码(这里没有引用其他OC的框架,因为引入之后clang出来的cpp文件会巨大,有好几千行):

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
void (^ blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}

这段简单的block代码clang之后就会变成如下源码(这里删除了部分代码,只显示了重要的部分):

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");}
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() {
void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

乍一看,我天,这都是什么鬼啊 = =。 没事我们一部分一部分来看。

__cself

这里边的参数__cself就相当于C++中指向自身的变量this,在OC中就是self,即参数__cself就是指向block值的变量。

__block_impl

__block_impl是我们要介绍的第一个block中的成员变量,他是一个结构体,其结构如下:

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
  • isa指针,所有对象都有改指针,用于实现对象相关的功能。
  • Flags,用于按bit位表示一些block的附加信息。
  • Reserved,保留变量。
  • FuncPtr,函数指针,指向block要执行的函数,即__main_block_func_0。

__main_block_desc_0

__main_block_desc_0是我们要介绍的第二个block中的成员变量,也是一个结构体,其结构如下:

1
2
3
4
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)};
  • reserved,结构体信息保留字段。
  • Block_size,block的大小。

初始化__main_block_func_0

另外一部分就是__main_block_func_0的初始化,用到的就是之前介绍的两个结构体。

1
2
3
4
5
6
7
8
9
10
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__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;
}
};

实现__main_block_func_0

这里主要就是我们在block中要实现的代码。

1
2
3
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}

实现main函数

另外main函数的源码在这里。

1
2
3
4
5
6
7
int main() {
void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

截获自动变量

看完了block的基本实现方式,那我们再看看他是如何截获自动变量呢?

我们先定义一个number

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
int number = 10;
void (^ blk)(void) = ^{printf("%d",number);};
blk();
return 0;
}

这时候clang之后发现:

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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int number; //number被直接加入了__main_block_impl_0结构体中
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _number, int flags=0) : number(_number) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int number = __cself->number; // bound by copy
printf("%d",number);}
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() {
int number = 10;
void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, number));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

我们看到number被直接加到了__main_block_impl_0结构体中。

__block

这时候我们再用__block来修饰一下number看看:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
__block int number = 10;
void (^ blk)(void) = ^{printf("%d",number);};
blk();
return 0;
}

clang之后发现:

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
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_number_0 *number; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__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_number_0 *number = __cself->number; // bound by ref
printf("%d",(number->__forwarding->number));}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 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};
int main() {
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 10};
void (* blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}

我们只是加了一个__block,结果代码一下子增加了巨多!

这时候仔细看代码,就能发现多了一个段代码:

1
2
3
4
5
__Block_byref_number_0 number = {
(void*)0,(__Block_byref_number_0 *)&number,
0,
sizeof(__Block_byref_number_0),
10};

找到这个结构体的声明:

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

那如果这时候我们给number赋一个新的值会怎么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main() {
__block int number = 10;
void (^ blk)(void) = ^{
number = 20;
printf("%d",number);
};
blk();
return 0;
}

clang后发现多了这里变化(就不贴全部代码了):

1
2
3
4
5
6
7
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_number_0 *number = __cself->number; // bound by ref
(number->__forwarding->number) = 20;
printf("%d",(number->__forwarding->number));
}

我们看到在向block变量赋值时,block的main_block_impl_0结构体实例持有指向block变量的Block_byref_number_0结构体实例的指针。

Block_byref_number_0结构体的实例的成员变量forwarding持有指向该实例自身的指针。并通过成员变量__forwarding访问成员变量val。

那么__forwarding又是什么呢?别急,后边我们再说他。

Block的存储域

在__main_block_func_0的初始化时,我们看到了有一行代码是:

1
impl.isa = &_NSConcreteStackBlock;

从名字应该可以判断出来,这行代码的意思是这个block是存储在栈上的。

那么除了栈,block还存储在哪些地方呢?

设置对象的存储域
_NSConcreteStackBlock 栈块
_NSConcreteGlobalBlock 全局块
_NSConcreteMallocBlock 堆块
logo

从名字就能看出来:

  • 栈块存在栈内存中,超出作用域后就会马上销毁。
  • 全局块在全局内存中,和全局变量一样。
  • 堆块存在堆内存中,是一个带有引用计数的对象,需要自行管理内存。

那么我们怎么样能够知道block是保存在哪里呢?

全局块

一般情况下,当满足以下情况时,block为_NSConcreteGlobalBlock类对象,也就是放在全局数据区。

  • 1.记录全局变量的地方有block语法时。
  • 2.block语法的表达式中不使用应截获的自动变量时。

栈块

理论上,除了在全局块条件之外的情况下,block都为_NSConcreteStackBlock类对象,也就是设置在栈区。

堆块

那么如果这样说起来,岂不是没有block会在堆上了么?

这就要说到一个问题,就是ARC和MRC下block的不同情况,MRC下访问外界变量的block默认就是存储在栈中了,但是ARC下,block会自动被从栈区拷贝到堆区,然后自动释放。

那为什么ARC下,访问外部变量的block会自动从栈区拷贝到堆区呢?

block中的copy

在栈上的block,如果所在的作用域结束,block和block中的__block变量都会被废弃掉。

logo

所以,我们需要将Block复制到堆中,延长其生命周期,这样即使是block所在的作用域结束,block还是可以在堆中继续存在。

开启了ARC时,大多数情况下编译器会恰当的判断是否有需要将block从栈复制到堆,如果有,自动生成将block从栈上复制到堆上的代码,block复制执行的是copy实例方法,只要调用了copy方法,栈块就会变成堆块,一般在如下情况时,block会自动copy到堆上。

  • 1.调用Block的copy方法。
  • 2.将Block作为函数返回值时(MRC下需要手动调用copy,否则无效)。
  • 3.将Block赋值给__strong修改变量时(MRC时无效)。
  • 4.向Cocoa框架中含有usingBlock的方法或者GCD的API传递Block参数时。
logo
1
2
3
4
5
6
7
8
9
int count = 0;
blk_t blk = ^(){
NSLog(@"In Stack:%d", count);
};
NSLog(@"blk's Class:%@", [blk class]);//打印:blk's Class:__NSMallocBlock__
NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打印:Stack Block:__NSStackBlock__

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

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

不管block配置在何处,用copy方法复制都不会引起任何问题,在不确定是调用copy即可。

1
2
blk = [[[[blk copy] copy] copy] copy];
// 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。

__block变量的存储域

之前只说到了block,那block变量又会有什么影响呢?使用block变量的block从栈复制到堆上时,__block变量也会受到影响。

__block变量的配置存储域 block从栈复制到堆时的影响
从栈复制到堆并被block持有
被block持有

那么栈上的block变量复制到堆上之后,block是可以同时访问栈上的block变量和堆上的block变量,但是具体访问时到底是访问栈上的还是堆上的呢?这时候还记得我们之前说的**forwarding**变量么?

logo

通过forwarding, 无论是在block中还是 block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。

Block的实践

说了这么多,我们来看看block在实际开发中比较常见的使用方法吧。

一般情况下,block会用来作为方法回调的功能,和代理的方法比较相似,处理一些比较耗时的操作比如网络数据的下载,在下载好之后直接调用block回调,返回正确或错误的信息。

block会使得代码结构紧凑,逻辑清晰,接下来我们就看一个简单的🌰:

首先我们先声明一个typedef block

1
typedef void(^MPBlockDownloadHandler)(NSData * receiveData, NSError * error);

然后在下载函数中传入block作为参数,并在下载结束后调用block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (MPBlockDownloadHandler)handler
{
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];
NSURLSession * session = [NSURLSession sharedSession];
//执行请求任务
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(data,error);
});
}
}];
[task resume];
}

最后调用这个函数:

1
2
3
4
5
6
7
8
9
[self downloadWithURL:@"https://www.vactualpapers.com/web/wallpapers/sights-and-scenes-of-beautiful-singapore-hd-wallpaper-29/thumbnail/lg.jpg" parameters:nil handler:^(NSData *receiveData, NSError *error) {
if (error) {
NSLog(@"下载失败:%@",error);
}else {
NSLog(@"下载成功,%@",receiveData);
}
}];

这样,一个简单的利用block实现网络加载回调的功能就做好了。

最后

好了,这就是block的全部内容了。说是写消息传递,好像越来越跑偏了。。。
另外以上内容仅供个人学习使用,大部分内容来自《Objective-C高级编程 iOS与OS X多线程和内存管理》。如果有什么地方不对,还请大佬们多多指教。

参考文档

Objective-C 高级编程

iOS开发-由浅至深学习block

《Objective-C 高级编程》干货三部曲(二):Blocks篇

谈Objective-C block的实现

译 Block 小测验

iOS Block用法和实现原理

iOS Block 详解