今天博主有一个Block的需求,遇到了一些困难点,在此和大家分享,希望能够共同进步.
1.什么是Block
Block是一个C级别的语法以及运行时的一个特性,和标准C中的函数(函数指针)类似,但是其运行需要编译器和运行时支持,block是对象,有自己的ISA指针,可以随时创建,作为参数传递,作为返回值返回
block是带有局部变量的匿名函数(即没有名称的函数),就是OC中的闭包(closure),又名匿名函数,块函数,块
2.在iOS开发中,什么情况下使用Block
Block除了能够定义参数列表、返回类型外,还能够获取被定义时的词法范围内的状态(比如局部变量),并且在一定条件下(比如使用__block变量)能够修改这些状态。此外,这些可修改的状态在相同词法范围内的多个block之间是共享的,即便出了该词法范围(比如栈展开,出了作用域),仍可以继续共享或者修改这些状态。通常来说,block都是一些简短代码片段的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调.
3.Block的定义
我们通过一张图来说明:
上图这个结构是在栈中的结构,我们来看看对应的结构体定义:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size; void (*copy)(void *dst, void *src); void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ };
从上面代码看出,Block_layout就是对block结构体的定义:
isa指针:指向表明该block类型的类。
flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
reserved:保留变量,我的理解是表示block内部的变量数。
invoke:函数指针,指向具体的block实现的函数调用地址。
descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。
variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
4.Block的复制
在属性定义一个block的时候需要使用copy,因为块是在栈上分配的,一旦离开作用域, 就会释放, 因此如果你要把块用在别的地方, 必须要复制一份
在ARC下, 以下几种情况, Block会自动被从栈复制到堆
被执行copy方法
作为方法返回值
将Block赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时
在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递的时候.
5.关键字__block
对于block外的变量引用,block默认是将其复制到其数据结构中来实现访问的,如下图:
通过block进行闭包的变量是const的。也就是说不能在block中直接修改这些变量。来看看当block试着增加x的值时,会发生什么:
myBlock = ^( void )
{
x++;
return x;
};
编译器会报错,表明在block中变量x是只读的。
有时候确实需要在block中处理变量,怎么办?别着急,我们可以用__block关键字来声明变量,这样就可以在block中修改变量了。
基于之前的代码,给x变量添加__block关键字,如下:
__block int x;
对于用__block修饰的外部变量引用,block是复制其引用地址来实现访问的,如下图:
6.Block循环引用
在实际项目中对于Block最常见的问题应该是循环引用。如果Block中使用了 __strong
修饰符的对象,那么当block从栈复制到堆时,该对象为Block所持有。这样容易造成循环引用,比较明显的我想大家肯定遇到过,我们来看一个比较隐蔽的,源代码如下: typedef void (^blk_t)(void);
@interface MyObject : NSObject @property (nonatomic, strong) id obj; @property (nonatomic, strong) blk_t blk; @end @implementation MyObject - (id)init { self = [super init]; _blk = ^{NSLog(@"obj = %@",_obj);}; return self; } @end
通过编译器我们可以看到造成了循环引用,即Block语法内部使用了 _obj
变量,是因为 _obj
变量实际上截获了self。对编译器来说, _obj
变量只不过是对象的成员变量罢了。
解决的方法便是便是通过 __weak
修饰符来修饰会被Block捕获的变量:
id __weak obj = _obj;
_blk = ^{NSLog(@"obj = %@",obj);};
还有一种定义设置weak变量的方式,可以用于宏定义。代码如下:
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
调用 WS(ws)
之后 ws
就变成为了 __weak
修饰符修饰的 self
了.
7.关于Block的应用:
block 属性,一般用 copy 修饰;
7.1.如果没有对block进行copy操作,block就存储于栈空间
7.2. 如果对 block 进行 copy 操作, block 就存储于堆空间 --- 强引用
7.3.如果block存储于栈空间,不会对block内部所用到的对象产生强引用
7.4.如果block存储于堆空间,就会对block内部所用到的对象产生强引用
注意 :
7.4.1 :由于使用了 copy 修饰,如果 block 中调用了 block 属性的对象,就会造成循环引用
为了避免循环引用,需要对对象进行若引用修饰:
ICKPerson *p = [[ICKPerson alloc] init];
// 1、修饰方法1
// __unsafe_unretained typeof(p) weakP = p; // 2、修饰方法2 __block typeof(p) weakP = p; p.testBlock = ^{ [weakP run]; };
7.4.2 、关于 block 中变量的值:
7.4.2.1 如果变量没有通过 __block 修饰,那么 block 中的变量本质是值捕获,在创建 block 的同时,是将变量的值传入到 b lock 中 , 无论什么时候调用,变量的值就是最初传进去的值
int age = 10;
void (^block)() = ^{ // 值捕获 NSLog(@"age=%d", age);// 打印是10; }; age = 20; block();
7.4.2.2 如果变量通过 __block 修饰,那么 block 中的变量实际传递的是变量的地址,在创建 block 的同时,是将变量的地址传入到 b lock 中 , 在调用block的时候,其变量的值是当时变量的值(通过地址(指针)获取到)。
__block int age = 1;
void (^block)() = ^{ // 值捕获 NSLog(@"age=%d", age);// 打印是20; }; age = 20; block();
7.4.2.3 、关于 block 的内部实现:
创建 block 的时候,内部是创建了对应的函数;
在调用 block 的时候,是调用了之前封装的函数。
7.4.2.4.3.1 、在头文件中(向其他文件中传递数据的文件)定义一个 block :是否带参数,根据需求确定
@class ICKAddViewController,ICKContact;
typedef void(^ICKAddViewControllerBlock)(ICKContact *contact);
@interface ICKAddViewController : UIViewController @property (nonatomic, strong) ICKAddViewControllerBlock contactBlock; @end
7.4.2.4.3.2 、在获取数据后,跳转页面之前,调用 block ,将数据传递过去
- (IBAction)addcontact {
ICKContact *contact = [ICKContact contactWithName:self.nameFiled.text andPhone:self.phoneFiled.text];
// 调用block if (self.contactBlock) { self.contactBlock(contact); } [self.navigationController popViewControllerAnimated:YES]; }
7.4.2.4.3.3 、在获取(保存、利用)数据的文件中(拿到获取数据的对象的时候)调用其 block 属性,保存 block 代码段(实现特定功能的代码)
// 跳转控制器时数据传递
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
ICKAddViewController *addVc = segue.destinationViewController; // 声明block addVc.contactBlock = ^(ICKContact *contact){ [self.contacts addObject:contact]; // 存储数据 NSString *cache = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; NSString *path = [cache stringByAppendingString:@"contacts.data"]; [NSKeyedArchiver archiveRootObject:self.contacts toFile:path]; [self.tableView reloadData]; }; }
8.Block的存储类型
block的存储形态有三种:_NSConcretStackBlock(栈)、_NSConcretGlobalBlock(全局)、_NSConcretMallocBlock(堆)
要点一:当block在函数内部,且定义的时候就使用了函数内部的变量,那么这个 block是存储在栈上的。
要点二:当block定义在函数体外面,或者定义在函数体内部且当时函数执行的时候,block体中并没有需要使用函数内部的局部变量时,也就是block在函数执行的时候只是静静地待在一边定义了一下而不使用函数体的内容,那么block将会被编译器存储为全局block。
要点三:全局block存储在堆中,对全局block使用copy操作会返回原函数指针;而对栈中的block使用copy操作,会产生两个不同的block地址,也就是两个匿名函数的入口地址。
要点四:ARC机制优化会将stack的block,转为heap的block进行调用。
9.Block存储域
__block
变量转换成了 __block
变量的结构体类型的自动变量。所谓结构体类型的自动变量,即栈上生成的该结构体的实例。如下表所示:
名称 实质 Block 栈上Block的结构体实例 __block变量 栈上__block变量的结构体实例
前面我们看到Block的类型说明_NSConcreteStackBlock.虽然该类没有出现已变换源代码中,但还有与之相识的类,如:
- _NSConcreteStackBlock
- _NSConcreteGlobalBlock
- _NSConcreteMallocBlock
他们分别对应的存储区域如下所示:
类 设置对象的存储区域 _NSConcreteStackBlock 栈 _NSConcreteGlobalBlock 程序的数据区域(.data区域) _NSConcreteMallocBlock 堆
定义Block时期内存区域分配在栈中,其Block类型为 __NSConcreteStackBlock
类对象.
那么 _NSConcreteMallocBlock
类型的对象由何而来?这里肯定有人还在疑惑为什么 __block
变量转化而来的结构体为生成指向自身的 __forwarding
指针变量。其目的是为了能然超出范围的Block也有效。有人会说设置个全局的Block不就可以搞定了么。
行不行先来段代码看看:
void (^blk)();
if (/* some condition */) {
blk = ^{ NSLog(@"Block A"); }; }else { blk = ^{ NSLog(@"Block B"); }; } blk();
看这段代码我声明了全局的Block变量blk,然后在if语句中定义。如果你不理解block那么就很容易写出这样的代码,其实这段代码是很危险的。因为全局的blk变量是分配在栈上的。在if和else语句中定义的blk内容,编译器会给每个块分配好栈内存,然后等离开了相应的范围之后,编译器有可能把分配给块的内存覆写了。如果编译器未覆写这块栈内存则程序照常运行,如果这块内容被覆写那么程序就会崩溃。
解决上面问题的方法就是使用 copy
方法,将block拷贝到堆中。拷贝完之后就是接下来要将的 _NSConcreteMallocBlock
类型。该类型是带有引用计数的对象,如果在ARC下,只要引用计数不为0,可以随意的访问,后继的内存管理就交给编译器来完成了。
还有一种类型是 _NSConcreteGlobalBlock
类型,这类Block不会捕捉任何状态的外部变量。块所使用的整个内存区域,在编译器已经完全的确定了,不需要每次调用时在栈中创建,如下就是一个全局快:
void (^blk)() = {
NSLog("This is a global block");
}
void main() { }
10.__block变量存储域
如果Block配置在栈中,则在Block中使用的 __block
变量也分配在栈中。当Block被复制到堆中时, __block
变量也一并被复制在堆中,并被Block所持有。如果非配在堆中的Block被废弃,那么它所使用的 __block
变量也就被释放了。下面来看堆和栈上 __block
混用的例子
__block int val = 0;
void (^blk) (void) = [^{val++;} copy]; ++val; blk(); NSLog(@"val:%d",val);
执行结果为:
val: 2
在Block中和在Block外修改 __block
变量完全等效,它是怎么实现的呢?
是因为执行 copy
方法之后,Block被复制到堆中,其内部捕获的 __block
变量也一并被复制。而此时分配在栈上的val任然存在的,栈上的 __block
变量val会将原本指向自身的 __forwarding
指针指向复制到堆中的 __block
变量val的地址。这样堆中的 __block
变量被修改之后就等同于栈上的block被修改。
通过该功能,无论是在Block的语法中、Block语法外使用 __block
变量,还是 __block
变量配置在栈上还是堆上,都可以顺利地访问一个 __block
变量。
11.截获对象
先来看一段Blcok截获可变数组对象的例子:
blk blk;
{
NSMutableArray *array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj]; NSLog(@"arrayCount = %lu",(unsigned long)array.count); }; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); blk([[NSObject alloc] init]);
执行该段代码的结果为
arrayCount = 1; arrayCount = 2; arrayCount = 3;
从表面上看是没什么问题,运行的结果也是正确的。而实际上如果我们大量的调用block向可变数组中添加对象元素程序会强制结束。原因是block截获的NSMutableArray对象是分配在栈上的,随着当可变数组元素增加到一定程度会造成栈溢出。
解决方法是调用 copy
方法,形式如下:
blk = [^(id obj){
[array addObject:obj];
NSLog(@"arrayCount = %lu",(unsigned long)array.count); } copy];
http://my.oschina.net/u/1432769/blog/390401
http://www.tuicool.com/articles/Zbuiuau?plg_nld=1&plg_uin=1&plg_auth=1&plg_nld=1&plg_usr=1&plg_vkey=1&plg_dev=1
http://www.jianshu.com/p/4b1cfd7e6361?plg_nld=1&plg_uin=1&plg_auth=1&plg_nld=1&plg_usr=1&plg_vkey=1&plg_dev=1
http://www.tanhao.me/pieces/310.html/