[编写高质量iOS代码的52个有效方法](九)块(block)
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway
先睹为快
37.理解块这一概念
38.为常用的块类型创建typedef
39.用handler块降低代码的分散程度
40.用块引用其所属对象时不要出现保留环
目录
第37条:理解块这一概念
块与函数类似,只不过是直接定义在另一个函数里,和定义它的那个函数共享同一个范围内的东西。块用^符号来表示,后面跟着一对花括号,括号里面是块的实现代码。
// 块的语法结构
return_type (^block_name)(parameters)
// 定义一个没有参数没有返回值的最简单的块
void (^someBlock)() = ^{
// code
};
// 定义一个接收2个int参数,返回int值的块
int (^addBlock)(int, int) = ^(int a, int b){
return a + b;
};
块的强大之处在于,在它的声明范围你,所有变量都可以为其捕获,也就是说范围内的全部变量在块内依然可用。
int additional = 5;
int (^addBlock)(int, int) = ^(int a, int b){
return a + b + additional;
};
int add = addBlock(2,5); // add = 12
默认情况下,为块所捕获的变量是不可以在块内修改的。不过声明变量时加上__block修饰符就可以在块内修改了。如果将块定义在Objective-C类的实例方法中,则无须加上__block修饰符也能修改类的实例变量,并可以使用self属性(存取实例变量时也会自动捕获self属性,需注意避免保留环)。
@implementation EOCClass{
NSString *_anInstanceVariable;
}
- (void)anInstanceMethod{
__block NSString *aBlockVariable = @"Anything";
void (^someBlock)() = ^{
_anInstanceVariable = @"Something";
aBlockVariable = @"Another thing";
NSlog(@"%@ %@", _anInstanceVariable, aBlockVariable);
}
someBlock();
}
@end
定义块时,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内有效。例如下面这段代码就有危险:
void (^someBlock)();
if(/* some condition */){
someBlock = ^{
NSLog(@"Block A");
};
}else{
someBlock = ^{
NSLog(@"Block B");
};
}
someBlock();
定义在if及else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应范围后,编译器可能把分配给块的内容覆写掉,于是运行时可能会程序崩溃。
可以给块对象发送copy消息以拷贝,这样就把块从栈上复制到堆上了。拷贝后的块可以在定义它的范围之外使用,而且复制到堆上后,块就成了带引用计数的对象了。由ARC负责管理释放。
void (^someBlock)();
if(/* some condition */){
someBlock = [^{
NSLog(@"Block A");
} copy];
}else{
someBlock = [^{
NSLog(@"Block B");
} copy];
}
someBlock();
除了栈块和堆块以外,还有一类块叫做全局块,这种块不会捕捉任何状态,运行时也无需有状态参与,块所使用的整个内存区域在编译期已经完全确定了。下面就是一个全局块:
void (^someBlock)() = ^{
NSLog(@"This is a block");
};
运行全局块所需的全部信息都能在编译期确定。
第38条:为常用的块类型创建typedef
每个块都具备其固有类型,因而可将其赋值给适当类型的变量,这个类型由块所接受的参数及其返回值组成。
// 定义新类型,表示接受BOOL及int参数并返回int值的块,类型名为EOCSomeBlock
typedef int(^EOCSomeBlock) (BOOL flag, int value);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建EOCSomeBlock类型变量
// block1与block2虽然实现不同,但类型相同
EOCSomeBlock block1 = ^(BOOL flag, int value){
if (flag) {
return value * 5;
}else{
return value * 10;
}
};
EOCSomeBlock block2 = ^(BOOL flag, int value){
return flag * value;
};
}
return 0;
}
通过这项特性,可以把使用块的API做的更易用些。类里面有些方法可能需要块来做参数,不如执行异步任务时所用的“completion handler”,参数就是块,凡是遇到这种情况都可以通过定义别名使代码变得更为易读。
// 不用typedef
- (void)startWithCompletionHandler:(void(^)(NSdata *data, NSError *error))completion;
// 使用typedef
typedef void(^EOCCompletionHandler)(NSdata *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;
在使用块类型的类中定义这些typedef时,还应该把这个类的名字加在由typedef所定义的新类型前面,这样可以阐明块的用途,还可以用typedef给同一个块签名类型创建数个别名。例如Mac OS X与iOS的Accounts框架就是个例子,其中有:
typedef void(^ACAccountStoreSaveCompletionHandler)(BOOL success, NSError *error);
typedef void(^ACAccountStoreRequestAccessCompletionHandler)(BOOL success, NSError *error);
这两个类型定义的签名相同,但是用在不同地方,便于开发者理解其用途。
第39条:用handler块降低代码的分散程度
异步方法在执行完任务之后,需要以某种方式通知相关代码。实现此功能有很多办法,常见的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时得到通知了。
例如用委托模式设计一个从URL中获取数据的类:
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher didFinishWithData:(NSData*)data;
@end
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
- (id)initWithURL:(NSURL*)url;
- (void)start;
@end
而其他类则可以像这样来使用此类提供的API
- (void)fetchFooData{
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
fetcher.delegate = self;
[fetcher start];
}
-(void)networkFetcher:(EOCNetworkFetcher*)fetcher didFinishWithData:(NSData*)data{
_fetchedFooData = data;
}
这样做没有问题,但是改用块来写的话会更加清晰。将获取数据的类修改为:
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
这和使用委托协议很像,不过可以直接以内联形式定义completion handler:
- (void)fetchFooData{
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetcherFooData = data;
}];
}
与委托模式的代码相比,用写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑和启动异步任务所用的代码放在了一起。而且块声明在创建获取器的范围内,它可以访问此范围内的全部变量。
这种写法还有其他用途,比如现在很多基于块的API都是用块来处理错误。这里又分为两种写法,可以分别用两个处理程序来分别处理成功和失败的情况,也可以把处理成功和失败情况的代码都封装到同一个handler中:
// 分别处理成功和失败
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end
// 处理成功和失败封装到同一个handler
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end
全部逻辑写到一起,会令块变得比较长,且比较复杂。但是更灵活,比如数据下载到一半时网络故障了,可以把数据及相关错误都回传给块。除此之外还有个优点,调用API的代码可能会在处理成功响应的过程中发现错误,采用单一块的话,那么就能把这种情况和获取器认定的失败情况统一处理了。
handler块也能定义成类的属性:
typedef void(^EOCNetworkFetcherCompletionHandler)(float progress);
@interface EOCNetworkFetcher : NSObject
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler progressHandler;
@end
第40条:用块引用其所属对象时不要出现保留环
使用块时,若不仔细思量,很容易导致保留环。例如下面这个类,提供了一套从URL中下载数据的接口:
// EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property(nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end
// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property(nonatomic, strong) NSData *downloadedData;
@end
@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL*)url{
if((self = [super init])){
_url = url;
}
reutrn self;
}
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion{
self.completionHandler = completion;
// 开始请求网络数据,结束后调用p_requestCompleted方法
}
// 处理下载数据的私有方法
- (void)p_requestCompleted{
if(_completionHandler){
_completionHandler(_downloadedData);
}
}
@end
某个类可能创建这种网络数据获取器对象,并用其从URL中下载数据
@implementation EOCClass{
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchData;
}
-(void)downloadData{
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"]
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchData = data;
}];
}
@end
这个时候就出现了保留环,由于completion handler块要设置EOCClass对象的实例变量,所以它必须捕获self属性,也就是说,handler块保留了创建网络数据获取器的EOCClass对象。而EOCClass又通过strong实例变量保留了获取器,最后获取器对象又保留了handler块,形成了保留环。
要打破保留环可以令_networkFetcher不再引用获取器,或者获取器的completionHandler属性不再持有handler块。例如,可以这样修改:
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchData = data;
// 令_networkFetcher不再引用获取器
_networkFetcher = nil;
}];
但这样做的缺点是,必须等completion handler运行后才能打破环,如果completion handler一直不运行,那么保留环就会一直存在,于是内存就会泄漏。
如果用另一种写法,EOCClass不再保留获取器:
-(void)downloadData{
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"]
EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", networkFetcher.url);
_fetchData = data;
}];
}
@end
这样不会出现之前的保留环,但又会出现了新的保留环。completion handler需要通过获取器对象来引用其中的URL,于是块要保留获取器,而获取器又经由completionHandler属性保留了块。
要打破这个保留环可以令获取器一旦运行过completion handler后就不再保留它。
- (void)p_requestCompleted{
if(_completionHandler){
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}