每个Application或多或少都有一些松耦合的对象(模块)组成,他们必须彼此通讯来完成工作。这篇文章将会通过可用的通讯机制,并以Apple的Framework来举例,并给出最佳的实践建议关于使用哪种通讯机制。
虽然这个问题是关于Foundation框架的,但是我们可以通过Foundation的通讯机制,差不多有这几个通讯方法 - KVO,Notification,delegate,block,target-action。
当然,没有人定义了在哪种情况就必须用哪种模式,选择往往都是看个人口味。但是也有很多情况可以很明确的知道使用哪种通讯机制。
在这篇文章中,我们经常使用‘recipient’(接收者)和‘sender’(发送者),举个例子:tableView是一个sender,它的代理是recipient。Core data managed对象是sender,他通过发通知然后接收到的对象是recipient。Slider是sender,它的action Implement是recipient,一个对象如果用KVO监听了一个属性,那么属性的改变是一个sender,observe是recipient。明白了吗?
首先我们看一下每个通讯模式他们的不同点。在此基础上,我们通过流程图的方式帮助你选择使用哪种模式。最后,我们通过苹果的几个framework来举几个例子。
KVO是通知对象属性改变(property change)的一种机制,Foundation和其他很多苹果的框架的实现都依靠它。如果你想知道更多关于KVO的使用例子,请点此链接。
如果你对某个对象的属性改变感兴趣,那么适合使用KVO。它有一些要求,首先,你的recipient(接收者)需要监听sender某个属性改变的消息,并且sender会把属性改变后的值告诉你。此外,recipient同样需要知道sender的生命周期,因为在sender调用dealloc之前需要注销监听。如果上面条件满足了,那么适合使用KVO。KVO甚至可以一对多,可以同时设置多个监听者。
如果你想在CoreData对象使用KVO,会有一点点的不同,这和它的faulting机制相关。当一个CoreData manager对象进入了faulting,他也会通知观察者尽管他们的属性没改变。
Notification是一种非常好的工具,在两个不相关的对象之间广播消息,尤其是有附带信息的时候,你也不需要做其他任何事。
Notifications被用来发送任何消息,在使用NSNotification这个子类的时候,还能附带自定义的参数在userInfo这个Dictionary里面。notifications是唯一一种通讯机制,它们使sender和recipient不需要知道彼此。也更加的松耦合。因此,这种机制是单向的-你不能去回复一个通知(对sender做某些事情)。
delegate模式在苹果的框架中使用广泛,它允许定制一个对象的行为并且通知它某个时候去执行。在delegate中,sender需要知道recipient,但和常规的方式不同,它更加的松耦合,因为sender仅仅知道他的delegate遵守了某个协议。
由于在protocol中能定义任何方法,所以你能精确的建立你想要的通讯模式,你能在参数里面传递自定义的数据,并且能对返回值做出响应。delegate是一种灵活和简单的方式,如果你仅仅需要在两个对象之间做出通讯,这是一种用在两个对象之间的关系相对比较亲近的时候。
过度使用delegate模式也是很危险的,如果两个对象是紧耦合的关系,一个不能没有另一个,那就不适合使用delegate。这种情况下,一个对象应该直接设置为另一个对象的属性。比如:UICollectionViewLayout和NSURLSessionConfiguration。
block是在OS X 10.6和iOS 4的时候才加进去的。Blocks替代了很多之前用delegate模式实现的地方。但是,这两种模式都有它自己的优缺点。
如果你使用了block。如果你的sender需要引用block,然而不能保证这个引用置为nil的话,并且在bloc里面使用了self这个变量的话,这里将会产生循环引用。假设我们不用delegate,而用block实现一个tableVeiw,像这样:
self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) {
// handle selection ...
};
这里出现的问题是self引用了tableview,tableView应用了block为了在将来之后某个时刻使用它。tableView不知道在什么时候把他设为nil。如果我们不能保证循环引用在某个时候会打破,block就不适合在这种情况使用。
使用NSOperation就没有这个问题。因为他将在某个时候打破循环引用。
self.queue = [[NSOperationQueue alloc] init];
MyOperation *operation = [[MyOperation alloc] init];
operation.completionBlock = ^{
[self finishedOperation];
};
[self.queue addOperation:operation];
咋一看,这里似乎会产生循环引用,self引用了queue,queue引用了operation,operation引用了complete block,complete block引用了self。但是,在operation被添加到queue的时候,operation就会执行,在执行完毕的时候,queue会把这个operation移除,这就打破了这个循环引用。
另一个例子:假设我们去实现一个视频编码类(video encoder class),我们调用了encodeWithCompletionHandler:方法。为了使调用无问题,我们必须保证编码对象在某个时刻把block引用置为nil。像这样:
@interface Encoder ()
@property (nonatomic, copy) void (^completionHandler)();
@end
@implementation Encoder
- (void)encodeWithCompletionHandler:(void (^)())handler
{
self.completionHandler = handler;
// do the asynchronous processing...
}
// This one will be called once the job is done
- (void)finishedEncoding
{
self.completionHandler();
self.completionHandler = nil; // <- Don't forget this!
}
@end
一旦我们调用了complete block之后,我们立即把他置为nil。
block非常适合去做只有单一回调的事情,因为我们知道在这种情况下,什么时候该去打破这个引用循环。此外,block的可读性比较好,因为方法调用和处理是在一起的。沿着这条线,在completeHandler和errorHandler这种情况下适合使用block。
Target-Action是用于响应用户UI操作的一种模式。支持这种模式在iOS上是UIControl,Mac上是NSControl、NSCell。Target-Action在sender和recipient之间建立了一种松耦合的关系。recipient不需要知道sender,sender也不需要知道recipient。在target为nil的情况下,action就会进入事件响应链,知道找到能响应它的对象。在iOS中,一个control能关联多个target-action。
使用target-action模式的限制是你不能传递自定义的参数。在Mac上总是以sender作为他的第一个参数。iOS中你可以选择sender和event模式作为action的参数。但是,没有办法传递其他对象。
基于上面提到的每个模式不同的特点,我们画了一个流程图,助于你选择在什么时候使用哪种模式。但也仅仅是个建议,并不是最终的答案。你可能也其他的选择一样能实现的非常好。但是在工作中,大多数情况下能引导你做出正确的选择。
图片中有一些细节需要解释一下:
图片中有一句‘sender is KVO compliant’,不仅仅意味着他能在属性改变的时候发送消息,而且观察者也必须知道sender的生命周期。如果sender是以弱引用形式被保存的,那么它将在任何时候都可能被释放,这将会导致内存泄露。
还有一句话是‘message is direct response to method call’,这句话的意思是方法调用的接收者必须回调调用者的方法作为它的一个直接响应。你也可以理解为你的调用代码和处理代码出现在同一个地方是有道理的。
最后,你也要考虑sender是否能保证在某个时刻把block设为nil,如果不能,将可能会产生循环引用。
接下来,我们通过一些苹果框架的例子来说明上述的流程图是有一定道理的,苹果为什么选择使用这种模式。
NSOperationQueue内部使用KVO来监听各个operation的状态改变(isFinished, isExecuting, isCancelled)。当状态发生改变的时候,queue会收到一个KVO通知。为什么NSOperationQueue使用KVO呢?
因为recipient(NSOperationQueue)清楚的知道sender(NSOperation)并且控制着它们的生命周期。进一步说,这种使用情况仅仅要求单向的通讯。如果NSOperationQueue仅仅是对NSOperation的属性值改变感兴趣,那其实也没必要一定使用KVO。但是我们至少可以说,值改变被模型化成了状态改变。由于NSOperationQueue无时无刻需要NSOperation的state属性的最新的状态。在这种情况下使用KVO是合乎逻辑的。
KVO并不是唯一的选择。我们可以想象一下NSOperationQueue成为NSOperation的代理,然后NSOperation内部就会调用类似于operationDidFinish:或operationDidBeginExecuting:语句通知NSOperationQueue状态的改变。这就很不方便了,为了保持NSOperation的状态最新就必须加入这些代码。
CoreData在managed对象的context发生改变的时候使用notification机制来通讯的(NSManagedObjectContextObjectsDidChangeNotification)。
这个通知是managed对象的contexts发送的,因此我们不能让消息的接收者知道发送者是谁。由于消息的起源显然不是一个UI事件,多个接收者可能对此感兴趣,而且他们必须是一个单向的通道,那么使用notification时唯一的选择。
tableView的delegate能实现多种功能,从管理编辑状态的views到追踪屏幕上的cells。举个例子,我们知道 tableView:didSelectRowAtIndexPath: 这个方法,为什么我们用delegate去实现?为什么不用target-action模式去实现?
在上面我们已经用图表的方式提到过,target-action仅仅用在我们不需要传参的情况下。如果一个选中事件发生了,那么collectionView不仅仅告诉我们发生了一个cell选中事件,还要告诉我们被选中的cell的indexPath。如果我们要传递indexpath参数,那么我们沿着图片就能找到使用delegate模式。
如果我们不向选择的方法里面传送indexPath参数,相反的通过访问tableView的方式去获取那个cell被选中了,这就很不方便,就多重选项的功能来说,到时候我们就必须去管理哪些cells被选中了为了告诉哪个cell是最近一次选中的。
类似的,我们可以想象一下使用通知和KVO的方式实现。
不管怎样,都会遇到上面说的问题。除了我们自己去管理,我们是区分不了哪个cell是最近一次被选中/取消选中的。
基于block调用我们以-[NSURLSession dataTaskWithURL:completionHandler:]为例,当调用方加载一个url的时候它是以什么方式回调的?首先,作为这个api的调用方,我们知道当发送一个消息的时候,但是我们不行引用它。或者说,这是一个单向的通信直接耦合dataTaskWithURL:方法调用。如果我们对照上面的图片,就会找到使用block的方式。
有其他的选择吗?当然,苹果自己的NSURLConnection就是一个很好的例子。NSURLConnection在Block出现前就已经存在了,所以他们使用delegate模式去实现。一旦block出现后,苹果在OS X 10.7 and iOS 5上又为NSURLConnectionin增加了methodsendAsynchronousRequest:queue:completionHandler:方法来实现简单的任务。
由于NSURLSession的API在OS X 10.9 and iOS 7之后才出现,block就被用来作为这种类型的通讯方式(NSURLSession仍然有delegate,但是用在其他方面)。
使用Target-Action模式最明显的例子是buttons,按钮除了他们被点击之外不需要传递其他任何的信息。因此,Target-Action模式是最适合处理用户界面事件的。
如果指定了target,那么action消息会直接发送到Target这个对象上。但是,如果target为nil,action就会进入事件响应链来寻找哪个对象能够处理它。当发送者不需要知道接受者的情况下,我们就有一种彻底解耦的通讯机制。
target-action这种模式用在用户界面事件中比较完美。没有其他的通讯模式可以提供相同的功能。Notifications也能对发送者和接收者完全解耦,但是它没有 target-action的事件响应链机制。
在通讯机制中有这么多可用的通讯模式值得我们高兴一下。但是,在使用中具体选择哪种模式往往很模糊。一旦我们仔细的研究每一种模式,会发现他们都有各自的特点。