消息传递机制


消息传递机制 卢思豪 (https://github.com/DJBen) 2014/04/26 每个应⽤或多或少都由⼀些需要相互传递消息的对象结合起来以完成任务。在这篇⽂章⾥,我们将介绍所有可 ⽤的消息传递机制,并通过例⼦来介绍怎样在苹果的框架⾥使⽤。我们还会选择⼀些最佳范例来介绍什么时候 该⽤什么机制。 虽然这⼀期的主题是关于 Foundation 框架的,但是我们会超出 Foundation 的消息传递机制 (KVO 和 通知) 来 讲⼀讲 delegation,block 和 target-action ⼏种机制。 当然,有些情况下该使⽤什么机制没有唯⼀的答案,所以应该按照⾃⼰的喜好去试试。另外⼤多数情况下该使 ⽤什么机制应该是很清楚的。 本⽂中,我们会常常提及“接收者”和“发送者”。它们在消息传递中的意思可以通过以下的例⼦解释:⼀个 table view 是发送者,它的 delegate 就是接收者。Core Data managed object context 是它所发出的 notification 的 发送者,获取 notification 的就是接收者。⼀个滑块 (slider) 是 action 消息的发送者,⽽实现这个 action (⽅ 法)的是它的接收者。任何修改⼀个⽀持 KVO 的对象的对象是发送者,这个 KVO 对象的观察者就是接收者。 明⽩精髓了吗? ⼏种消息传递机制 ⾸先我们来看看每种机制的具体特点。在这个基础上,下⼀节我们会画⼀个流程图来帮我们在具体情况下正确 选择应该使⽤的机制。最后,我们会介绍⼀些苹果框架⾥的例⼦并且解释为什么在那些⽤例中会选择这样的机 制。 KVO KVO 是提供对象属性被改变时的通知的机制。KVO 的实现在 Foundation 中,很多基于 Foundation 的框架都 依赖它。想要了解更多有关 KVO 的最佳实践,请阅读本期 Daniel 写的 KVO 和 KVC ⽂章 (http://objccn.io/issue-7-3)。 如果只对某个对象的值的改变感兴趣的话,就可以使⽤ KVO 消息传递。不过有⼀些前提:第⼀,接收者(接 收对象改变的通知的对象)需要知道发送者 (值会改变的对象);第⼆,接收者需要知道发送者的⽣命周 期,因为它需要在发送者被销毁前注销观察者身份。如果这两个要去符合的话,这个消息传递机制可以⼀对多 (多个观察者可以注册观察同⼀个对象的变化) 如果要在 Core Data 上使⽤ KVO 的话,⽅法会有些许差别。这和 Core Data 的惰性加载 (faulting) 机制有关。 ⼀旦⼀个 managed object 被惰性加载处理的话,即使它的属性没有被改变,它还是会触发相应的观察者。 编者注 把属性值先取⼊缓存中,在对象需要的时候再进⾏⼀次访问,这在 Core Data 中是默认⾏为, 这种技术称为 Faulting。这么做可以避免降低内存开销,但是如果你确定将访问结果对象的具体属性值 时,可以禁⽤ Faults 以提⾼获取性能。关于这个技术更多的情况,请移步官⽅⽂档 (https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdFaultingUniquing.html) 通知 要在代码中的两个不相关的模块中传递消息时,通知机制是⾮常好的⼯具。通知机制⼴播消息,当消息内容丰 富⽽且⽆需指望接收者⼀定要关注的话这⼀招特别有⽤。 通知可以⽤来发送任意消息,甚⾄可以包含⼀个 userInfo 字典。你也可以继承 NSNotification 写⼀个⾃ ⼰的通知类来⾃定义⾏为。通知的独特之处在于,发送者和接收者不需要相互知道对⽅,所以通知可以被⽤来 在不同的相隔很远的模块之间传递消息。这就意味着这种消息传递是单向的,我们不能回复⼀个通知。 委托 (Delegation) 返回期刊列表 (/issues/)   Delegation 在苹果的框架中⼴泛存在。它让我们能⾃定义对象的⾏为,并收到⼀些触发的事件。要使⽤ delegation 模式的话,发送者需要知道接收者,但是反过来没有要求。因为发送者只需要知道接收者符合⼀定 的协议,所以它们两者结合的很松。 因为 delegate 协议可以定义任何的⽅法,我们可以照着⾃⼰的需求来传递消息。可以⽤⽅法参数来传递消息 内容,delegate 可以通过返回值的形式来给发送者作出回应。如果只要在相对接近的两个模块间传递消息, delgation 是很灵活很直接的消息传递机制。 过度使⽤ delegation 也会带来⻛险。如果两个对象结合得很紧密,任何其中⼀个对象都不能单独运转,那么就 不需要⽤ delegate 协议了。这些情况下,对象已经知道各⾃的类型,可以直接交流。两个⽐较新的例⼦是 UICollectionViewLayout 和 NSURLSessionConfiguration 。 Block Block 是最近才加⼊ Objective-C 的,⾸次出现在 OS X 10.6 和 iOS 4 平台上。Block 通常可以完全替代 delegation 消息传递机制的⻆⾊。不过这两种机制都有它们⾃⼰的独特需求和优势。 ⼀个不使⽤ block 的理由通常是 block 会存在导致 retain 环 (retain cycles (https://developer.apple.com/library/mac/documentation/cocoa/conceptual/memorymgmt/Articles/mmPractical.html#//apple_ref/doc/uid/TP40004447- 1000810)) 的⻛险。如果发送者需要 retain block 但⼜不能确保引⽤在什么时候被赋值为 nil , 那么所有在 block 内对 self 的引⽤就会发⽣潜在的 retain 环。 假设我们要实现⼀个⽤ block 回调⽽不是 delegate 机制的 table view ⾥的选择⽅法,如下所示: self.myTableView.selectionHandler = ^void(NSIndexPath *selectedIndexPath) { // 处理选择 }; 这⼉的问题是, self 会 retain table view,table view 为了让 block 之后可以使⽤⽽⼜需要 retain 这个 block。然⽽ table view 不能把这个引⽤设为 nil,因为它不知道什么时候不需要这个 block 了。如果我们不能 保证打破 retain 环并且我们需要 retain 发送者,那么 block 就不是⼀个的好选择。 NSOperation 是使⽤ block 的⼀个好范例。因为它在⼀定的地⽅打破了 retain 环,解决了上述的问题。 self.queue = [[NSOperationQueue alloc] init]; MyOperation *operation = [[MyOperation alloc] init]; operation.completionBlock = ^{ [self finishedOperation]; }; [self.queue addOperation:operation]; ⼀眼看来好像上⾯的代码有⼀个 retain 环: self retain 了 queue,queue retain 了 operation, operation retain 了 completionBlock, ⽽ completionBlock retain 了 self 。然⽽,把 operation 加⼊ queue 中会使 operation 在某个时间被执⾏,然后被从 queue 中移除。(如果没被执⾏,问题就⼤了。)⼀旦 queue 把 operation 移除,retain 环就被打破了。 另⼀个例⼦是:我们在写⼀个视频编码器的类,在类⾥⾯我们会调⽤⼀个 encodeWithCompletionHandler: 的⽅法。为了不出问题,我们需要保证编码器对象在某个时间点会释放对 block 的引⽤。其代码如下所示: @interface Encoder () @property (nonatomic, copy) void (^completionHandler)(); @end @implementation Encoder - (void)encodeWithCompletionHandler:(void (^)())handler { self.completionHandler = handler; // 进⾏异步处理... } // 这个⽅法会在完成后被调⽤⼀次 - (void)finishedEncoding { self.completionHandler(); self.completionHandler = nil; // <- 不要忘了这个! } @end ⼀旦任务完成,completion block 调⽤过了以后,我们就应该把它设为 nil 。 如果⼀个被调⽤的⽅法需要发送⼀个⼀次性的消息作为回复,那么使⽤ block 是很好的选择, 因为这样做我 们可以打破潜在的 retain 环。另外,如果将处理的消息和对消息的调⽤放在⼀起可以增强可读性的话,我们也 很难拒绝使⽤ block 来进⾏处理。在⽤例之中,使⽤ block 来做完成的回调,错误的回调,或者类似的事情, 是很常⻅的情况。 Target-Action Target-Action 是回应 UI 事件时典型的消息传递⽅式。iOS 上的 UIControl 和 Mac 上的 NSControl / NSCell 都⽀持这个机制。Target-Action 在消息的发送者和接收者之间建⽴了⼀个松散的关系。 消息的接收者不知道发送者,甚⾄消息的发送者也不知道消息的接收者会是什么。如果 target 是 nil ,action 会在响应链 (responder chain) (https://developer.apple.com/library/ios/documentation/general/conceptual/Devpedia- CocoaApp/Responder.html) 中被传递下去,直到找到⼀个响应它的对象。在 iOS 中,每个控件甚⾄可以和多 个 target-action 关联。 基于 target-action 传递机制的⼀个局限是,发送的消息不能携带⾃定义的信息。在 Mac 平台上 action ⽅法的 第⼀个参数永远是发送者。iOS 中,可以选择性的把发送者和触发 action 的事件作为参数。除此之外就没有别 的控制 action 消息内容的⽅法了。 做出正确的选择 基于上述对不同消息传递机制的特点,我们画了⼀个流程图来帮助我们在不同情境下做出不同的选择。⼀句忠 告:流程图的建议不代表最终答案。有些时候别的选择依然能达到应有的效果。只不过⼤多数情况下这张图能 引导你做出正确的决定。 图中有些细节值得深究: 有个框中说到: 发送者⽀持 KVO。这不仅仅是说发送者会在值改变的时候发送 KVO 通知,⽽且说明观察者需 要知道发送者的⽣命周期。如果发送者被存在⼀个 weak 属性中,那么发送者有可能会⾃⼰变成 nil,那时观察 者会导致内存泄露。 ⼀个在最后⼀⾏的框⾥说,消息直接响应⽅法调⽤。也就是说⽅法调⽤的接收者需要给调⽤者⼀个消息作为⽅ 法调⽤的直接反馈。这也就是说处理消息的代码和调⽤⽅法的代码必须在同⼀个地⽅。 最后在右下⻆的地⽅,⼀个选择分⽀这样说:发送者能确保释放对 block 的引⽤吗?这涉及到了我们之前讨论 block 的 API 存在潜在的 retain 环的问题。如果发送者不能保证在某个时间点会释放对 block 的引⽤,那么你 会惹上 retain 环的麻烦。 Framework 示例 本节我们通过⼀些苹果框架⾥的例⼦来验证流程图的选择是否有道理,同时解释为什么苹果会选择⽤这些机 制。 KVO NSOperationQueue ⽤了 KVO 观察队列中的 operation 状态属性的改变情况 ( isFinished , isExecuting , isCancelled )。当状态改变的时候,队列会收到 KVO 通知。为什么 operation 队列要⽤ KVO 呢? 消息的接收者(operation 队列)知道消息的发送者(operation),并 retain 它并控制后者的⽣命周期。另 外,在这种情况下只需要单向的消息传递机制。当然如果考虑到 oepration 队列只关⼼那些改变 operation 的 值的改变情况的话,就还不⾜以说服⼤家使⽤ KVO 了。但我们可以这么理解:被传递的消息可以被当成值的 改变来处理。因为 state 属性在 operation 队列以外也是有⽤的,所以这⾥适合⽤ KVO。 当然 KVO 不是唯⼀的选择。我们也可以将 operation 队列作为 operation 的 delegate 来使⽤,operation 会调 ⽤类似 operationDidFinish: 或者 operationDidBeginExecuting: 等⽅法把它的 state 传递给 queue。 这样就不太⽅便了,因为 operation 要保存 state 属性,以便于调⽤这些 delegate ⽅法。另外,由于 queue 不能主动获取 state 信息,所以 queue 也必须保存所有 operation 的 state。 Notifications Core Data 使⽤ notification 传递事件(例如⼀个 managed object context 中的改变 ———— NSManagedObjectContextObjectsDidChangeNotification ) 发⽣改变时触发的 notification 是由 managed object contexts 发出的,所以我们不能假定消息的接收者知道 消息的发送者。因为消息的源头不是⼀个 UI 事件,很多接收者可能在关注着此消息,并且消息传递是单向 的,所以 notification 是唯⼀可⾏的选择。 Delegation Table view 的 delegate 有多重功能,它可以从管理 accessory view,直到追踪在屏幕上显示的 cell。例如我 们可以看看 tableView:didSelectRowAtIndexPath: ⽅法。为什么⽤ delegate 实现⽽不是 target-action 机 制? 正如我们在上述流程图中看到的,⽤ target-action 时,不能传递⾃定义的数据。⽽选中 table view 的某个 cell 时,collection view 不仅需要告诉我们⼀个 cell 被选中了,也要通过 index path 告诉我们哪个 cell 被选中 了。如果我们照着这个思路,流程图会引导我们使⽤ delegation 机制。 如果不在消息传递中包含选中 cell 的 index path,⽽是让选中项改变时我们像 table view 主动询问并获取选中 cell 的相关信息,会怎样呢?这会⾮常不⽅便,因为我们必须记住当前选中项的数据,这样才能在多选择中知 道哪些 cell 是被新选中的。 同理,我们可以想象通过观察 table view 选中项的 index path 属性,当该值发⽣改变的时候,获得⼀个选中 项改变的通知。不过我们会遇到上述相似问题:不做记录的话我们就不能分辨哪⼀个 cell 被选择或取消选择 了。 Block 我们⽤ -[NSURLSession dataTaskWithURL:completionHandler:] 来作为⼀个 block API 的介绍。那么从 URL 加载部分返回给调⽤者是怎么传递消息的呢?⾸先,作为 API 的调⽤者,我们知道消息的发送者,但是 我们并没有 retain 它。另外,这是个单向的消息传递————它直接调⽤ dataTaskWithURL: 的⽅法。如果 我们对照流程图,会发现这属于 block 消息传递机制。 有其他的选项吗?当然,苹果⾃⼰的 NSURLConnection 就是最好的例⼦。 NSURLConnection 在 block 问世 之前就存在了,所以它并没有⽤ block 来实现消息传递,⽽是使⽤ delegation 来完成。当 block 出现以后,苹 果就在 OS X 10.7 和 iOS 5 平台上的 NSURLConnection 中加了 sendAsynchronousRequest:queue:completionHandler: ,所以我们不再在简单的任务中使⽤ delegate 了。 因为 NSURLSession 是个最近在 OS X 10.9 和 iOS 7 才出现的 API,所以它们使⽤ block 来实现消息传递机 制( NSURLSession 有⼀个 delegate,但是是⽤于其他⽬的)。 Target-Action ⼀个明显的 target-action ⽤例是按钮。按钮在不被按下的时候不需要发送任何的信息。为了这个⽬的, target-action 是 UI 中消息传递的最佳选择。 如果 target 是明确指定的,那么 action 消息会发送给指定的对象。如果 target 是 nil , action 消息会⼀直在 响应链中被传递下去,直到找到⼀个能处理它的对象。在这种情况下,我们有⼀个完全解耦的消息传递机制: 发送者不需要知道接收者,反之亦然。 Target-action 机制⾮常适合响应 UI 的事件。没有其他的消息传递机制能够提供相同的功能。虽然 notification 在发送者和接收者的松散关系上最接近它,但是 target-action 可以⽤于响应链——只有⼀个对象获得 action 并响应,action 在响应链中传递,直到能遇到响应这个 action 的对象。 总结 ⼀开始接触这么多的消息传递机制的时候,我们可能有些⽆所适从,觉得所有的机制都可以被选⽤。不过⼀旦 我们仔细分析每个机制的时候,它们各⾃都有特殊的要求和能⼒。 ⽂中的选择流程图是帮助你清楚认识这些机制的好的开始,当然它不是所有问题的答案。如果你觉得这和你⾃ ⼰选择机制的⽅式相似或是有任何缺漏,欢迎来信指正。 原⽂ Communication Patterns (http://www.objc.io/issue-7/communication-patterns.html) 参考译⽂ iOS中消息的传递机制 - 破船之家 (http://beyondvincent.com/blog/2013/12/14/124- communication-patterns/) 译者简介 卢思豪 (https://github.com/DJBen) iOS 开发者,⽬前就读 Johns Hopkins University © 2016 OneV's Den (https://onevcat.com) & ObjC 中国 (http://objccn.io/) 本站由 @onevcat (http://onev.cat) 创建
还剩8页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

assassinmt

贡献于2017-03-31

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf