说过了target-action
说过了KVO
说过了NotificationCenter
这次我们来说一个一对一的消息传递方式,Block。
Block是什么
Block,很多语言中翻译做闭包,用《Objective-C高级编程》中的话说:
Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的额匿名函数。
所以,Block就是一个带有自动变量的匿名函数。
匿名函数
顾名思义,匿名函数就是没有名称的函数,C语言是不允许出现没有函数名的函数的,但是因为实际上调用函数也是调用指向函数的函数指针,但是没有函数名,就没办法获取到函数的指针。那么block具体是怎么来实现的呢?我们先往下看。
自动变量
在栈上声明一个变量如果不是静态变量或全局变量,是不可以在这个栈内声明的匿名函数中使用的,但是在block中却可以。
Block结构
在Xcode里,我们敲入快捷键inlineBlock
就会看到有这样一个block的样式提供给我们。
//增加了returnType,可省略
returnType(^blockName)(parameterTypes) = ^returnType(parameters) {
statements
};
其中第二个returnType
是我加上去的,为了能看的明显一点。
但是一下子看到一个这样的东西还是有点乱,我们把它拆分成声明部分和实现部分来看就会清楚很多了。
声明Block
returnType(^blockName)(parameterTypes)
声明block中包括了返回类型、^、block名称、参数列表。
实现Block
^returnType(parameters) {
statements
};
实现block中包括了返回类型(可省略)、参数列表(可省略)、实现代码。
其中返回值类型可以省略。
^(parameters) {
statements
};
参数列表也可以省略。
^{statements};
Block的使用
因为block的传入参数和返回值都可以为空,所以Block的使用可以分为4中模式:
1.无参数、无返回值。
2.有参数、无返回值。
3.无参数、有返回值。
4.有参数、有返回值。
接下来我们就来举例子看看这几种方式的使用。
无参数、无返回值
//无参数、无返回值
- (void)blockWithoutParameterAndWithoutReturn
{
void(^noParameterNoReturn)(void) = ^(void){
NSLog(@"无参数、无返回值");
};
noParameterNoReturn();
}
有参数、无返回值
//有参数、无返回值
- (void)blockWithParameterAndWithoutReturn
{
void(^parameterNoReturn)(NSInteger number) = ^(NSInteger number){
NSLog(@"有参数、无返回值,参数是%lu",number);
};
parameterNoReturn(10);
}
无参数、有返回值
//无参数、有返回值
- (void)blockWithoutParameterAndWithReturn
{
NSInteger(^noParameterReturn)(void) = ^{
NSInteger number = 20;
NSLog(@"无参数、有返回值,返回值是%lu",number);
return number;
};
NSInteger number = noParameterReturn();
NSLog(@"返回值是%lu",number);
}
有参数、有返回值
//有参数、有返回值
- (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参数:
//number作为参数,无返回值
typedef void(^typedefBlock)(NSInteger number);
然后声明一个函数中带有此变量
//typedef block
- (void)testTypedefBlockWith:(typedefBlock)testTypedefBlock
{
NSLog(@"开始使用typedef block");
testTypedefBlock(12);
NSLog(@"结束使用typedef block");
}
这时候调用此方法,在回调的方法中就可以获取到传递过来的值。
[self testTypedefBlockWith:^(NSInteger number) {
NSLog(@"回调 typedef block number %lu",number);
}];
Block与外界变量
默认情况
通常情况下,对于block外的变量引用,block默认是将其复制到block的数据结构中实现访问的,也就是说只有block中用到的变量,block才会把他自动截获进来,而且因为截取的是瞬时值,所以之后在外部改变变量的值也不会改变值得大小。因为截获自动变量会存储在block内部,所以会导致block体积变大。
另外需要注意的一点就是block内部只能调用getter方法,不可以调用setter方法,所以是没办法修改外部变量的值的。
比如:
- (void)autoParamterTest
{
NSInteger number = 100;
void(^autoParamter)(void) = ^(void){
NSLog(@"%lu",number); //输出100
};
number = 200;
autoParamter();
}
这段代码最后会输出100,因为在定义block时,他已经把number的值复制到block中了,所以再改变他,对block中的值也不会有影响。
另外,在block中对number赋值时,编译器会直接报错。
[图片上传中...(block-setter.png-8afa38-1521083690526-0)]
__block
对于这种情况,OC提供了_block(两个下划线)来修饰外部变量,使用了__block修饰的外部变量,block内部是复制其引用地址来实现访问数据的,所以block内部可以修改block外部的变量值。
- (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的方法中使用了这个类自己的东西,这时候两者互相持有就会发生循环引用,引起内存泄漏的问题。比如如下代码:
- (void)blockCircularReference
{
self.circleBlock = ^(NSInteger number) {
[self autoParamterTest];
};
}
但是苹果也给出了相应的解决方案来处理block下的循环引用。
__weak修饰
可以直接用__weak(有两个下划线)来修饰,来打破block中的循环,使用__weak修饰解决循环引用一共有三种实现的方式。
- 使用__weak ClassName
- (void)blockCircularReference
{
__weak MPBlockViewController *weakSelf = self;
self.circleBlock = ^(NSInteger number) {
[weakSelf autoParamterTest];
};
}
- 使用__weak typeof(self)
- (void)blockCircularReference
{
__weak typeof (self) weakSelf = self;
self.circleBlock = ^(NSInteger number) {
[weakSelf autoParamterTest];
};
}
- 使用Reactive Cocoa中的@weakify和@strongify
- (void)blockCircularReference
{
@weakify(self);
self.circleBlock = ^(NSInteger number) {
@strongify(self);
[self autoParamterTest];
};
}
@weakify, @strongify的具体使用可以看这里
__block
在MRC下,可以直接使用__block进行修饰。
也可以先用__block修饰,然后在block方法中使用完将其设为nil,但是要注意就是block必须要被调用一次。
- (void)blockCircularReference
{
__block MPBlockViewController *blockSelf = self;
self.circleBlock = ^(NSInteger number) {
[blockSelf autoParamterTest];
blockSelf = nil; //必须设为nil
};
self.circleBlock(10); //必须至少调用一次
}
将self作为参数传递
也可以直接将self作为一个参数传递到block中。
- (void)blockCircularReference
{
self.circleBlock = ^(MPBlockViewController *vc) {
[vc autoParamterTest];
};
}
Block的实现
block实际上是用C语言源码来处理的,含有block的源码首先被转换成C语言编译器能够处理的源码,再作为C进行编译。
Clang
使用LLVM编译器的clang
可以将OC的代码翻译成C++的源代码,说是C++的代码,但是实际上也就是C语言的源代码。
使用的方式就是打开Terminal,cd到源代码文件目录,输入:
clang -rewrite-objc 源代码文件名
比如这样一段代码(这里没有引用其他OC的框架,因为引入之后clang出来的cpp文件会巨大,有好几千行):
#include
int main() {
void (^ blk)(void) = ^{printf("Block\n");};
blk();
return 0;
}
这段简单的block代码clang之后就会变成如下源码(这里删除了部分代码,只显示了重要的部分):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
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中的成员变量,他是一个结构体,其结构如下:
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中的成员变量,也是一个结构体,其结构如下:
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的初始化,用到的就是之前介绍的两个结构体。
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中要实现的代码。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
实现main函数
另外main函数的源码在这里。
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
:
#include
int main() {
int number = 10;
void (^ blk)(void) = ^{printf("%d",number);};
blk();
return 0;
}
这时候clang之后发现:
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看看:
#include
int main() {
__block int number = 10;
void (^ blk)(void) = ^{printf("%d",number);};
blk();
return 0;
}
clang之后发现:
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
,结果代码一下子增加了巨多!
这时候仔细看代码,就能发现多了一个段代码:
__Block_byref_number_0 number = {
(void*)0,(__Block_byref_number_0 *)&number,
0,
sizeof(__Block_byref_number_0),
10};
找到这个结构体的声明:
struct __Block_byref_number_0 {
void *__isa;
__Block_byref_number_0 *__forwarding;
int __flags;
int __size;
int number;
};
那如果这时候我们给number
赋一个新的值会怎么样呢?
#include
int main() {
__block int number = 10;
void (^ blk)(void) = ^{
number = 20;
printf("%d",number);
};
blk();
return 0;
}
clang后发现多了这里变化(就不贴全部代码了):
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的初始化时,我们看到了有一行代码是:
impl.isa = &_NSConcreteStackBlock;
从名字应该可以判断出来,这行代码的意思是这个block是存储在栈上的。
那么除了栈,block还存储在哪些地方呢?
| 类 | 设置对象的存储域 |
| ------| ------ |
| _NSConcreteStackBlock | 栈块 |
| _NSConcreteGlobalBlock | 全局块 |
| _NSConcreteMallocBlock | 堆块 |
从名字就能看出来:
栈块存在栈内存中,超出作用域后就会马上销毁。
全局块在全局内存中,和全局变量一样。
堆块存在堆内存中,是一个带有引用计数的对象,需要自行管理内存。
那么我们怎么样能够知道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变量都会被废弃掉。
所以,我们需要将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参数时。
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即可。
blk = [[[[blk copy] copy] copy] copy];
// 经过多次复制,变量blk仍然持有Block的强引用,该Block不会被废弃。
__block变量的存储域
之前只说到了block,那__block变量又会有什么影响呢?使用__block变量的block从栈复制到堆上时,__block变量也会受到影响。
| __block变量的配置存储域 | block从栈复制到堆时的影响 |
| ------| ------ |
| 栈 | 从栈复制到堆并被block持有 |
| 堆 | 被block持有 |
那么栈上的__block变量复制到堆上之后,block是可以同时访问栈上的__block变量和堆上的__block变量,但是具体访问时到底是访问栈上的还是堆上的呢?这时候还记得我们之前说的****__forwarding****变量么?
通过__forwarding, 无论是在block中还是 block外访问__block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量。
Block的实践
说了这么多,我们来看看block在实际开发中比较常见的使用方法吧。
一般情况下,block会用来作为方法回调的功能,和代理的方法比较相似,处理一些比较耗时的操作比如网络数据的下载,在下载好之后直接调用block回调,返回正确或错误的信息。
block会使得代码结构紧凑,逻辑清晰,接下来我们就看一个简单的:
首先我们先声明一个typedef block
typedef void(^MPBlockDownloadHandler)(NSData * receiveData, NSError * error);
然后在下载函数中传入block作为参数,并在下载结束后调用block。
- (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];
}
最后调用这个函数:
[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 详解