注1:本文翻译自BrokenBoat的CommunicationPatterns。每个应用程序或多或少都是由一些松散耦合的对象组成的,这些对象要想很好地完成任务就需要相互传递消息。本文将介绍所有可用的消息传递机制,并通过例子来介绍这些机制在Apple的Framework中是如何使用的。同时,它也介绍了一些最佳实践建议,告诉你什么时候选择使用哪种机制。虽然本期的主题是关于FoundationFramework,但本文也介绍了一些超出FoundationFramework范围的消息传递机制(KVO和Notification),同时也介绍了delegation、block和target-action。在大多数情况下,很清楚消息传递使用什么机制。当然,有些情况下使用什么机制并没有明确的答案,需要你自己去尝试。在这篇文章中,经常会提到接收者[recipient]和发送者[sender]。在消息传递机制中是什么意思,我们可以用一个例子来解释:一个表视图是发送者,它的委托是接收者。CoreData托管对象上下文是通知的发送者,而这些通知的主题是接收者。一个滑块(slider)是动作消息的发送者,代码中动作对应的响应者是接收者。对象中的某个属性支持KVO,所以谁修改了这个值就是发送者,对应的观察者(observer)就是接收者。可用机制首先让我们看一下每种机制的具体特征。在下一节中,我将结合一个流程图来介绍在特定情况下如何选择正确的消息传递机制。***,会介绍一些来自AppleFramework的例子,会解释为什么在某些情况下应该选择固定的机制。KVOKVO提供了这样一种机制:当对象中的某个属性值发生变化时,可以通知这些值的观察者。Foundation中包含了KVO的实现,很多基于Foundation构建的Framework都依赖于KVO。想了解更多KVO的使用方法,可以阅读本期大牛写的KVO和KVC文章。如果你对一个对象中值的变化感兴趣,你可以使用KVO消息传递机制。这里有两个要求,第一,接收者(将收到值已更改的消息的人)必须知道发送者(其值将发生变化的对象)。另外,接收者还需要知道发送者的生命周期,因为在发送者对象销毁之前需要注销观察者的注册。如果满足这两个要求,消息传递过程可以是一对多的(多个观察者可以在一个对象中注册一个值)。如果你打算在CoreData对象上使用KVO,你需要知道这与一般的KVO用法有点不同。即必须结合CoreData的faulting机制。一旦核心数据发生故障,就会触发其属性对应的观察者(即使这些属性的值没有发生变化)。Notification是一种非常好的在两个不相关的代码部分传递消息的机制,可以广播消息。尤其是想传达丰富的信息,并不一定指望有人关心这个消息。通知可用于发送任意消息,甚至包含userInfo字典或NSNotifacation的子类。通知的独特之处在于发送者和接收者不需要彼此认识。这允许在非常松散耦合的模块之间传递消息。请记住,这种消息传递机制是单向的,接收者无法回复消息。委托在Apple的Framework中,委托模式被广泛使用。委托允许我们自定义对象的行为并接收某些特定事件。为了使用委托模式,消息的发送者需要知道消息的接收者(委托),反之则不行。这里的发送方和接收方是比较松耦合的,因为发送方只知道它的委托遵循特定的协议。委托协议可以定义任意方法,因此您可以准确定义您需要的类型。可以以函数参数的形式处理消息内容,委托也可以以返回值的形式响应发送者。如果只需要在距离比较近的两个模块之间传递消息,那么Delegation是一种非常灵活直接的方式。但是,过渡性使用委托也存在一定的风险。如果两个对象紧耦合,不能相互独立存在,那么此时就没有必要使用委托协议。在这种情况下,对象可以知道相互类型,然后直接进行消息传递。示例包括UICollectionViewLayout和NSURLSessionConfiguration。blockBlock是比较新的技术,最早出现在OSX10.6和iOS4。一般来说,block可以满足delegation实现的消息传递机制。然而,这两种机制都有其自身的需求和优势。在不考虑block的使用的情况下,一般认为block很容易造成retainloops。如果发件人需要retain块,但不能确保引用何时为nil,则会发生潜在的retaincycle。假设我们要实现一个tableview,使用block代替delegate作为selection的回调,如下:self.myTableView.selectionHandler=^void(NSIndexPath*selectedIndexPath){//handleselection...};上面代码的问题是self保留了tableview,而tableview保留了block是为了后面能够使用block。并且表视图不能nil这个引用,因为它不知道什么时候不再需要这个块。如果我们不能保证保留环可以被打破,而我们又需要保留发送者,此时block并不是一个好的选择。NSOperation可以很好的使用block,因为它可以在某个时候打破retainring:self.queue=[[NSOperationQueuealloc]init];MyOperation*operation=[[MyOperationalloc]init];operation.completionBlock=^{[selffinishedOperation];};[self.queueaddOperation:操作];乍一看这好像是一个保留环:self保留queue,queue保留operation,operation保留completionblock,completionblock保留self。但是,这里,当操作加入队列后,操作会在某个时间执行,然后从队列中移除(如果不执行,就会有大问题)。单个队列移除操作后,retainring被破坏。另一个例子:这是一个视频编码器类,它有一个名为encodeWithCompletionHandler:的方法。为了避免保留循环,我们需要确保编码器对象可以在特定时间将其对块的引用置零。其内部代码如下:@interfaceEncoder()@property(nonatomic,copy)void(^completionHandler)();@end@implementationEncoder-(void)encodeWithCompletionHandler:(void(^)())handler{self.completionHandler=处理程序;//dotheasynchronousprocessing...}//Thisonewillbecalledoncethejobisdone-(void)finishedEncoding{self.completionHandler();self.completionHandler=nil;//<-Don'tforgetthis!}@end在上面的代码中,一旦job完成后完成,将调用编译块,并且引用将为零。如果我们发送的消息是一次性的(特定于某个方法调用),因为这可以打破潜在的保留环,那么使用块是一个非常好的选择。另外,如果你想让代码更具可读性和连贯性,最好使用块。根据这个思路,block往往可以用于completionhandler、errorhandler等。Target-ActionTarget-Action主要用在响应用户界面事件需要传递的消息中。iOS中的UIControl和Mac中的NSControl/NSCell都支持这种机制。Target-Action在消息的发送者和接收者之间建立了一个非常松散的耦合。消息的接收者不知道发送者,甚至消息的发送者也不需要事先知道消息的接收者。如果target为nil,则action将在响应者链中传递,直到找到可以响应aciton的对象。在iOS中,每个控件都可以与多个目标操作相关联。基于目标-动作消息传递的机制的一个限制是发送的消息不能携带自定义负载。在Mac的action方法中,receiver总是放在第一个参数中。在iOS中,您可以选择使用发送者和触发操作的事件作为参数。此外,没有其他方法可以控制发送的动作消息的内容。做出正确的选择基于上面讨论的结果,我在这里画了一个流程图来帮助我们更好地决定何时使用什么消息传递机制。警告:流程图中的建议不是最终答案;可能还有其他选择仍然可以实现目标。只是在大多数情况下这张图可以指导你做出正确的决定。上图中还有一些细节需要进一步说明:上图中有一个方框写着:senderisKVOcompliant(发送者支持兼容)。这不仅意味着发送者在值更改时发送KVO通知,而且观察者还需要知道发送者的生命周期。如果发送者存储在弱属性中,则发送者可能为nilout,导致观察者泄漏。另外,最下面有个框说:messageisdirectresponsetomethodcall(消息直接在方法的调用代码中响应)。也就是说,处理消息的代码和方法的调用代码在同一个地方。***,左下角,处于决策问题的判断状态:sender能保证niloutreferencetoblock?(发送方能保证nil落到blockreference吗?),这其实涉及到之前讨论的Block-basedAPIshavepotentialretainrings。在使用块时,如果发送方不能保证在一定时间内可以将块的引用置为nil,那么就会遇到retainring的问题。FrameworkExamples在本节中,我们使用Apple的Framework中的一些示例来了解为什么Apple在实际使用某种机制之前做出选择。KVONSOperationQueue是KVO的狮子,用于观察队列中操作状态属性的变化(isFinished,isExecuting,isCancelled)。当状态改变时,队列会收到一个KVO通知。operationqueue为什么要使用KVO?消息的接收者(操作队列)清楚地知道发送者(操作),通过retain控制操作的生命周期。此外,在这种情况下,只需要单向消息传递。当然,如果你这样想:如果操作队列只关心操作值的变化,可能不足以说服大家使用KVO。但我们至少可以这样理解:什么机制可以消息值变化。当然,KVO不是唯一的选择。我们可以这样设计:操作队列是操作的委托,操作会调用operationDidFinish:或operationDidBeginExecuting:等方法将其状态传递给队列。这种方式不是很方便,因为操作需要保存它的state属性,然后再调用这些delegate方法。另外,由于队列不能主动获取状态信息,所以队列还必须保存所有操作的状态。NotificationsCoreData使用通知来传递事件(例如托管对象上下文内部更改-NSManagedObjectContextDidChangeNotification)。更改通知是由托管对象上下文发送的,因此我们不能确定消息的接收者一定知道发送者。如果消息不是UI事件,但多个接收者可能对消息感兴趣,并且消息的传递是一种单向通信渠道,那么通知是最佳选择。DelegationTable视图的delegate有多种功能,从accessoryview的管理到屏幕上cell显示的跟踪,都归功于delegate。例如,让我们看一下tableView:didSelectRowAtIndexPath:方法。为什么要通过调用delegate来实现呢?为什么不使用目标行动方法呢?正如我们在流程图中看到的,当使用目标动作时,您不能传递自定义数据。当表格视图的一个单元格被选中时,集合视图不仅需要告诉我们一个单元格被选中了,还需要告诉我们选中了哪个单元格(索引路径)。按照这样的思路,从流程图可以看出应该使用委托机制。如果所选单元的索引路径不包含在消息传递中,但是每当所选项目更改时,我们会积极转到表视图以获取所选单元的相关信息,该怎么办?其实这样会很麻烦,因为这样一来,我们就必须记住与当前选中项相关的数据,才能知道选中的单元格。同理,虽然我们也可以在表视图中观察选中项的indexpaths属性值,但是当该值发生变化时,我们可以获得选中项变化的通知。但是,我们会遇到和上面一样的问题:如何在不做任何记录的情况下获取选中项的信息。关于block的介绍,我们看一下[NSURLSessiondataTaskWithURL:completionHandler:]。从URL加载系统返回到调用者,这个过程是如何传递消息的?首先,作为这个API的调用者,我们知道消息的发送者,但是我们不保留发送者。此外,这是一种单向消息传递——直接调用dataTaskWithURL:方法。如果按照这种思路按照流程图走,就会发现应该使用基于块消息传递的机制。还有其他替代机制吗?当然,Apple自己的NSURLConnection就是最好的例子。NSURLConnection在block出现之前就已经存在,所以它不使用block进行消息传递,而是使用委托机制。当出现block时,Apple在NSURLConnection中添加了sendAsynchronousRequest:queue:completionHandler:方法(OSX10.7iOS5),因此如果是简单的任务,则不需要再使用委托。在OSX10.9和iOS7中,Apple引入了一个非常现代的API:NSURLSession,它使用块作为消息传递机制(NSURLSession仍然有一个委托,但它用于其他目的)。Target-ActionTarget-Action最明显的地方之一就是按钮(button)。按钮除了发送点击事件外不需要发送其他信息。所以Target-Action是用户界面事件传递过程中的最佳选择。如果已明确指定taget,则将操作消息直接发送到指定的对象。如果taget为nil,则动作消息将在响应链中向上冒泡,以查找可以处理该消息的对象。在这一点上,我们有一个完全解耦的消息传递机制——发送者不需要知道接收者,等等。Target-Action非常适合用户界面中的事件。目前没有其他合适的消息传递机制可以提供相同的功能。虽然通知最接近发送者和接收者的这种解耦,但可以在响应者链中使用目标-动作——只有一个对象获得动作并做出响应,动作可以沿着响应者链向下传递,直到一个对象能够响应遇到动作。小结接触了这些机制后,感觉可以用来做两个对象之间的消息传递。但仔细想想,你会发现它们各有各的需求和作用。文中给出的决策流程图可以为我们选择使用哪种机制提供参考,但图中给出的方案并不是最终答案,很多地方还需要自己去实践。
