Effective objective-C—第六章学习–块
- 前言
- 理解“块”这一概念
- 块的基础知识
- 块的内部结构
- 全局块,堆块,栈块
- 栈块
- 堆块
- 全局块
- 要点
- 为常用的块类型创建typedef
- 要点
- 用handle块降低代码分散度
- 要点
- 用块引用其所属对象时不要引入保留环
- 要点
前言
补进度ing
理解“块”这一概念
首先要了解一下块和GCD的概念,这两者都是一起引入的
块”是一种可在 C 、C++及 Objective-C 代码中使用的 “词法闭包”(lexical closure),它极为有用,这主要是因为借由此机制,开发者可将代码像对象一样传递,令其在不同环境(context)下运行。还有个关键的地方是,在定义“块”的范围内,它可以访问到其中的全部变量。
GCD 是一种与块有关的技术,它提供了对线程的抽象,而这种抽象则基于“派发队列”(dispatch queue)(亦称调度队列)。开发者可将块排入队列中,由 GCD 负责处理所有的调度事宜。GCD 会根据系统资源情况,适时地创建、复用、摧毁后台线程(background thread),以便处理每个队列。此外,使用 GCD 还可以方便地完成常见编程任务,比如编写 “只执行一次的线程安全代码”(thread-safe single-code execution),或者根据可用的系统资源来并发执行多个操作。
块的基础知识
块的基础语法如上,int是返回值类型,块的指针名为addBlock,a,b为传入的参数,等于号后面是一个块对象 ;
块其实就是个值,而且自有其相关类型。与 int、float 或 Objective-C 对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。
块的强大之处是:在声明它的范围里。所有变量都可以为其所捕获。这也就是说,那个范围里的全部变量,在块里依然可用。
默认情况下,为块所捕获的变量,是不可以在块里修改的。在本例中,假如块内的代码改动了 additional 变量的值,那么编译器就会报错。不过,声明变量的时候可以加上 __block 修饰符,这样就可以在块内修改了。
如下:
传给 “numerateObjectsUsingBlock:” 方法的块并未先赋给局部变量,而是直接内联在函数调用里了。这种用法叫做内联块 ;
块本身也和其他对象一样,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 变量一并捕获了,因为实例变量是与 self 所指代的实例关联在一起的。
一定要记住:self 也是个对象,因而块在捕获它时也会将其保留。如果 self 所指代的那个对象同时保留了块,那么这种情况通常就会导致 “保留环”。
块的内部结构
块本身也是对象,在存放块对象的内存区域中,首个变量是指向 Class 对象的指针,该指针叫做 isa。其余内存里含有块对象正常运转所需要的各种信息。
- invoke 变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个 void * 型的参数,此参数代表块。
- descriptor 变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy 与 dispose 这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,而后者则将之释放。
- 块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在 descriptor 变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke 函数为何需要把块对象作为参数传进来呢?原因在于,执行块时,要从内存中把这些捕获到的变量读出来。
全局块,堆块,栈块
栈块
定义对象的时候是初始分配在栈上的,也就是有可能在使用之后内存被覆写,那样就是产生崩溃
堆块
为了解决问题可以给块对象发送copy信息,这样子就会把块从栈复制到堆上,块也就成了带引用计数的对象了,在ARC下编译器会自动的合理的释放对象。
全局块
除了“栈块”和“堆块”之外,还有一类块叫做“全局块”。这种块不会捕捉任何状态(比如外围的变量),运行时也无须有状态来参与。而且全局块的copy属于空操作。可以把他认为是单例。
在使用单例模式封装网络请求的时候就是使用了全局块
要点
- 块是C、C++、Objective-C 中的词法闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。
为常用的块类型创建typedef
为了隐藏复杂的块类型,需要用到C语言的类型定义,typedef关键字。
return _type(^block_name)(parameters)
上面是块的定义,如果不使用typedef,在调用时的形式比较复杂难记,如下;
所以我们一般以下面这种方式定义块变量:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
这样再调用时就很方便了 ;
EOCSomeBlock block = ^(BOOL flag, int value)
使用类型定义还有个好处,就是当你打算重构块的类型签名时会很方便 ;
其原理类似于用函数来分离一些常用代码,在更改时比较方便,也方便排错 ;
要点
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需要修改相应typedef中的块签名即可,无须改动其他typedef。
用handle块降低代码分散度
为用户界面编码时,一种常用的范式就是“异步执行任务”。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程。
异步执行任务的通常使用委托模式实现,也就协议传值
比如我们在执行网络请求,想从某个类对象获取api的data,以前常常使用委托模式,也就是设置delegate ;
当我们在使用别的类对象时,可以将该对象设置为上面那个对象的代理,这样当上面那个对象网络请求完得到的data,当前对象也能访问到 ;
这种做法确实可行,而且没什么错误。然而如果改用块来写的话,代码会更清晰。块可以令这种 API 变得更紧致,同时令开发者调用起来更加方便。
如下:
与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。
尤其是在有多个api要请求时,委托模式的代码就显得更加昂长 ;
建议使用同一个块来处理成功与失败情况
要点
- 在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
- 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用 handler
块来实现,则可直接将块与相关对象放在一起。 - 设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
用块引用其所属对象时不要引入保留环
使用块时,若不仔细思量,则很容易导致“保留环”(retain cycle)。
举个例子,就是上面用api获取器的例子,也就是通过某个类的对象获取API的data ;
#import <Foundation/Foundation.h> 45 typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data); 6789 @interface EOCNetworkFetcher : NSObject10 11 @property (nonatomic, strong, readOnly) NSURL *url;12 13 - (id)initWithURL:(NSURL *)url;14 15 - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionhandler)completion;16 17 @end
// EOCNetworkFetcher.m 23 #import "EOCNetworkFetcher.h" 4567 @interface EOCNetworkFetcher () 89 @property (nonatomic, strong, readwrite) NSURL *url;10 11 @property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;12 13 @property (nonatomic, strong) NSData *downloadedData;14 15 @end16 17 @implementation EOCNetworkFetcher18 19 - (id)initWithURL:(NSURL *)url {20 21 if ((self = [super init])) {22 _url = url;23 }24 return self;25 26 }27 28 - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {29 30 self.completionHandler = completion;31 // Start the request32 // Request sets downloadedData property33 // When request is finished,p_requestCompleted is called34 }35 36 - (void)p_requestCompleted {37 38 if (_completionHandler) {39 _completionHandler(_downloadedData);40 }41 }42 43 @end
当使用这个网络获取器时,
1 @implementation EOCClass { 23 EOCNEtworkFetcher *_networkFetcher; 4 NSData *_fetchedData; 5 } 6789 - (void)downloadData {10 11 NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];12 _networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];13 [_networkFetcher startWithCompletionHandler:^(NSData *data) {14 NSLog(@"Request URL %@ finished", _networkFetcher.url);15 _fetchedData = data;16 }];17 }18 19 @end
上面这段代码中,handle块中通过实例变量保留了self,而self又通过属性保留了handle块,所以形成了保留环 ;如图:
获取器对象之所以要把 completion handler 块保存在属性里面,其唯一目的就是想稍后使用这个块。可是,获取器一旦运行过 completion handler 之后,就没有必要再保留它了。所以,只需要将 p_requestCompleted 方法按如下方式修改即可:
- (vlid)p_requestCompleted {2 3 if (_completionHandler) {4 5 _completionHandler(_downloadedData);6 }7 self.completionHandler = nil;8 }
这样一来,只要下载请求执行完毕,保留环就解除了,而获取器对象也将会在必要时为系统所回收。请注意,之所以要在 start 方法中把 completion handler 作为参数传进去,这也是一条重要原因。
要点
- 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
- 一定要找个适当的时机解除保留环,而不能把责任推给API 的调用者。