浅谈Block的应用及原理
1.使用:
NSInteger (^mySum)(NSInteger,NSInteger) = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB;
};
以上定义了一个Block变量,block本来就是一个程序段,因此有返回值有输入的参数,这里这个block返回的类型是BOOL ,“^”表示block定义开始,block名称mySum紧跟在^符号之后,这里这个block接受一个NSInteger类型的参数。
调用这个block的方法:
mySum(1,3);
NSLog(@"sum is %ld",mySum(1,3)); // sum is 4
上述block内部使用了接受的两个NSInteger类型参数,除此之外,block外的变量也可以在block内部使用,比如:
NSInteger paramC = 5;
// Block变量定义
NSInteger (^sum)(NSInteger,NSInteger) = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB + paramC;
};
NSLog(@"sum is %ld",sum(1,3)); // 输出:sum is 9
需要注意的一点是,不能在block内部改变本地变量,编译器会直接报错。而更需要注意的是paramC这样的局部变量的变化是不会体现在block里的,比如接着上面的代码,继续写:
// Block变量定义
NSInteger (^sum)(NSInteger,NSInteger) = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB + paramC;
};
paramC = 10;
NSLog(@"sum is %ld",sum(1,3));// 输出:sum is 9
输出依旧是:“sum is 9”,paramC赋值为10,但是在block内部的paramC的值依旧是5,这是因为block内部的paramC在block声明是copy了一份到block内,block外部的paramC的变化与block无关,可以理解为paramC为值类型(值类型,只引用了数据,没有对变量进行地址引用)。如果paramC为引用类型的话,外部的变化会影响block内部(引用类型,直接对地址进行引用)。
如果需要传递个block变量值,可以将变量声明为___block,可以使用实例变量。
例:将变量声明为___block
// Block变量定义
__block NSInteger paramC = 5;
NSInteger (^sum)(NSInteger,NSInteger) = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB + paramC;
};
paramC = 10;
NSLog(@"sum is %ld",sum(1,3));// 输出:sum is 14
例:使用实例变量,使用weakSelf避免循环引用问题。
self.paramD = 10;
__weak typeof(self) weakSelf = self;
// Block变量定义
NSInteger (^sum)(NSInteger,NSInteger) = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB + weakSelf.paramD;
};
self.paramD = 20;
NSLog(@"sum is %ld",sum(1,3));// 输出:sum is 24
刚开始使用block感觉语法奇怪,而且书写起来麻烦,我们还可以通过typedef将block进行简单的封装。将上述block进行封装如下:
typedef NSInteger (^MySum)(NSInteger, NSInteger);
使用:声明一个类型MySum类型的block实例,定义内容后直接使用block。
MySum mySum;
mySum = ^(NSInteger paramA, NSInteger paramB){
return paramA + paramB + weakSelf.paramD;
};
self.paramD = 20;
二、应用
Apple所推荐的block使用范围包括以下几个方面:
- 枚举——通过block获取枚举对象或控制枚举进程
- View动画——简单明了的方式规定动画
- 排序——在block内写排序算法
- 通知——当某事件发生后执行block内的代码
- 错误处理——当错误发生时执行block代码
- 完成处理——当方法执行完毕后执行block代码
- GCD多线程——多线程控制
三、实现
上文中提到了不能修改局部变量,但使用_block修饰可以修改局部变量,之前很疑惑这是为什么?接下来我们便研究一下block的实现。
研究工具:clang
为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。该命令是
clang -rewrite-objc block1.c
使用上述命令编译后,我们得到一份block1.cpp文件,在这份文件中我们可以看到以下代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
block 的数据结构定义如下,通过该图,我们可以知道,一个 block 实例实际上由 4 部分构成:
- isa 指针,所有对象都有该指针,用于实现对象相关的功能。
- flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
- reserved,保留变量。
- FuncPtr,block对应的函数指针。
接下来,我们看一下,只输出以一句话的block的什么样的
int main()
{
void (^myBlock)(void) = ^{
printf("hello, block");
};
return 0;
}
生成的中间代码有500多行,我们抽出主要的代码如下图:
首先出现的结构体就是_main_block_impl0,可以看出是根据所在函数(main函数)以及出现序列(第0个)进行命名的。如果是全局block,就根据变量名和出现序列进行命名。
**main_block_impl_0中包含了两个成员变量和一个构造函数,成员变量分别是**block_impl结构体和描述信息Desc。
接着出现的是_main_block_func0函数,即block对应的函数体。该函数接受一个__cself参数,即对应的block自身。
接这出现的是_main_block_desc0结构体,其中比较有价值的信息是block大小。
最后就是main函数中对block的创建和调用,可以看出执行block就是调用一个以block自身作为参数的函数,这个函数对应着block的执行体。
赋值代码中,类型转换太多了,我们去掉类型转换后如下图:
//去掉类型转换后的代码
void (*myBlock)(void) = &__main_block_impl_0(参数1, 参数2);
Block语法赋值给myBlock,相当于把*_main_block_impl*0的函数地址指针赋值给myBlock。
参数一 为 main_block_func*0,也就是匿名函数的函数名称,通过main_block_impl*0的定义可以知道main_block_func_0赋值给了FuncPtr。
参数二 为 main_block_desc*0*DATA,是 main_block_impl_0 的结构体的大小。
*_main_block_impl*0这里可以看到,block的类型用_NSConcreteStackBlock来表示,表明这个block位于栈中。同样地,block类型还有_NSConcreteMallocBlock和_NSConcreteGlobalBlock。
由block_impl中isa,得知block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架对其copy了一份。如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block栈已经不存在了,使用copy属性可以将block放到堆中。
修改局部变量
再看一个访问局部变量的block是怎样的。
在main函数中添加局部变量,并且打印这个局部变量
![Pasted Graphic](/Users/sunmingyue/Desktop/周总结/浅谈block的应用与实现/Pasted Graphic.tiff)
重新编译得到的中间文件源码如下:
可以看出block结构体*_main_block_impl*0多了个成员变量i,用来存储使用到的局部变量i,
如果我们尝试修改局部变量i,则会得到如下错误:
错误信息很详细,既告诉我们变量不可赋值,也提醒我们要使用__block类型标识符。
为什么不能给变量i赋值呢?
因为main函数中的局部变量i和函数main_block_func*0不在同一个作用域中,调用过程中只是进行了值传递。当然,在上面代码中,我们可以通过指针来实现局部变量的修改。不过这是由于在调用main_block_func*0时,block所在的栈还没有销毁,变量i还在栈中。但是在很多情况下,block被执行时,定义时所在的函数栈已经被销毁,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就奔溃了。
所以对于局部变量,不允许block进行修改是合理的。
__block的实现
__block类型变量是如何支持修改的呢?
在main函数中添加局部变量,修改局部变量的值,并且打印这个局部变量:
![Pasted Graphic 1](/Users/sunmingyue/Desktop/周总结/浅谈block的应用与实现/Pasted Graphic 1.tiff)
我们通过__block修改局部变量,此时再看中间代码:
此时再看中间代码,会多出很多信息。首先是__block变量对应的结构体:
![Pasted Graphic 3](/Users/sunmingyue/Desktop/周总结/浅谈block的应用与实现/Pasted Graphic 3.tiff)
由第一个成员__isa指针也可以知道_Block_byref_i0也可以是NSObject。
第二个成员__forwarding指向自己,为什么要指向自己?指向自己是没有意义的,只能说有时候需要指向另一个*_Block_byref_i*0结构。
最后一个成员是目标存储变量i。
在下图中可以看到,main_block_impl*0的成员变量i变成了Block_byref_i*0 类型。
![Pasted Graphic 2](/Users/sunmingyue/Desktop/周总结/浅谈block的应用与实现/Pasted Graphic 2.tiff)
对应的函数main_block_func*0中,亮点是Block_byref_i*0指针类型变量i,通过其成员变量__forwarding指针来操作另一个成员变量。 :-)
接下来再看main函数
通过这样看起来有点复杂的改变,我们可以修改变量i的值。但是问题同样存在:*_Block_byref_i*0类型变量i仍然处于栈上,当block被回调执行时,变量i所在的栈已经被销毁怎么办?
在这种关键时刻,*_main_block_desc*0站出来了:
此时,main_block_desc*0多了两个成员函数:copy和dispose,分别指向main_block_copy*0和*_main_block_dispose*0。
当block从栈上被copy到堆上时,会调用**main_block_copy_0将**block类型的成员变量i从栈上复制到堆上;而当block被释放时,相应地会调用**main_block_dispose_0来释放**block类型的成员变量i。
一会在栈上,一会在堆上,那如果栈上和堆上同时对该变量进行操作,怎么办?
这时候,**forwarding的作用就体现出来了:当一个**block变量从栈上被复制到堆上时,栈上的那个**Block_byref_i_0结构体中的**forwarding指针也会指向堆上的结构。
到此,我们大概了解了block实现,学习过程中参考了很多大神的博客,很感谢大神们的分享。