block知多少
block对象就是一个结构体,里面有isa指针指向自己的类(global malloc stack),有desc结构体描述block的信息,__forwarding指向自己或堆上自己的地址,如果block对象截获变量,这些变量也会出现在block结构体中。最重要的block结构体有一个函数指针,指向block代码块。block结构体的构造函数的参数,包括函数指针,描述block的结构体,自动截获的变量(全局变量不用截获),引用到的__block变量。(__block对象也会转变成结构体)
block代码块在编译的时候会生成一个函数,函数第一个参数是前面说到的block对象结构体指针。执行block,相当于执行block里面__forwarding里面的函数指针。
一点一点解释:
定义
block作为C语言的扩展,并不是高新技术,和其他语言的闭包是一回事。iOS4.0系统已开始支持block,在编程过程中,block被Obj-C看成是对象,它封装了一段代码,这段代码可以在任何时候执行。block可以作为函数参数或者函数的返回值,而其本身又可以带输入参数或返回值。
Block的使用很像函数指针,(1)可以保存代码(2)有返回值(3)有形参(4)调用方式一样。不过与函数最大的不同是:block可以访问函数以外、词法作用域以内的外部变量的值。换句话说,block不仅实现函数的功能,还能携带函数的执行环境。
// 声明和实现写在一起,就像变量的声明实现 int a = 10;
int (^aBlock)(int, int) = ^(int num1, int num2) {
return num1 * num2;
};
// 声明和实现分开,就像变量先声明后实现 int a; a = 10;
int (^cBlock)(int,int);
cBlock = ^(int num1,int num2){
return num1 * num2;
};
//之后我们就可以像使用函数那样使用block了
block重命名
我们可以使用typedef为block重命名
typedef int (^Sum) (int, int);
这样我们就利用typedef定义了一个block,这个block的名字就是Sum,需要传入两个int类型参数:
Sum mySum = ^(int a, int b) {
n = 2;
return (a + b)*n;
};
这样就完整的定义好了一个block了,接下来的使用如下:
#import <Foundation/Foundation.h>
typedef int (^Sum) (int, int);
int main(int argc, const char * argv[]) {
__block int n = 1;
@autoreleasepool {
Sum mySum = ^(int a, int b) {
n = 10;
return (a + b)*n;
};
NSLog(@"(1 + 2) * %i = %d", n, mySum(1, 2));
}
return 0;
}
block类型
内存区域划分:
栈(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
堆(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式类似于链表
全局区(静态区static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由系统释放
常量区:常量字符串放在这里, 程序结束后由系统释放
程序代码区:存放函数体的二进制代码
根据isa指针,block一共有3种类型的block:
- NSConcreteGlobalBlock 全局静态,类似函数,位于text段(程序代码段),不会访问任何外部变量,不会涉及到任何拷贝,比如一个空的block
- NSConcreteStackBlock 保存在栈中,出函数作用域就销毁
- NSConcreteMallocBlock 保存在堆中,retainCount == 0销毁,该类型的block都是由_NSConcreteStackBlock类型的block从栈中复制到堆中形成的
而ARC和MRC中,还略有不同:
- 不管在ARC还是MRC环境下,block内部如果没有访问外部变量,这个block是全局block
NSGlobalBlock
,形式类似函数,存储在内存中的代码区。 - 在MRC下,block内部如果访问外部变量,这个block是栈block
NSStackBlock
,存储在内存中的栈上。block内部访问外部变量,同时对该block做一次copy操作,这个block是堆blockNSMallocBlock
,存储在内存中的堆上。 - 在ARC下,block内部如果访问外部变量,这个block是堆block
NSMallocBlock
,存储在内存中的堆上,因为在ARC下,默认对block做了一次copy操作。
Block对不同类型的变量的存取
基本类型
-
局部自动变量,在Block中只读。Block定义时copy变量的值,在Block中作为常量使用,所以即使变量的值在Block外改变,也不影响他在Block中的值。
int base = 100; BlkSum sum = ^ long (int a, int b) { // base++; 编译错误,只读 return base + a + b; }; base = 0; printf("%ld\n",sum(1,2)); // 这里输出是103,而不是3
-
static变量、全局变量。如果把上个例子的base改成全局的、或static。Block就可以对他进行读写了。因为全局变量或静态变量在内存中的地址是固定的,Block在读取该变量值的时候是直接从其所在内存读出,获取到的是最新值,而不是在定义时copy的常量。
static int base = 100; BlkSum sum = ^ long (int a, int b) { base++; return base + a + b; }; base = 0; printf("%d\n", base); printf("%ld\n",sum(1,2)); // 这里输出是3,而不是103 printf("%d\n", base);
输出结果是
0 4 1
,表明Block外部对base的更新会影响Block中的base的取值,同样Block对base的更新也会影响Block外部的base值。 -
Block变量,被
__block
修饰的变量称作Block变量。基本类型的Block变量等效于全局变量、或静态变量。Block被另一个Block使用时,另一个Block被copy到堆上时,被使用的Block也会被copy。但作为参数的Block是不会发生copy的。
用法
作为方法的参数
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self blockAsPara:^{
NSLog("block作为参数");
}];
}
- (void)blockAsPara:(void(^)())block{
block(); //执行block内部的代码
}
作为属性
@property (nonatomic,copy) void (^myBlock)();//block作为属性
- (void)viewDidLoad{
[super viewDidLoad];
[self setMyBlock:^{
NSLog("block作为属性");
}];
NSLog("%@",self.myBlock);
self.myBlock();//myBlock作为属性时的调用
}
链式编程思想
-(CaculatorViewController *(^)(int))add{
return ^(int value){
_result += value;
return self;
};
}
本质
Block的本质是可以截取自动变量的匿名函数 匿名函数:顾名思义就是不带名字的函数,在C语言中不允许这样的方法存在,至少要知道函数的名字来对函数进行引用,而在OC中的Block则可以用指针来直接调用一个函数,但虽说如此我们还是需要知道指针的名称。 截取自动变量:
int b = 0;
void (^blo)() = ^{
NSLog(@"Input:b=%d",b);
};
b = 3;
blo();
/* Output:b=0 */
虽然我们在调用blo之前改变了b的值,但是输出的还是Block编译时候b的值,所以截获瞬间自动变量就是:在Block中会保存变量的值,而不会随变量的值的改变而改变。
block注意事项
block访问外部变量
-
block内部可以访问外部的变量,block默认是将其复制到其数据结构中来实现访问的
-
默认情况下,block内部不能修改外面的局部变量,因为通过block进行闭包的变量是const的
-
给局部变量加上__block关键字,这个局部变量就可以在block内部修改,block是复制其引用地址来实现访问的
block作为属性应该用copy修饰
- 如果用weak、assign修饰block属性时,block访问外部变量,此时block的类型是栈block。保存在栈中的block,当block所在函数\方法返回\结束,该block就会被销毁。在其他方法内部调用访问该block,就会引发野指针错误EXC_BAD_ACCESS
- 当用copy、strong修饰block属性时,block访问外部变量,此时block的类型是堆block。保存在堆中的block,当引用计数器为0时被销毁,该类型block是由栈block从栈中复制到堆上,因此可以在其他方法内部调用该block。在ARC下,strong和copy都可以用来修饰block,但是建议修饰block属性使用copy
block指定为copy后是否会拷贝一份呢?(或者说是浅拷贝还是深拷贝)
- copy可变变量:在赋值指针的同时也会复制指针指向的内存区域。深拷贝,例如NSMutableString对象
- copy不可变变量:等同于strong,还是浅拷贝,例如NSString对象
因为block是一段代码,即不可变的,所以并不会深拷贝。
循环引用
对于非ARC下, 为了防止循环引用, 我们使用__block来修饰在Block中使用的对象。
对于ARC下, 为了防止循环引用, 我们使用__weak来修饰在Block中使用的对象。原理就是:ARC中,Block中如果引用了__strong修饰符的自动变量,则相当于Block对该变量的引用计数+1。
@interface BlockViewController ()
@property (nonatomic, strong) void (^block)(void);
@property (nonatomic, copy) NSString *str;
@end
@implementation BlockViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.block = ^{
self.str = @"123";
};
}
@end
上面的代码,self.block强引用block,而block中又使用了self.str,所以block强引用self,造成强引用,解决方法:weakSelf和strongSelf
__weak __typeof(self)weakSelf = self;
[self.context performBlock:^{
__strong __typeof(weakSelf)strongSelf = weakSelf;
[strongSelf doSomething];
[strongSelf doMoreThing];
}];
那block中的StrongSelf又是做什么的呢?还是上面的例子,当你加了weakSelf后,block中的self随时都会有被释放的可能,所以会出现一种情况,在调用doSomething的时候self还存在,在doMoreThing的时候self就变成nil了,所以为了避免这种情况发生,我们会重新strongify self。
block的实现
block的数据结构定义
从上图可以看出,Block_layout就是对block结构体的定义:
isa指针:指向表明该block类型的类。
flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
reserved:保留变量,我的理解是表示block内部的变量数。
invoke:函数指针,指向具体的block实现的函数调用地址。
descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。
variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
block编译过后
通过clang编译器,可以看到block和其他Objective-c对象一样,都是被编译为C语言里的普通的struct结构体来实现的。我们来看一个最简单的block会被编译成什么样:
//这个是源代码
int main(){
void (^blk)(void) = ^{
printf("Block\n");
};
block();
return 0;
}
编译后的代码如下:
struct __block_impl { //结构体,block的基类
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 { //block变量
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;
}
};
struct void __main_block_func_0(struct __main_block_impl_0 *__cself){ //匿名函数
printf("Block\n");
}
static struct __main_block_desc_0{ //block的描述
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
代码非常长,但是并不复杂,一共是四个结构体,显然一个block对象被编译为了一个__main_block_impl_0
类型的结构体。这个结构体由两个成员结构体和一个构造函数组成。两个结构体分别是__block_impl
和__main_block_desc_0
类型的。其中__block_impl
结构体中有一个函数指针,指针将指向__main_block_func_0
类型的结构体。总结了一副关系图:
block在定义的时候:
//调用__main_block_impl_0结构体的构造函数
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
block在调用的时候:
(*blk->impl.FuncPtr)(blk);
block有自己的数据和算法。显然算法(代码)是放在__main_block_func_0
结构体里的。那么数据在哪里呢,这个问题比较复杂,我们来看一看下面的代码会编译成什么样,为了简化代码,这里只贴出需要修改的部分。
typedef void (^Block)(void);
Block block;
{
int val = 0;
block = ^(){
NSLog(@"val = %d",val);
};
}
block();
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _val, int flags=0) : val(_val){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
int val = __cself->val;
printf("val = %d",val);
}
可以看到,当block需要截获自动变量的时候,首先会在__main_block_mpl_0
结构体中增加一个成员变量并且在结构体的构造函数中对变量赋值。以上这些对应着block对象的定义。
在block被执行的时候,把__main_block_impl_0
结构体,也就是block对象作为参数传入__main_block_func_0
结构体中,取出其中的val的值,进行接下来的操作。
为什么__block中不能修改变量值?
通过把block拆成这四个结构体,系统“完美”的实现了一个block,使得它可以截获自动变量,也可以像一个微型程序一样,在任意时刻都可以被调用。但是,block还存在这一个致命的不足:
注意到之前的__main_block_func_0
结构体,里面有printf方法,用到了变量val,但是这个block,和最初block截获的block,除了数值一样,再也没有一样的地方了。参见这句代码:
int val = __cself->val;
当然这并没有什么影响,甚至还有好处,因为int val
变量定义在栈上,在block调用时其实已经被销毁,但是我们还可以正常访问这个变量。但是试想一下,如果我希望在block中修改变量的值,那么受到影响的是int val
而非__cself->val
,事实上即使是 __cself->val
,也只是截获的自动变量的副本,要想修改在block定义之外的自动变量,是不可能的事情。
既然无法实现修改截获的自动变量,那么编译器干脆就禁止程序员这么做了。
__block修饰符是如何做到修改变量值的?
如果把val变量加上__block修饰符,编译器会怎么做呢?
//int val = 0; 原代码
__block int val = 0;//修改后的代码
编译后的代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_val_0 *val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,__Block_byref_val_0 *_val, int flags=0) : val(_val->__forwrding){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
__Block_byref_val_0 *val = __cself->val;
printf("val = %d",val->__forwarding->val);
}
改动并不大,简单来说,只是把val封装在了一个结构体中而已。可以用下面这个图来表示五个结构体之间的关系:
但是关键在于__main_block_impl_0
结构体中的这一行:
__Block_byref_val_0 *val;
由于__main_block_impl_0
结构体中现在保存了一个指针变量,所以任何对这个指针的操作,是可以影响到原来的变量的。
进一步,我们考虑截获的自动变量是Objective-C的对象的情况。在开启ARC的情况下,将会强引用这个对象一次。这也保证了原对象不被销毁,但与此同时,也会导致循环引用问题。
需要注意的是,在未开启ARC的情况下,如果变量附有__block修饰符,将不会被retain,因此反而可以避免循环引用的问题。