第23条:通过委托与数据源协议进行对象间通信

  第4章:协议与分类

  Objective-C 语言有一项特性叫做“协议”(protocol),它与 Jave 的 “接口”(interface)类似。Objective-C 不支持多重继承,因而我们把某个类应该实现的一系列方法定义在协议里面。协议最为常见的用途是实现委托模式,不过也有其他用法。理解并善用协议可令代码变得更易维护,因为协议这种方式能很好的描述接口。

  “分类”(category)也是 Objective-C 的一项重要语言特性。利用分类机制,我们无须继承子类即可直接为当前类添加方法,而在其他编程语言中,则需通过继承子类来实现。由于 Objective-C 运行期系统是高度动态的,所以才能支持这一特性,然而,其中隐藏者一些陷阱,因此在使用分类之前,应该先理解它。

  本条要点:(作者总结)

  • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象
  • 将委托对象应该支持的接口定为成协议,在协议中把可能需要处理的事件定义成方法
  • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦称 “数据源协议”(data source protocal)
  • 若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中


  对象之间经常需要相互通信,而通信方式有很多种。Objective-C 开发者广泛使用一种名叫“委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信,该模式的主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其 “委托对象”(delegate)。而这 “另一个对象” 则可以给其委托对回传一些信息,也可以在发生相关事件时通知委托对象。

  此模式可以将数据与业务逻辑解藕。比方说,用户界面里有个显示一系列数据所用的视图,那么,此视图只应包含显示数据所需的逻辑代码,而不应决定要显示何种数据以及数据之间如何交互等问题。视图对象的属性中,可以包含负责数据与事件处理的对象。这两种对象分别称为“数据源”(data source)与 “委托”(delegate)。

  在 Objective-C 中,一般通过 “协议” 这项语言特性来实现此模式,整个 Cocoa 系统框架都是这么做的。如果你的代码也这样做。如果你的代码也这样写,那么就能和系统框架很好地融合在一起了。

  为演示此模式,我们举个例子,假设要编写一个从网上获取数据的类。此类也许要从远程服务器的某个资源里获取数据。那个远程服务器可能过很长时间才会应答,而在获取数据的过程中阻塞应用程序则是一种非常糟糕的做法。于是,在这种情况下,我们通常会使用委托模式:获取网络数据的类含有一个“委托对象”,在获取完数据之后,它会回调这个委托对象。图演示了此概念:EOCDataModel 对象就是 EOCNetworkFetcher 的委托对象。EOCDataModel 请求 EOCNetworkFetcher “以异步方式执行一项任务”(perform a task asynchronously),而 EOCNetworkFetcher 在执行完这项任务之后,就会通知其委托对象,也就是 EOCDataModel。 

  回调委托对象的流程。请注意, “委托对象” 未必非得由 EOCDataModel 实例来担任不可,也可以由另外一个对象扮演此角色

 

  利用协议机制,很容易就能以 Objective-C 代码实现此模式。图演示的这种情况下,协议可以这样来定义:

1 @protocol EOCNetworkFetcherDelegate <NSObject>
2 
3 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
4 - (void)netWorkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
5 
6 @end

  委托协议名通常是在相关类名后面加上 Delegate 一词,整个类名采用 “驼峰法” 来写。以这种方式来命名委托协议的话,使用此代码的人很快就能理解其含义了。

  有了这个协议之后,类就可以用一个属性来存放其委托对象了。在本例中,这个类就是 EOCNetworkFetcher 类。于是,此类的接口可以写成这样:

1 @interface EOCNetworkFetcher : NSObject
2 
3 @property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
4 
5 @end

  一定要注意:这个属性需要定义成 weak,而非 strong,因为两者之间必须为 “非拥有关系”(nonowning relationship)。通常情况下,扮演 delegate 的那个对象也要持有本对象。例如在本例中,想使用 EOCNetworkFetcher 的那个对象就会持有本对象,直到用完本对像之后,才会释放。假如声明属性的时候用 strong 将本对象与委托对象之间定为 “拥有关系”,那么就会引入 “保留环”(retain cycle)。因此,本类中存放委托对象的这个属性要么定义成 weak,要么定义成 unsafe_unretained,如果需要在相关对象销毁时自动清空(autoniling),则定义为前者,若不需要自动清空,则定义为后者。图演示了本对象与委托对象之间的所有权关系。

  实现委托对象的办法是声明某个类遵从委托协议,然后把协议中想要实现的那些方法在类里实现出来。某类若要遵从委托协议,可以在其接口中声明,也可以在 “class-continuation 分类” 中声明。如果要向外界公布此类实现了某协议,那么就在接口中声明,而如果这个协议是个委托协议的话,那么通常只会在类的内部使用。所以说,这种情况一般都是在 “class-continuation 分类” 里声明的:

 1 @interface EOCDataModel () <EOCNetworkFetcherDelegate>
 2 
 3 @end
 4 
 5 @implementation EOCDataModel
 6 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data {
 7     /*Handle data*/
 8 }
 9 
10 - (void)netWorkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error {
11     /*Hnadle error*/
12 }
13 
14 @end

  委托协议中的方法一般都是 “可选的”(optional),因为扮演 “受委托者” 角色的这个对象未必关心其中的所有方法。在本例中,DataModel 类可能并不关心获取数据的过程中是否有错误发生,所以此类也续不会实现 “networkFetcher:didFailWithError:” 方法。为了指明可选方法,委托经常使用 @optional 关键字来标注其大部分或全部的方法:

1 @protocol EOCNetworkFetcherDelegate <NSObject>
2 @optional
3 
4 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
5 - (void)netWorkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
6 
7 @end

  如果要在委托对象上调用可选方法,那么必须提前使用类型信息查询方法判断这个委托对象能否响应相关选择子。以 EOCNetworkFetcher 为例,应该这样写:

2     NSData *data = /* data obtained from network */;
3     if ([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]) {
4         [_delegate networkFetcher:self didReceiveData:data];
5     }

  这段代码用 “respondsToSelector:” 来判断委托对象是否实现了相关方法。如果实现了,就调用。如果没实现,就不执行任何操作。这样的话,delegate 对象就可以完全按照其需要来实现委托协议中的方法了,不用担心因为哪个方法没实现而导致程序出问题。即便没有设置委托对象,程序也照常运行,因为给 nil 发送消息将使 if 语句的值成为 false。

  delegate 对象中的方法名也一定要起得很恰当才行。方法名应该准确描述当前发生的事件以及 delegate 对象为何要获知此事件。在本例中,delegate 对象里的方法名读起来非常清晰,表明某个 “网络数据获取器”(network fetcher)对象刚刚接收到某份数据。正如上一段代码所示,在调用 delegate 对象中的方法时,总是应该把发起委托的实例也一并传入方法中,这样,delegate 对象在实现相关方法时,就能根据传入的实例分别执行不同的代码了。比方说可以这样写:

1 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data {
2     /*Handle data*/
3     if (fetcher == _myFetcherA) {
4         /*Handle data */
5     } else if (fetcher == _myFetcherB) {
6         /*Handle data */
7     }
8 }

  上面这段代码表明,委托对象有两个不同的 “网络数据获取器” ,所以它必须根据传入的参数来判断到底是哪个 EOCNetworkFetcher 获取到了数据。若没有此消息,则委托对象在同一时间只能使用一个网络数据获取器,这么做不太好。

  delegate 里的方法也可以用于从获取委托对象中获取信息。比方说,EOCNetworkFetcher 类也许想提供一种机制:在获取数据时如果遇到了 “重定向”(redirect),那么将询问其委托对象是否应该发生重定向。delegate 对象中的相关方法可以写成这样:

1 - (BOOL)networkFetcher:(EOCNetworkFetcher *)fetcher shouldFollowRedirectToURL:(NSURL *)url;

  通过这个例子,大家应该很容易理解此模式为何叫做 “委托模式”:因为对象把应对某个行为的责任委托给另外一个类了。

  也可以用协议定义一套接口,令某类经由该接口获取其所需的数据。委托模式的这一用法旨在向类提供数据,故而又称 “数据源模式”(Data Source Pattern)。在此模式中,信息从数据源(Data Source )流向类(Class);而在常规的委托模式中,信息则从类流向受委托者(Delegate)。图演示了这两条信息流。

   在信息源模式中,信息从数据源流向类,而在普通的委托模式中,信息则从类流向受委托者

  比方说,用户界面框架中的 “列表视图”(list view)对象可能会通过数据源协议来获取要在列表中显示的数据。除了数据源之外,列表视图还有一个受委托者,用于处理用户与列表的交互操作。将数据源协议与委托协议分离,能使接口更加清晰,因为这两部分的逻辑代码也分开了。另外,“数据源”与“受委托者”可以是两个不同对象。然而一般情况下,都用同一个对象来扮演这两种角色。

  在实现委托模式与数据源模式时,如果协议中的方法是可选的,那么就会写出一大批类似下面这样的代码来:

1     if ([_delegate respondsToSelector:@selector(someClassDidSomething)]) {
2         [_delegate someClassDidSomething];
3     }

  很容易用代码查出某个委托对象是否能响应特定的选择子,可是如果频繁执行此操作的话,那么除了第一次检测的结果有用之外,后续的检测可能都是多余的。如果委托对象本身没变,那么不太可能会突然响应某个原来不能响应的选择子,也不太会突然无法响应某个原来可以响应的选择子。鉴于此,我们通常把委托对象能否响应某个协议方法这一信息缓存起来,以优化程序效率。假设在 “网络数据获取器” 那个例子中,delegate 对象所遵从的协议里有个表示数据获取进度的回调方法,每当数据获取有进度时,委托对象就会得到通知。这个方法在网络数据获取器的生命周期(life cycle)里会多次调用,如果每次检查委托对象是否能响应此选择子,那就显的多余了。

  将刚才说的那个选择子加入之后,delegate 对象所要实现的委托协议就扩充成:

1 @protocol EOCNetworkFetcherDelegate <NSObject>
2 @optional
3 
4 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
5 - (void)netWorkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
6 - (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float)progress;
7 
8 @end

  扩充后的协议只增加了一个方法,就是可选的 “networkFetcher:didUpdateProgressTo:” 方法。将方法响应能力缓存起来的最佳途径是使用 “位段”(bitfield)(又称“位域”、“位字段”) 数据类型。这是一项乏人问津的 C 语言特性,但在此处用起来却正合适。我们可以把结构体中某个字段所占用的二进制位个数设为特定的值。比如像这样:

1 struct data {
2     unsigned int fieldA : 8;
3     unsigned int fieldB : 4;
4     unsigned int fieldC : 2;
5     unsigned int fieldD : 1;
6 };

  在结构体中,fieldA 位段将占用 8 个二进制位,fieldB  占用 4 个,fieldC 占用 2 个, fieldD 占用 1 个。于是,fieldA 可以表示 0 至 255 之间的值,而  fieldD 则可以表示 0 或 1 这两个值。我们可以像 fieldD  这样,把委托对象是否实现了协议中的相关方法这一信息缓存起来。如果创建的结构体中只有大小为 1 的位段,那么就能把许多 Boolean 值塞入一小块数据里面了。以网络数据获取器为例,可以在该实例中嵌入一个含有位段的结构体作为其实例变量,而结构体中的每个位段则表示 delegate 对象是否实现了协议中的相关方法。此结构体的用法如下:

1 @interface EOCNetworkFetcher () {
2     struct {
3         unsigned int didReceiveData : 1;
4         unsigned int didFailWithError : 1;
5         unsigned int didUpdateProgressTo : 1;
6     } _delegateFlags;
7 }
8 
9 @end

  笔者使用第 27 条所讲的 “class-continuation 分类” 来新增实例变量,而新增的这个实例变量是个结构体,其中含有三个位段,每个位段都与 delegate 所遵从的协议中某个可选方法相对应。在 EOCNetworkFetcher 类里,可以像下面这样查询并设置结构体中的位段:

1         // Set flag
2         _delegateFlags.didReceiveData = 1;
3         
4         // Check flag
5         if (_delegateFlags.didReceiveData) {
6             // Yes, flag set
7         }

  这个结构体用来缓存委托对象是否能响应特定的选择子。实现缓存功能所用的代码可以写在 delegate 属性所对应的设置方法里:

1 - (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
2     _delegate = delegate;
3     _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
4     _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(netWorkFetcher:didFailWithError:)];
5     _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
6 }

  这样的话,每次调用 delegate 的相关方法之前,就不用检测委托对象是否能响应给定的选择子了,而是直接查询结构体里的标志:

1     if (_delegateFlags.didUpdateProgressTo) {
2         [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
3     }

  在相关方法要调用很多次时,值得进行这种优化。而是否需要优化,则应依照具体代码来定。这就需要分析代码性能,并找出瓶颈,若发现执行速度需要改进,则可使用此技巧。如果要频繁通过数据源协议从数据源中获取多份相互独立的数据,那么这项优化技术极有可能会提高程序效率。

END

  

 

 

 

 

 

 

 

 

 

 

  

 

posted @ 2017-07-29 00:13  鳄鱼不怕牙医不怕  阅读(362)  评论(0编辑  收藏  举报