浅析Block闭包

简单来说,block就是将函数及其上下文封装起来的对象,从功能上可以把它看作是C++中的匿名函数,也可称之为块。

Block类型写法:

返回值+(^块名)+(参数)= ^(参数){ 内容 }

如下所示:

int (^myBlock)(int a, int b) = ^(int a, int b){ return a + b; }; Block结构 Block存储区域

Block本质上也是OC对象,所以每个Block对象也有isa指针指向它们的类对象。根据Block类对象存储的内存空间的不同可分为三种不同的类,分别是:

位于全局区的Block类:__NSGlobalBlock__

位于栈区的Block类:__NSStackBlock__

位于堆区的Block类:__NSMallocBlock__

全局区Block:当Block不捕获外部变量时,会被编译器分配到全局区。因为无外部变量,所以运行时不会在Block内部进行copy或dispose操作,为了削减开销,所以在编译时就确定了大小,即存储在全局区。如下:

void (^myBlock)(void)=^(void){ NSLog(@"global"); }; NSLog(@"%@",[myBlock class]); //输出: //__NSGlobalBlock__

栈区Block:当Block捕获了外部变量后,会被分配到栈区。但是在ARC环境下,系统会自动为生成的栈区Block进行copy操作,所以为了验证是否是在栈区,需要采用MRC环境,在main.m文件的编译选项设置为: -fno-objc-arc后运行如下代码:

NSString* flag=@"yes"; void (^myBlock)(void)=^(void){ NSLog(@"stack:%@",flag); }; NSLog(@"%@",[myBlock class]); //输出: //__NSStackBlock__

堆区Block:在MRC模式下,用copy后,会将栈区block复制到堆区。在ARC模式下,系统自动将初始化的Block复制到堆区。

//MRC环境下: NSString* flag=@"yes"; void (^myBlock)(void)=[^(void){ NSLog(@"stack:%@",flag); } copy]; NSLog(@"%@",[myBlock class]); //输出: //__NSMallocBlock__

Block内部结构

官方的Block定义在 Block_private.h中,具体的源码:Block_private.h

#define BLOCK_DESCRIPTOR_1 1 struct Block_descriptor_1 { uintptr_t reserved; uintptr_t size; }; #define BLOCK_DESCRIPTOR_2 1 struct Block_descriptor_2 { // requires BLOCK_HAS_COPY_DISPOSE void (*copy)(void *dst, const void *src); void (*dispose)(const void *); }; #define BLOCK_DESCRIPTOR_3 1 struct Block_descriptor_3 { // requires BLOCK_HAS_SIGNATURE const char *signature; const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT }; //Block结构 struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables };

isa指针:指向类对象的指针,即根据不同分区指向: __NSGlobalBlock__、 __NSStackBlock__和 __NSMallocBlock__,但是这里的底层isa实际上指向的是父类的结构体(C语言)即:_NSConcreteGlobalBlock、 _NSConcreteStackBlock和 _NSConcreteMallocBlock结构体,但意义是一样的。

flags:类型为枚举,主要用来保存Block的状态信息。

reserved:为之后开发准备的保留信息,暂时无用。

invoke:函数指针,指向的是实际的功能运行函数。在invoke函数的参数中还包含了Block结构体本身,这么做的目的是在执行时,可以从内存中获取block中捕获的变量。

descriptor:主要存储Block的附加信息,其中包括占址大小、签名等。默认指向Block_descriptor_1结构体,当Block被copy到堆上时,则会添加Block_descriptor_2和Block_descriptor_3,新增copy和dispose方法用来拷贝和销毁捕获的变量。

Block内部结构图(来自于Effective-OC):

浅析Block闭包

Block作用

在日常的开发中,使用Block的主要用处在以下两个方面:

作为回调的方式之一,对比于代理模式,Block可将将分散的代码块集中写在一处编写。因为有捕获变量的机制,所以可以很轻松的访问上下文,并且Block的代码是内联的,运行效率会更高。

正是因为有了以上的优势,所以在编写异步代码,作为异步处理回调时,在封装时往往会采用handler块的方式来编写相关代码。

在编写handler块时有两种策略,一种是在一个方法中提供提供两个Block块分别处理CompletionHandler和errorHandler,另外一种是只提供一个Block块,在Block块中提供error参数,用户自己来对error值进行判断。一般我们更倾向于后者的方式,因为这样处理数据会更加灵活

两种Handler风格如下:

Downloader *myDownloader = [[Downloader alloc] initWithURL:url]; [myDownloader downloadWithCompletionHandler:^(NSData *onlineData){ //download success } failureHandler:^(NSError *error){ //handle error }]; Downloader *myDownloader = [[Downloader alloc] initWithURL:url]; [myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){ if(succeeded){ //download success } else{ //handle error } }]; Block内存泄漏

当几个oc对象互相强引用成环时,就会导致对象永远都不会被释放,当这些对象的数量很大时,就会造成内存泄漏,从而导致整个系统crash的风险。

举个例子:

当A类对象强引用了B类对象,B类对象强引用了C类对象,而C对象又强引用了A类对象。假设它们都在一个代码段中。如下图所示:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppwjf.html