blcok是一种特殊的数据结构,它能够保存一段代码,等到须要的时候进行调用执行这段代码,经常使用于GCD、动画、排序及各种回调。html
Block变量的声明格式为: 返回值类型(^Block名字)(参数列表);ios
//声明一个没有传参和返回值的blcok void(^myBlock1)(void) ; //声明一个有两个传参没有返回值的blcok 形参变量名称能够省略,只留有变量类型便可 void(^myBlock2)(NSString *name,int age); //声明一个没有传参但有返回值的blcok NSString *(^myBlock3)(); //声明一个既有返回值也有参数的blcok int(^myBlock4)(NSString *name);
block的赋值: Block变量 = ^(参数列表){函数体};c++
//若是没有参数能够省略写(void) myBlock1 = ^{ NSLog(@"hello,word"); }; myBlock2 = ^(NSString *name,int age){ NSLog(@"%@的年龄是%d",name,age); }; //一般状况下都将返回值类型省略,由于编译器能够从存储代码块的变量中肯定返回值的类型 myBlock3 = ^{ return @"小李"; }; myBlock4 = ^(NSString *name){ NSLog(@"根据查找%@的年龄是10岁",name); return 10; };
固然也能够直接在声明的时候就赋值: 返回值类型(^Block名字)(参数列表) = ^(参数列表){函数体};面试
int(^myBlock5)(NSString *address,NSString *name) = ^(NSString *address,NSString *name){ NSLog(@"根据查找家住%@的%@今年18岁了",address,name); return 18; };
blcok的调用:Block名字();数据库
//没有返回值的话直接 Block名字();调用 myBlock1(); //有参数的话要传递相应的参数 myBlock2(@"校花",12); //有返回值的话要对返回值进行接收 NSString *name = myBlock3(); NSLog(@"%@",name); //既有参数又有返回值的话就须要即传参数又接收返回值 int age = myBlock5(@"河北村",@"大梨"); NSLog(@"%d",age);
在实际使用Block的过程当中,咱们可能须要重复地声明多个相同返回值相同参数列表的Block变量(blcok内部执行的代码功能不同),若是老是重复地编写一长串代码来声明变量会很是繁琐,数组
因此咱们可使用typedef来定义Block类型。网络
#import "ViewController.h" typedef void(^commentBlock)(NSString *name,int age); @interface ViewController () @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; commentBlock commentBlock1 = ^(NSString *name,int age){ //这里的操做是将age的name从数据库中筛选出来 }; commentBlock commentBlock2 = ^(NSString *name,int age){ //这里的操做是将age的name添加到数据库 }; commentBlock commentBlock3 = ^(NSString *name,int age){ //这里的操做是将age的name从数据库中删除 }; commentBlock1(@"li",12); commentBlock2(@"dong",19); commentBlock3(@"mi",8); } 这样能够减小重复代码,避免重复用void(^commentBlock)(NSString *name,int age);声明blcok
上面,只是讲到了blcok的一些基本使用,那么在咱们实际开发中,block是怎么应用的呢?其实在实际开发中把block做为方法的参数是一种比较常见的用法,好比咱们用到的网络请求工具.数据结构
好比,咱们举一个block做为参数的小例子:app
1 #import "ViewController.h" 2 typedef void(^BtnBlock)(void); 3 @interface ViewController () 4 @property(nonatomic,weak)BtnBlock currentBlcok; 5 @end 6 7 @implementation ViewController 8 9 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 10 [self alertWithBlcok:^{ 11 NSLog(@"用户点击了肯定 在这里能够执行对应的操做"); 12 }]; 13 // [self alertWithBlcok:nil]; 14 } 15 - (void)alertWithBlcok:(BtnBlock)block{ 16 _currentBlcok = block; 17 //低层最大的背景View 18 UIView *alertBgView = [[UIView alloc]initWithFrame:self.view.bounds]; 19 alertBgView.tag = 99; 20 alertBgView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.73]; 21 [self.view addSubview:alertBgView]; 22 23 //中间的View 24 UIView *alertCenterView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 240, 140)]; 25 alertCenterView.clipsToBounds = YES; 26 alertCenterView.layer.cornerRadius = 10; 27 alertCenterView.center = alertBgView.center; 28 alertCenterView.backgroundColor = [UIColor redColor]; 29 [alertBgView addSubview:alertCenterView]; 30 31 32 //取消按钮 33 UIButton *cancelBtn = [[UIButton alloc]initWithFrame:CGRectMake(0, 100, alertCenterView.frame.size.width/2, 40)]; 34 [cancelBtn setTitle:@"取消" forState:UIControlStateNormal]; 35 cancelBtn.titleLabel.font = [UIFont systemFontOfSize:15]; 36 [cancelBtn setTitleColor:[UIColor colorWithRed:51/255.0 green:51/255.0 blue:51/255.0 alpha:1.0] forState:UIControlStateNormal]; 37 [cancelBtn addTarget:self action:@selector(dismissAlertView) forControlEvents:UIControlEventTouchUpInside]; 38 [alertCenterView addSubview:cancelBtn]; 39 40 //短的分割线 41 UIView *shortView = [[UIView alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 110, 1, 20)]; 42 shortView.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.53]; 43 [alertCenterView addSubview:shortView]; 44 45 //取消按钮 46 UIButton *continueBtn = [[UIButton alloc]initWithFrame:CGRectMake(alertCenterView.frame.size.width/2, 100, alertCenterView.frame.size.width/2,40)]; 47 [continueBtn setTitle:@"肯定" forState:UIControlStateNormal]; 48 [continueBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; 49 continueBtn.titleLabel.font = [UIFont systemFontOfSize:15]; 50 [continueBtn addTarget:self action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside]; 51 [alertCenterView addSubview:continueBtn]; 52 } 53 - (void)dismissAlertView{ 54 [[self.view viewWithTag:99] removeFromSuperview]; 55 } 56 -(void)buttonAction{ 57 if (_currentBlcok) { 58 _currentBlcok(); 59 } 60 }
用户点击肯定按钮执行的操做能够经过block先封存起来,等用户点击肯定按钮时再调用,最终实现效果:iphone
固然blcok除了做为方法参数外,还能够当作属性和返回值。
接下来咱们来看一下block到底是一个什么样的结构?
经过clang命令将oc代码转换成c++代码(若是遇到_weak的报错是由于_weak是个运行时函数,因此咱们须要在clang命令中指定运行时系统版本才能编译):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main.cpp
-(void)viewDidLoad{ [super viewDidLoad]; int i = 1; void(^block)(void) = ^{ NSLog(@"%d",i); }; block(); }
转换成c++代码以下:
//block的真实结构体 struct __ViewController__viewDidLoad_block_impl_0 { struct __block_impl impl; struct __ViewController__viewDidLoad_block_desc_0* Desc; int i; //构造函数(至关于OC中的init方法 进行初始化操做) i(_i):将_i的值赋给i flags有默认值,可忽略 __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _i, int flags=0) : i(_i) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //封存block代码的函数 static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) { int i = __cself->i; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_3g_7t9fzjm91xxgdq_ysxxghy_80000gn_T_ViewController_c252e7_mi_0,i); } //计算block须要多大的内存 static struct __ViewController__viewDidLoad_block_desc_0 { size_t reserved; size_t Block_size; } __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)}; //viewDidLoad方法 static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) { ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad")); //定义的局部变量i int i = 1; //定义的blcok底部实现 void(*block)(void) = &__ViewController__viewDidLoad_block_impl_0( __ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, i)); //block的调用 bloc->FuncPtr(block); }
从中咱们能够看出,定义的block实际上就是一直指向结构体_ViewController_viewDidLoad_block_impl_0的指针(将一个_ViewController_viewDidLoad_block_impl_0结构体的地址赋值给了block变量)。而这个结构体中,咱们看到包含如下几个部分:
impl、Desc、引用的局部变量、构造方法。
而从构造方法咱们又能够看出impl中有如下几个成员:isa、Flags、FuncPtr,因此综合以上信息咱们能够知道block内部有如下几个成员:
接下来,咱们依依来看block底层结构中这些结构体j或者参数的做用是什么?
首先Desc:
static struct __ViewController__viewDidLoad_block_desc_0 { size_t reserved; size_t Block_size; } __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};
desc结构体中存储着两个参数,reserved和Block_size,而且reserved赋值为0而Block_size则存储着__ViewController__viewDidLoad_block_impl_0的占用空间大小。最终将desc结构体的地址传入__ViewController__viewDidLoad_block_impl_0中赋值给Desc。因此Desc的做用是记录Block结构体的内存大小。
接下来,咱们来看,int i:
i也就是咱们定义的局部变量,由于在block块中使用到i局部变量,因此在block声明的时候这里才会将i做为参数传入,也就说block会捕获i。若是没有在block中使用age,这里将只会传入impl,Desc两个参数。这里须要注意的一点是,调用block结构体的构造函数时,是将咱们定义的局部变量i的值传进去了的,也就是构造函数实现的时候i(_i) 这部分代码的做用就是将_i的值传给i。其实这也就解释清楚为何咱们在block中没法修改i的值了,由于block用到的i根本和咱们本身定义的i不是同一个,block内部是本身单首创建了一个参数i,而后将咱们定义的局部变量i的值赋给了本身建立的i。
最后咱们来看一下impl这个结构体:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
咱们在block结构体的构造函数中也能够看出这几个成员分别有什么做用:
isa:指针,存放结构体的内存地址
Flags:这个用不到 有默认值
FuncPtr:block代码块地址
因此经过以上分析,咱们能够得出如下几个结论:
一、block本质上也是一个OC对象,它内部也有个isa指针,这一点咱们也能够经过打印其父类是NSObject来证实;
二、block是封装了函数调用以及函数调用环境的OC对象.(所谓调用环境就是好比block用到了变量i就把它也封装进来了);
三、FuncPtr则存储着viewDidLoad_block_func_0函数的地址,也就是block代码块的地址。因此当调用block的时候,bloc->FuncPtr(block);是直接调用的FuncPtr方法。
四、impl结构体中isa指针存储着&_NSConcreteStackBlock地址,能够暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
五、Desc存储__viewDidLoad_block_impl_0结构体所占用的内存大小,也就是存储着Block的内存大小。
咱们 能够用一张图来表示各个结构体之间的关系:
再简单化就是:(网络图片 可是原理同样)
block底层的数据结构也能够经过一张图来展现(variables就是结构体中所引用的变量,invoke就是上面的FuncPtr也就是block封装代码的函数地址,这个图是根据上面的分析能够总结出):
1.isa指针,全部对象都有该指针,用于实现对象相关的功能。 2.flags,用于按bit位表示一些block的附加信息,block copy的实现代码能够看到对该变量的使用。 3.reserved,保留变量。 4.invoke,函数指针,指向具体的Block实现的函数调用地址。就是FuncPtr 5.descriptor,表示该Block的附加描述信息,主要是size大小,以及copy和dispose函数的指针。 6.variables,截取过来的变量,Block可以访问它外部的局部变量,就是由于将这些变量(或变量的地址)复制到告终构体中。
咱们定义了几个变量:全局变量name、局部变量-auto变量:i,obj、局部变量-静态变量:height,分别在blcok内部修改和访问↓↓
咱们发如今block内部均可以访问这些变量,可是没法修改局部变量中的auto变量,没法修改的缘由咱们在上面的分析中也能够看出,是由于block内部本身建立了对应的变量,外部auto变量只是将值传递到block内赋给block建立的内部变量。block内部存在的只是本身建立的变量并不存在block外部的auto变量,因此没办法修改。
可是为何全局变量和静态变量就能够访问呢?咱们将oc代码转换成c++代码来看
发现,block内部虽然访问了四个变量,可是其底层只捕获了三个变量,并没有捕获全局变量name。
并且比较局部变量中的auto变量和静态变量,发现blcok底层捕获auto变量时是捕获的其值,而捕获静态变量时是捕获的变量地址(i是值, *height是地址),这也就是为何咱们能够在block修改静态变量,由于blcok内修改的静态变量其实和blcok外的静态变量是同一个内存地址,同一个东西。
关于auto变量obj,blcok内部也是捕获它的值,不要由于它有*就以为捕获的是地址,由于obj自己就是个对象,自己就是地址,若是block捕获的是obj的地址的话应该是NSObject **obj 即指向指针的地址。
因此咱们经过上面这个例子能够总结出:
为何会出现这种区别呢?
首先,为何捕获局部变量而不捕获全局变量这个问题很好理解:
全局变量整个项目均可以访问,block调用的时候能够直接拿到访问,不用担忧变量被释放的状况;
而局部变量则不一样,局部变量是有做用域的,若是blcok调用的时候blcok已经被释放了,就会出现严重的问题,因此为了不这个问题block须要捕获须要的局部变量。(好比咱们局部变量和block都卸载了viewDidLoad方法,可是我在touchesBegan方法中调用block,这个时候局部变量早就释放了,因此block要捕获局部变量)
接下来,为何auto变量是捕获的值,而静态变量是捕获的地址呢?
这是由于自动变量和静态变量存储的区域不一样,二者释放时间也不一样。
咱们在关于局部变量、全局变量的分析中讲到了自动变量是存放在栈中的,建立与释放是由系统设置的,随时可能释放掉,而静态变量是存储在全局存储区的,生命周期和app是同样的,不会被销毁。因此对于随时销毁的自动变量确定是把值拿进来保存了,若是保存自动变量的地址,那么等自动变量释放后咱们根据地址去寻值确定会发生怀内存访问的状况,而静态变量由于项目运行中永远不会被释放,因此保存它的地址值就彻底能够了,等须要用的时候直接根据地址去寻值,确定能够找到的。
那么,又有一个问题了,为何静态变量和全局变量一样不会被销毁,为何一个被捕获地址一个则不会被捕获呢?
我我的以为是静态变量和全局变量由于二者访问方式不一样形成的,咱们都知道全局变量整个项目均可以拿来访问,因此某个全局变量在全局而言是惟一的(也就是全局变量不能出现同名的状况,即便类型不一样也不行,不然系统不知道你具体访问的是哪个)而静态变量则不是,全局存储区可能存储着若干个名为height的静态变量。
因此这就致使了访问方式的不一样,好比说有个block,内部有一个静态变量和一个全局变量,那么在调用的时候系统能够直接根据全局变量名去全局存储区查找就能够找到,名称是唯一的,因此不用捕获任何信息便可访问。而静态变量而不行,全局存储区可能存储着若干个名为height的静态变量,因此blcok只能根据内存地址去区分调用本身须要的那个。
我以前有个想法:是否是由于二者访问范围不一样,全局变量能够全局访问,静态变量只能当前文件访问。但仔细想一想block即便不是在当前文件调用的,但它的具体执行代码块内代码确定是在当前文件执行的,也就是blcok内部访问变量不存在跨文件访问的状况,既然二者均可以访问到那么访问范围就不是缘由了。
-(void)viewDidLoad{ [super viewDidLoad]; void(^block)(void) = ^{ NSLog(@"%@",self); }; block(); void(^block2)(void) = ^{ NSLog(@"%@",self.address); }; block2(); }
上面这段代码中的block是怎么捕获变量的呢?
咱们转换成c++代码能够看出,咱们能够看到viewdidload其实是转换成了 void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd)
也就是这个方法转换为底层实现时是有两个参数的:self和_cmd,既然self是方法的参数,那么self确定是个局部变量,又由于这个局部变量并无static修饰,因此self应该会被捕获而且是值传递
在访问实例对象(self)的属性(address),咱们发现block并无捕获这个具体的属性而是捕获的实例对象(self),这是由于经过self就能够获取到这个实例对象的属性,捕获一个实例对象就够了,而在block内部使用这个属性的时候,也是经过实例对象来获取的↓↓
咱们在分析block底层结构的时候,看到了isa存储的是&_NSConcreteStackBlock地址,也就是这个block是个block类型的,那么blcok只有这一种类型吗?
答案是否认的,blcok有三种类型,咱们能够经过代码来验证:
有的同窗可能将oc转为c++↓↓发现oc代码编译后三个block都是StackBlock类型的,和咱们刚才打印的不同。这是由于runtime运行时过程当中进行了转变。最终类型固然以runtime运行时类型也就是咱们打印出的类型为准。
既然存在三种不一样的类型,那系统是根据什么来划分block的类型的呢?不一样类型的block分别存储在哪呢?
也就是根据两点:有没有访问auto变量、有没有调用copy方法:
而这三种变量存放在内存中的位置也不一样:__NSMallocBlock__是在平时编码过程当中最常使用到的。存放在堆中须要咱们本身进行内存管理。
关于判断类型的两个条件,咱们第一个条件也就是判断有误访问auto变量这个是明白的,可是第二个就不太清楚了,调用copy有什么用?作了哪些操做了?
__NSGlobalBlock __ 调用copy操做后,什么也不作 __NSStackBlock __ 调用copy操做后,复制效果是:从栈复制到堆;副本存储位置是堆 __NSMallocBlock __ 调用copy操做后,复制效果是:引用计数增长;副本存储位置是堆
也就是:
因为ARC环境下,系统会对一下状况下的block自动作copy处理:
//1.block做为函数返回值时 typedef void (^Block)(void); Block myblock() { int a = 10; //此时block类型应为__NSStackBlock__ Block block = ^{ NSLog(@"---------%d", a); }; return block; } int main(int argc, const char * argv[]) { @autoreleasepool { Block block = myblock(); block(); // 打印block类型为 __NSMallocBlock__ NSLog(@"%@",[block class]); } return 0; } //2.将block赋值给__strong指针时,好比(arc中默认全部对象都是强指针指引) void (^block1)(void) = ^{ NSLog(@"Hello"); };
//3.block做为Cocoa API中方法名含有usingBlock的方法参数时。例如:遍历数组的block方法,将block做为参数的时候。 NSArray *array = @[]; [array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { }]; //4.block做为GCD API的方法参数时 例如:GDC的一次性函数或延迟执行的函数,执行完block操做以后系统才会对block进行release操做。 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
因此咱们关闭ARC才能更好的看清楚copy的做用:
project -> Build settings -> Apple LLVM complier 3.0 - Language -> objective-C Automatic Reference Counting设置为NO
而后咱们定义几个不一样类型的block,并分别调用copy方法查看结果:
发现block的copy方法确实是有这样的做用,须要说明的一点,block3虽然屡次copy后打印出来的retainCount始终是1,但其内存管理器中仍然会增长。
既然copy方法最大的一个做用是把block从栈拷贝到堆,它这样作的缘由是什么?block在栈中和堆中有什么区别吗?
咱们都知道栈中对象的内存空间是随时可能被系统释放掉的,而堆中的内存空间是由开发者维护的,若是block存在于栈中就能够出现一个问题,当咱们调用block的时候,他被释放掉了,从而出现错误:
咱们发现,当咱们调用block的时候打印出来莫名其妙的东西,这是由于test方法执行完后,栈内存中block所占用的内存已经被系统回收,所以就有可能出现乱得数据。
可能有的同窗会根据上面block访问auto变量的想法来思考,blcok不是已经捕获了这个变量了么,其实这彻底是两码事,block确实是把变量a捕获到了本身内部,可是如今它本身的空间都被释放掉了,更不用说它捕获的变量了,确定被释放掉了。
因此,这种状况下,是须要将block移到堆上面的,让开发者控制它的生命周期,这就用到了copy(arc环境下不用,由于test方法中将block赋值给了一个__strong指针,会生成copy)
既然blcok存放在堆中了,block内部有捕获了a的值,因此就能够正常输出了。
咱们从这种状况也能够知道为何不一样环境下block的声明属性写法的不一样:
MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);
ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void); @property (copy, nonatomic) void (^block)(void);
copy属性意味着,系统会自动对修饰的block进行一次copy操做,
因此在mrc环境下,copy属性修饰block就不会出现上面block存在栈里,在访问时被释放的状况;
而在arc环境下,系统会在block被__strong指针引用时自动执行copy方法,因此就能够写strong和copy两种。
既然block内部能够修改静态变量和全局变量的值,而没法修改自动变量的值,那么有没有什么方式能够解决这个问题呢?
答案是确定的,咱们能够经过_ _block修饰这个自动变量,从而能够在block内部访问并修改这个自动变量了:
__block不能修饰全局变量、静态变量(static)
那么,_ _block 这个修饰符作了什么操做呢?就让可让block内部能够访问自动变量
咱们经过底层代码能够看出,__weak将int类型的数据转换成了一个__Block_byref_i_0的结构体类型
而这个结构体的结构是:
struct __Block_byref_i_0 { void *__isa; __Block_byref_i_0 *__forwarding; int __flags; int __size; int i; };
而从赋值上看,isa为0,既然有isa指针,那么说明这个结构体也是一个对象,__forwarding存储的是__Block_byref_i_0的地址值,flags为0,size为Block_byref_i_0的内存大小,i是真正存储变量值的地方,其内部结构就是这样的↓↓(关于des结构体为何会多了一个copy函数和一个dispose函数在下面相互引用中会讲到)
而当咱们在block内部修改和访问这个变量是,底层实现是这样的:
是经过__Block_byref_i_0结构体的指针__forwarding读取和修改的变量i.
为何要经过__forwarding转一下呢,而不是直接读取i
这是由于当咱们调用block的时候,block可能存在于栈中可能存在于堆中,
void (^block)(void); void test() { // __NSStackBlock__ int a = 10; block = ^{ NSLog(@"block---------%d", a); } ; //状况一:此时调用block还存在与栈中 block(); //此时block有两份 一个在栈中 一个在堆中 [blcok copy]; //一次copy对应一次release [block release]; //这个方法执行完后虽然栈中的block释放了 可是已经拷贝到堆里一份,因此仍是能够继续调用的 } int main(int argc, const char * argv[]) { @autoreleasepool { test(); //状况二:test方法执行完后 栈中的block被释放了 堆中还有一个copy的block block(); } return 0; }
__forwarding指向示意图:
若是是直接经过结构体的内存地址访问变量,由于结构体在堆中的地址和在栈中的地址确定不同,状况一和状况二很明显又是执行的同一个方法,因此就没有办法实现这个功能,也就是若是方法里是根据栈中的地址访问属性的,那么状况二就会出错,由于这个时候这个地址已经被释放了,若是是根据堆中的值去访问变量的话,那么状况一又有问题了,由于这个时候堆里尚未这个block呢。因此须要根据__forwarding指针去访问变量,这样的话才能确保状况一和状况二都会访问到这个结构体。
因此咱们总结一下上面的分析:
1.__block将int i进行包装,包装成一个__Block_byref_i_0结构体对象,结构体中的i是存储i的int值的;
2.当咱们在block内修改或访问该对象时,是经过该对象的__forwarding去找对应的结构体再找对应的属性值,这是由于__forwarding在不一样状况下指向不一样的地址,防止只根据单一的一个内存地址出现变量提早释放没法访问的状况。
那么咱们就明白为何能够修改__block修饰的自动变量了,由于__block修饰变量变成了一个对象,咱们修改只是修改的这个对象中的一个属性,并非修改的这个对象:就像这样↓↓
__block修饰下的i再也不是int类型而变成一个对象(对象p),咱们block内部访问修改和访问的是这个对象内部的int i(字符串p),因此是能够修改访问的。只不过这个转化为对象的内部过程封装起来不让开发者看到,因此就给人的感受是能够修改auto变量也就是修改时是int i。
block外部访问__block修饰的变量也是经过__forwarding指针找到结构体对象内部的int i,既然是访问你的block内部的属性i,那么就是修改后的21了↓↓
上面咱们讲到了,__block是将int类型的数据包装成了一个对象,而后block内部捕获这个对象访问或者修改对象内部的int属性,那么block对其捕获的对象变量是怎么引用怎么管理的呢?
当block在栈上时,不会对指向的对象产生强引用 当block被copy到堆时 会调用block内部的copy函数 copy函数内部会调用_Block_object_assign函数 _Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak)作出相应的操做,造成强引用或者弱引用 若是block从堆上移除 会调用block内部的dispose函数 dispose函数内部会调用_Block_object_dispose函数 _Block_object_dispose函数会自动释放指向的对象(release)
也就是block对其捕获的对象时强引用仍是弱引用,主要看block存在于哪?
若是在栈上,那么对捕获的对象一概不会产生强引用;
若是在堆上的话,这个要看这个对象自身的修饰符了,本身的修饰符是strong那就是强引用,是weak的话那就是弱引用(oc中的指针默认为strong,除非指定为weak)
这里的copy函数和dispose函数就是咱们在解析__block中看到block结构体内存描述结构体(desc)中的多出来的那两个函数:
咱们看到copy函数内部调用的是_Block_object_assign函数,assign函数传递了三个参数:对象p的地址,对象p以及3,这个函数的做用就是根据person对象是什么类型的指针,对person对象产生强引用或者弱引用。
能够理解为_Block_object_assign函数内部会对person进行引用计数器的操做,若是结构体内person指针是__strong类型,则为强引用,引用计数+1,若是结构体内person指针是__weak类型,则为弱引用,引用计数不变。
dispose函数内部调用的是_Block_object_dispose函数,dispose函数传递了两个参数,对象p和3,这个函数的做用就是对person对象作释放操做,相似于release,也就是断开对person对象的引用,而person到底是否被释放仍是取决于person对象本身的引用计数。
下面咱们经过图片来形象化地看一下这个流程:
当block从栈copy到堆的时候回自动执行des结构体重的copy函数:
当block从堆中被释放的时候,会调用dispose函数释放捕获的变量:
循环引用也是block中一个常见的问题,什么是循环引用呢?
咱们从上面block捕获对象变量的分析能够看出,block在堆中的时候会根据变量本身的修饰符来进行强引用或者弱引用,假设block对person对象进行强引用,而person若是对block也进行强引用的话,那就造成了循环引用,person对象和block都有强指针指引着,使它们得不到释放:
咱们发现当最后一行代码执行完后,dealloc并无执行,也就是person并无被释放,这就是由于循环引用。
block是p对象的一个属性,因此p对block是一个强引用关系,而block内部又捕获了p对象,p默认是强指针的,因此block对p也是一个强引用,双方就造成了这样一个关系:
当block调用完后,系统对block说:用完了你就释放吧,block说我如今释放不了,由于person对象还要用我呢。而后系统又找到person对象说你先释放吧,person说我也是放不了,由于block里面还要用我呢。就这样,person和block都双双没法释放。
那么怎么解决这个循环引用呢?很简单,只要把任意一根红线设置为弱引用就行,好比说这样↓↓
这样的话就要对代码这样修改:
@property(nonatomic,weak)MyBlock block;
测试发现确实被释放了,可是这种方案不太合理,由于咱们前面讲到了,block最好是放在栈中去操做,在arc中修饰符应该是strong或者copy,因此咱们须要再换种方案。
这样的话就须要对代码这样修改了:
person *p = [[person alloc]init]; __weak person *weakP = p; p.block = ^{ NSLog(@"%@",weakP); }; p.block();
测试发现,block和person对象都被释放了。
那么除了这个方案还有其余方法么?答案是有的,下面是循环引用的结构↓↓只要这三条线有一条是弱引用就不会发生循环引用的状况。
首先,①这个是没有办法改成弱引用的,由于block要copy到堆中就得用strong或者copy修饰,不能用weak;
咱们经过__weak是将②变为弱引用,固然除了__weak,咱们也能够用__unsafe_unretained:
__unsafe_unretained和__weak同样,表示的是对象的一种弱引用关系。 惟一的区别是:__weak修饰的对象被释放后,指向对象的指针会置空,也就是指向nil,不会产生野指针;而__unsafe_unretained修饰的对象被释放后,指针不会置空,而是变成一个野指针,那么此时若是访问这个对象的话,程序就会Crash,抛出BAD_ACCESS的异常。
__weak person *weakP = p; __unsafe_unretained person *unsafeP = p; //当p释放后,weakp会自动指向nil 而unsafeP则不会,会继续指向对象的地址,对象已经销毁,此时unsafeP访问的是"僵尸"对象
那么咱们还有一种办法能够解决循环引用,那就是将③在block执行完后手动释放:经过__block
__block person *p = [[person alloc]init]; p.block = [^{ NSLog(@"%@",p); p = nil; }copy]; p.block();
可是这个方案的话必需要求调用block,不调用的话p=nil不会执行,也就是③这条强引用仍是存在的。
因此,咱们能够总结一下解决循环引用的方案:
ARC环境:
1.用__weak、__unsafe_unretained解决;
2.用__block解决(必需要调用block)
MRC环境:
1.用__unsafe_unretained解决;
2.用__block解决(必需要调用block)。
最后再提一个面试中常常问道的问题:block能够给NSMutableArray中添加元素吗,需不须要添加__block?
答案是不须要,下面经过代码验证。
由于在block块中仅仅是使用了array的内存地址,往内存地址中添加内容,并无修改arry的内存地址,所以array不须要使用__block修饰也能够正确编译。
下面这两篇文章对block本质有着详细的介绍,同窗们也能够看一下↓↓