第四十三条 掌握GCD和操作队列(NSOperation)的使用时机

虽然GCD很棒,但是我们有时候也应该选用正确的工具来使用。所以我们这里来介绍下NSOperation

很少有其他技术能与GCD的同步机制相媲美。对于那些只需执行一次的代码来说,使用GCDdispatch_once最为方便。

但是如果执行后台任务,NSOperation也许更适合。NSOperation是在GCD出现之前就有的一种技术。甚至现在操作队列在底层是用GCD来实现的。

在两者的诸多差别中,首先要注意:GCD是纯C的API,而操作队列则是Objective-C的对象。GCD中,任务用块来表示,而块是个轻量级数据结构(参见第37条)。与之相 反,“操作"(operation)则是个更为重量级的Objective-C对象。虽说如此,但GCD并不总是最佳方案。有时候采用对象所带来的开销微乎其微,使用完整对象所带来的好处反而大大超过其缺点。

所以我们来介绍下它的好处:

  • 取消某个操作。如果使用操作队列,那么想要取消操作是很容易的。运行任务之前, 可以在NSOperation对象上调用cancel方法,该方法会设置对象内的标志位,用以表明此任务不需执行,不过,已经启动的任务无法取消。若是不使用操作队列,而是把块安排到GCD队列,那就无法取消了。那套架构是“安排好任务之后就不管了"(fire and forget)。

  • 指定操作间的依赖关系。一个操作可以依赖其他多个操作。开发者能够指定操作之间的依赖体系,使特定的操作必须在另外一个操作顺利执行完毕后方可执行。比方说,从服务器端下载并处理文件的动作,可以用操作来表示,而在处理其他文件之前,必须先下载“清单文件”(manifest file)。后续的下载操作,都要依赖于先下载清单文件这一操作。如果操作队列允许并发的话,那么后续的多个下载操作就可以同时执行,但前提是它们所依赖的那个清单文件下载操作已经执行完毕。

  • 通过键值观测机制监控NSOperation对象的属性。NSOperation对象有许多属性都适合通过键值观测机制(简称KVO)来监听,比如可以通过isCancelled属性来判断任务是否已取消,又比如可以通过isFinished属性来判断任务是否已完成。如果想在某个任务变更其状态时得到通知,或是想用比GCD更为精细的方式来控制所要执行的 任务,那么键值观测机制会很有用。

  • 指定操作的优先级。操作的优先级表示此操作与队列中其他操作之间的优先关系。优先级高的操作先执行,优先级低的后执行。操作队列的调度算法(scheduling algorithm)“不透明"(opaque),但必然是经过一番深思熟虑才写成的。反之,GCD则没有直接实现此功能的办法。GCD的队列确实有优先级,不过那是针对整个队列来说的,而不是针对每个块来说的。而令开发者在GCD之上自己来编写调度算法,又不太合适。因此,在优先级这一点上,操作队列所提供的功能要比GCD更为便利。

NSOperation对象也有“线程优先级”(thread priority),这决定了运行此操作的线程处在 何种优先级上。用GCD也可以实现此功能,然而采用操作队列更简单,只需设置一个属性。

  • 重用 NSOperation对象。系统内置了一些NSOperation的子类(比如NSBlockOperation)供开发者调用,要是不想用这些固有子类的话,那就得自己来创建了。这些类就是普通的Objective-C对象,能够存放任何信息。对象在执行时可以充分利用存于其中的信息,而且还可以随意调用定义在类中的方法。这就比派发队列中那些简单的块要强大许多。这些NSOperation类可以在代码中多次使用,它们符合软件开发中的“不重复”原则。

正如大家所见,操作队列有很多地方胜过派发队列。操作队列提供了多种执行任务的方式,而且都是写好了的,直接就能使用。

所以我们说,某些功能确实可以用高层的Objective-C方法来做,但这并不等于说它就一定比底层实现方案好。要想确定哪种方案更佳,最好还是测试一下性能。

要点

  • 在解决多线程与任务管理问题时,派发队列并非唯一方案。
  • 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。
2017/9/16 posted in  Effective OC2.0

第四十二条 多用GCD,少用performSelector系列方法

Objective-C本质上是一门非常动态的语言(参见第11条),NSObject定义了几个方法, 令开发者可以随意调用任何方法。这几个方法可以推迟执行方法调用,也可以指定运行方法所用的线程。这些功能原来很有用,但是在出现了GCD之后,这些功能就尽量不要使用了,尽量用GCD来取代他们。

其中他们又如下几个方法:

-(id)performSelector : (SEL) selector
//可以传两个参数
-(id)performSelector : (SEL) selector
           withObject:(id)objectA
           withObject:(id)objectB
//传一个参数
-(id)performSelector : (SEL) selector
           withObject:(id)object

具体用法如:

[object performSelector: @selector(selectorName)];

[object performSelector: @selector(setValue:)
             withObject: newValue];

上面的用法,会有很多局限,比如在ARC下,会发出警告表示:也许会内存泄露,这是因为编译器并不知道将要的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚,而且由于编译器不知道方法名,所以就没办法运用ARC的内存管麵则来判定返回值是不是该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而这么
做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

另一个局限性是:返回值只能是void或对象类型。尽管所要执行的 选择子也可以返回void,但是performSelector方法的返回值类型毕竟是id。如果想返回整数或浮点数等类型的值,那么就需要执行一些复杂的转换操作了,而这种转换很容易出错。

第三个局限是:这个方法最多只能传递两个参数,当选择子得到参数超过两个时,只能运用字典来传送数据(但是容易增加开销和造成bug)。

所以我们的解决方法是:我们使用来代替,并且performSelector系列方法都可以使用GCD机制使用块来实现。我们来举几个例子说明:

如果我们想要延后执行某个任务:

//Using performSelector:withObjectrafterDelay: 
[self performSelector:@selector(doSomething)
           withObject:nil 
           afterDelay:5.0];

//Using dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64 t)(5.0 * NSEC PER SEC));

dispatch_after(time, dispatch_get_main—queue(), ^(void){
     [self doSomething];
});

后者想要在主线程执行某个任务:

// Using performSelectorOnMainThread: withObject .-waitUntilDone :
[self performSelectorOnMainThread:@selector(doSomething)
                      withObject:nil 
                      waitUntilDone:NO];
//Using dispatch_async
// (or if waitUntilDone is YES, then dispatchasync) 
dispatch_async(dispatch_get_main_queue(), ^{
        [self doSomething];
});

要点

  • performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
  • performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
  • 如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用大中枢派发机制的相关方法来实现。
2017/9/11 posted in  第六章 块与GCD

第四十一条 多用派发队列,少用同步锁

OC中,如果有多个线程要执行同一份代码,那么有时可能会出问题。这种情况下,通常要使用锁来实现某种同步机制。在GCD出现之前,有两种办法,第一种是采用内置的“同步块”(synchronization block):

-(void)synchronizedMethod {
    @synchronized(self) {
    //Safe
    }
}

这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,锁就释放了。在本例中,同步行为所针对的对象是self。这么写通常没错,因为它可以保证每个对象实例都能不受干扰地运行其synchronizedMethod方法。然而,滥用 @SynChronized(self)则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。

另一种加锁的办法是NSLock对象:

_lock = [[NSLockalloc] init];

-(void)synchronizedMethod {
    [_lock lock];
    //Safe
    [_lock unlock];
}

也可以使用NSRecursiveLock这种“递归锁’(recursive lock),线程能够多次持有该锁,而不会出现死锁(deadlock)现象。

虽然上面两种方法可以用,但是也有一些缺陷,比如:在某些情况下,同步快会导致死锁,另外,效率不是很高。而如果直接使用锁对象的话,一旦遇到死锁,就会非常麻烦。

所以我们一般使用GCD来替代,它的优点就是可以简单,高效的为代码加锁。属性因为需要经常性的同步,所以当要线程安全的时候,加上“atomic”特质来修饰属性。

但是如果我们想要自己实现属性访问方法时,可以:

-(NSString*)someString {
    @synchronized(self) {
        return _someString;
    }
}
-(void)setSomeString:(NSString*)someString { 
    @synchronized(self) {
        _someString = someString;
    }
}

但是滥用@synchronized(self)会很危险,因为所有同步块都会彼此抢夺同一个锁。要是有很多个属性都这么写的话,那么每个属性的同步块都要等其他所有同步块执行完毕才能执行,这也许并不是开发者想要的效果。我们只是想令每个属性各自独立地同步。

而且上述做法并不是绝对的线程安全。因为在两次访问操作之间,其他线程可能会写入新的属性值。

这里我们使用“串行同步队列”(serial synchronization queue)。将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

-(NSString*)someString {
    _block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
    return localSomeString;
}

-(void)setSomeString:(NSString*)someString { 
    dispatch_sync(_syncQueue,^{
        _someString = someString;
    });
}

此模式的思路是:把设置操作与获取操作都安排在序列化的队列里执行(串行同步队列并不会拓展新的线程),这样的话,所有针对属性的访问操作就都同步了。(关于GCD的串行队列/并发队列与iOS多线程这里不详细讲解了,后续深入探讨)。

多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,利用这个特点,还能写出更快一些的代码来。此时正可以体现出GCD写法的好处。用同步块或锁对象,是无法轻易实现出下面这种方案的。这次不用串行队列,而改用并发队列(concurrentquene):

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

-(NSString*}someString {
    __block NSString *localSomeString; 
    dispatch_sync( _syncQueue, ^{
        localSomeString = _someString;
    }); 
    return localSomeString;
}

-(void)setSomeString:(NSString*)someString { 
        dispatch_async(syncQueue, ^{
        _someString = someScring;
    });
}

光是上面这些代码还不够,所有读取操作与写入操作都会在同一个队列上执行,不过由于是并发队列,所以读取与写人操作可以随时执行。而我们恰恰不想让这些操作随意执行。这就要用到栅栏(barrier),是GCD中的一个功能:

void dispatch_barrier_async(dispatch_queue_t queue,
                            dispatch_block_t block);
void dispatch_barrier_sync(dispatch_queue_t queue,
                            dispatch_block_t block);

在队列中,栅栏块必须单独执行,不能与其他块并行。这只对并发队列有意义,因为串行队列中的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块 (barrier block),那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。

在本例中,可以用栅栏块来实现属性的设置方法。在设置方法中使用了栅栏块之后,对属性的读取操作依然可以并发执行,但是写人操作却必须单独执行了。例如:

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

-(NSString*}someString {
    __block NSString *localSomeString; 
    dispatch_sync( _syncQueue, ^{
        localSomeString = _someString;
    }); 
    return localSomeString;
}

-(void)setSomeString:(NSString*)someString { 
        dispatch_barrier_async(syncQueue, ^{
        _someString = someScring;
    });
}

执行的顺序如图所示:

测试一下性能,你就会发现,这种做法肯定比使用串行队列要快。注意,设置函数也可以改用同步的栅栏块(synchronous barrier)来实现,那样做可能会更髙效,其原因刚才已经 解释过了。最好还是测一测每种做法的性能,然后从中选出最适合当前场景的方案

要点

  • 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块NSLock对象更简单。
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
  • 使用同步队列及栅栏块,可以令同步行为更加髙效。
2017/9/9 posted in  第六章 块与GCD

第四十条 用块引用其所属对象时不要出现保留环

我们使用块的时候,如果不仔细思索,很容易出现“保留环”,我们来举个例子,下面这个类就提供了一套接口,调用者可由此从某个URL中下载数据。在启动获取器时,可设置 completion handler,这个块会在下载结束之后以回调方式执行。为了能在下载完成后通过p_requestCompleted方法执行调用者所指定的块,这段代码需要把completion handler保存到实例变量里面。

// EOCNetwor kFetcher. 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;



// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property(nonatomic, strong, readwrite) NSURL *url;
@property(nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler; (nonatomic, strong) NSData *downloadedData;
@implementation EOCNetworkFetcher

-(id)initWithURL:(NSURL*)url {
    if ((self = [super init])) {
        _url = url;
    }
    return sel£;
}

-(void)startWithCompletionHandler:
        (EOCNetworkFetcherCompletionHandlei) completion
{
    self.completionHandler = completion;
    //Start the request
    // Request sets downloadedData property
    //When request is finished, p_requestCompleted is called
}

-(void)p_requestCompleted { 
    if (_completionHandler){ 
    _completionHandler(_downloadedData);
    }
}
@end

某个类可能会创建这种网络数据获取器对象,并用其从URL中下载数据:

@implemantation EOCClass {
    EOCNetworkFetcher *_networkFetcher;
    NSData *_fetchedData;
}
-(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);
        _fetchedData = data;
    }];
}
@end

这段代码没有什么问题,但是里面有一个隐蔽的保留环,因为completion handler块要设置_fetchedData实例变量,所以它必须捕获self变量(变量捕获问题详见第37条。这就是说,handler块保留了创建网络数据获取器的那个EOCClass实例。而EOCClass实例则通过strong实例变量保留了获取器,最后,获取器对象又保留了handler块。

如下图所示:

要打破保留环也很容易:要么令_networkFetcher实例变量不再引用获取器,要么令获取器的completionHaiidler属性不再持有handler块。在网络数据获取器这个例子中,应该等 completion handler块执行完毕后,再去打破保留环,以便使获取器对象在handler块执行期间保持存活状态。比方说,completion handler块的代码可以这么修改:

[_networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog (@"Request URL %@ finished", _networkFetcher.url);
        _fetchedData = data;
        _networkFetcher = nil;
    }];

如果设计API时用到了completion handler这样的同调块,那么很容易形成保留环,所以必须意识到这个重要问题。一般只要适时的清理环中的某个引用即可。

但是上例如果不执行completion handler,那么保留环就无法打破,于是内存就会泄漏。

所以我们使调用API的那段代码无须在执行期间保留指向网络数据获取器的引用,而是设定一套机制,令获取器对象自己设法保持存活。要想保持存活,获取器对象可以 在启动任务时把自己加到全局的collection中(比如用set来实现这个collection),待任务完成后,再移除。

所以我们改写一下:

-(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);
        _fetchedData = data;
    }];
}

大部分网络通信库都采用这种办法,因为假如令调用者自己来将获取器对象保持存活的话,他们会觉得麻烦。Twitter框架的TWRequest对象也用这个办法。

但是上面这个例子仍然有保留环,completion handler块会通过获取器对象来引用其中的URL,之后获取器会反过来经由CompletionHandler属性保留这个块。我们把块保留在属性里的目的是想稍后来使用这个块。所以一旦我们运行过completion handler之后就可以将它释放了。我们消除保留环可以按照下面修改:

-(void)p_requestCompleted { 
    if (_completionHandler){ 
        _completionHandler(_downloadedData);
    }
    self.completionHandler = nil;
}

这样只要下载请求执行完毕,保留环就解除了。

请注意,之所以要在start方法中把completion handler作为参数传进去,这也是一条重要原因。假如把completion handler暴露为获取器对象的公共属性,那么就不便在执行完下载请求之后直接将其淸理掉了,因为既然已经把handler作为属性公布了,那就意味着调用者可以自由使用它,若是此时又在内部将其清理掉的话,则会破坏“封装语义” (encapsulation semantic)

这两种保留环都很容易发生。使用块来编程时,一不小心就会出现这种bug,反过来说,只要小心谨慎,这种问题也很容易解决。关键在于,要想清楚块可能会捕获并保留哪些对象。如果这些对象又直接或间接保留了块,那么就要考虑怎样在适当的时机解除保留环。

要点

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
2017/9/5 posted in  第六章 块与GCD

第三十九条 用handler块降低代码分散程度

iOS中我们经常采用异步执行任务的方式,来避免主线程的阻塞。因为“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。导致程序崩溃。

但是异步方法执行任务后,需要以某种手段来通知相关代码。实现这一功能有很多方法,常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。例如:

#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
-(void)networkFetcher:(EOCNetworkFetcher*)networkFetcher 
    didFinishWithData:(NSData*)data;
@end

@interface EOCNetworkFetcher : NSObject 
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate; 
-(id)initWithURL:(NSURL*)url;
-(void)start;
@end

其它类可以像下面这样来使用:

-(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*)networkFetcher didFinishWithData:(NSData*)data
{
    _fetchedFooData = data;
}

上面在EOCNetworkFetcher类中声明了一个协议,协议中有一个方法用于通知对象已获取完数据。

之后想要获取到通知的对象遵守该协议,成为它的委托对象。这样在执行完start方法之后,EOCNetworkFetcher会调用委托对象所遵守的协议方法,让委托对象获取收到的数据(也就是通知它)。

上面这种做法没有错误,确实可行。但是如果我们改用块来写的话,代码会更清晰。就是把completion handler定义为块类型,将其当作参数直接传给start方法:

#import <Foundation/Foundation.h>

typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject 
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
            (EOCNetworkFetcherCompletionHandler)handler;
@end

这和使用委托协议很想,不过多了个好处,就是可以在调用start方法时直接以内联形式 定义completion handler,以此方式来使用“网络数据获取器”(network fetcher),可以令代码比原先易懂很多。例如,下面这个类就以块的形式来定义completion handler,并以此为参数调用API:

-(void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com/foo.dat"]; 
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){ 
    _fetchedFooData = data;
});

与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。

这种写法其实最重要的用途是处理错误。现在很多基于块的API都使用块来处理错误,可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里,我们建议使用后者,因为苹果公司也是这样设计API的。我们举例来说:

#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler) 
                                (NSData *data, NSError *error);

@interface EOCNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
                (EOCNetworkFetcherCompletionHandler)completion;
@end

此种API的调用方式如下:

EOCNetworkFetcher *fetcher =
        [[EOCNetworkFetcher alloc] initWithURL:url]; 
[fetcher startWithCompletionHander:
    ^(NSData *data, NSError *error){ 
    if (error) {
        //Handle failure 
    }else {
        // Handle success
    }
}];

要点

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切 换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
2017/9/4 posted in  第六章 块与GCD

第三十八条 为常用的块类型创建typedef

我们在定义一个块时,语法是这样的:

int^(variableName)(BOOL flag,int value) = ^(BOOL flag, int value){
    //Implemention
    return someInt;
}

此块接受两个类型分别为BOOLint的参数,并返回类型为int的值。并且把它赋给了一个变量。

与其他类型的变量不同,在定义块变量时,要把变量名放在类型之中,而不要放在右侧。这种语法非常难记,也非常难读。鉴于此,我们应该为常用的块类型起个别名,尤其是打算把代码发布成API供他人使用时,更应这样做。开发者可以起个更为易读的名字来表示块的用途,而把块的类型隐藏在其后面。例如:

//定义
typedef int(^EOCSomeBlock)(BOOL flag, int value);

声明变量时,要把名称放在类型中间,并在前面加上“^”符号,而定义新类型时也得这么做。上面这条语句向系统中新增了一个名为EOCSomeBlock的类型。此后,不用再以复杂的块类型来创建变量了,直接使用新类型即可:

EOCSomeBlock block = ^(BOOL flag, int value){
    // Implementation
};

这次代码读起来就顺畅多了:与定义其他变量时一样,变量类型在左边,变量名在右边。

我们可以利用这个将使用块的API做的简单易用些,例如:

-(void)startWithCompletionHandler:
            (void(^)(NSData *data, NSError *error))completion;

上面代码接受了一个块作为参数,所以我们可以用上面定义块的方法来改写它:

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);

-(void)startWithCompletionHandler: (EOCCompletionHandler)completion;

现在参数看上去就简单多了,而且易于理解。

我们在定义块的时候要注意,最好在使用块类型的类中定义这些typedef,而且还应该把这个类的名字加在由typedef所定义的新类型名前面,这样可以阐明块的用途。还可以用typedef给同一个块签名类型创建数个别名。在这件事上,多多益善。因为,开发者看到类型的别名以及签名中的参数之后,可以很容易的理解类型的用途。

与此相似,如果有好几个类都要执行相似但各有区别的异步任务,而这几个类又不 能放人同一个继承体系,那么,每个类就应该有自己的completion handler类型。这几个completion handler的签名也许完全相同,但最好还是在每个类里都各自定义一个别名,而不 要共用同一个名称。反之,若这些类能纳人同一个继承中,则应该将类型定义语句放在超类中,以供各子类使用。

要点

  • typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef
2017/9/4 posted in  第六章 块与GCD