第十五章 消息发送模式

应用和运行回路

运行回路

应用从操作系统中接收鼠标点击等事件的消息,并将其转到相应的例行程序来处理,如此反复,这样的过程被称为运行回路(run loop)或事件循环(event loop)。

因为Cocoa应用本身就有GUI功能,所以一旦开始运行,就一定会产生一个运行回路,它也称为主运行回路。同时,应用的事件处理或资源的管理功能需要有一个对象来完成,所以Mac OS中提供了NSApplication类的实例,iOS中提供了UIApplication类的实例,该实例会根据操作系统发送的事件选择对应的处理对象,并发送相应的消息。

回路运行后,当有新的事件消息到来而别的处理还没有结束的情况下,应用就可以把该事件放入等待队列中并在之后按顺序执行。这种性质非常适合多线程的异步执行。

在使用引用计数的方式的情况下,主运行回路在启动事件处理方法时会生成一个自动释放池,并在方法终止的时候释放它。而垃圾回收的情况下,一个事件处理完后就会启动一个垃圾回收器。应用中执行的方法基本上是自己管理自动释放池,不需要担心什么时候进行垃圾回收。而主线程之外执行的方法则要自己生成自动释放池。

定时器对象

现在介绍一下能够在特定的时间或者按照一定的时间间隔来发送指定消息的组件。灵活使用该组件,就能够轻松实现一些必须并行处理才能够实现的操作。

NSTimer类能够让指定的消息在一定的时间间隔后执行。而定时器对象即实现了NSTimer类的实例。

定时器的使用必须要有运行回路。在运行回路上注册定时器后,到达规定的时间时,运行回路就会调用注册的方法来处理。

更多有关NSTimer的相关介绍请参考相关文档。

消息的延迟执行

下面介绍一下经过一段时间后在执行消息的方法。该方法由NSObject定义,所有对象都可以使用。同样延迟执行也要有运行回路。

- (void)performSelector:(SEL)aSelector withObject:(nullableid)anArgument afterDelay:(NSTimeInterval)delay

//该消息被发送后,至少要经过delay秒,aSelector指定的消息才会真正发送给anArgument。

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector withObject:(id)anArgument

//取消先前执行的请求。

委托

委托的概念

当对象需要根据用途改变或增加新功能时,为了执行新添加的处理,就需要引用一个特殊的类似于“被咨询者”的对象。这个对象就称为委托(delegate)。委托可在运行时动态分配。

委托是对象之间分担功能并协同处理时的一个典型的设计模式。在面向对象中,委托一般可以被解释为“某对象接收到不能处理的消息时让其他对象带为处理的一种方式”。

Cocoa环境中的委托

在Cocoa环境中,委托是应用为了添加必要的处理而“增设”的对象。

委托具有很高的可复用性,在作为组件使用的类中十分有效。通过使用委托,可以在不损害原类别的独立性的同时,给软件增加独立的功能。

一个对象并不是只能有一个委托,某些情况下一个对象也可以拥有多个委托。

在某种程度上,使用继承也可以达到相同的效果,但是继承在运行时无法灵活的分配。继承和委托必须要区分使用,但委托有很多继承没有的优点。

委托的设置和协议

设置委托,返回委托对象的方法使用如下函数:

- (void)setDelegate:(id)anObject

//将参数对象设置为委托。一般不需要保留(retain)参数对象。是显式调用委托的协议。

- (id)delegate

//返回接受的委托对象。

在实际编程时,除了要关注各类中可以调用的方法外,还需要注意可以用委托实现什么样的功能。

如果要将某个对象作为委托使用,就需要在该类的接口部分中声明使用委托的协议。定义实现委托方法的范畴就是一个好方法。而且,一个对象可以作为多个类的委托。此时将使用与之相对应的多个协议。

声明委托的协议时,基本上所有的方法中都要指定@optional选项。持有委托的对象会在运行时检查该委托所能处理的消息。委托中没实现的消息不会被发送。所以委托对象只需实现相应的处理方法即可。

此外,由于某些情况下向委托发送消息是以存在运行回路为前提的,因此我们就需要注意生成命令行程序时的情况。

自定义类虽然也可以有委托的功能,但是实现起来并不简单。实现时需要使用respondsToSelector:等来检查是否可以处理某个消息。

通知

通知和通知中心的概念

在面向对象的程序中,有时也需要将发生的事件通知给多个对象。

在Foundation框架中,为了执行某一行为,程序需要将这一行为向多个对象发出通知,这种消息发送方式就称为通知(notification)。

程序内提供了通知中心这个对象。(notification center)这个对象。期望取得通知的对象预先向通知中心注册期望取得的通知。通知有多种类型,分别以通知名来标记,也可以自定义新的通知名。

某对象向通知中心发送消息发送请求,这称为发送(post)通知。只要是注册过该通知的对象,都会获得通知中心推送的消息。

消息发送目标,也就是在通知中心注册的对象,称为观察者(observer)。对象在通知中心将自己注册为观察者时,会指定接受什么名字的通知以及什么什么选择器的消息。而且,也可以指定只接收特定对象发送(post)的通知。观察者很多或者完全没有都没有关系。

无论什么对象都可以自由的使用通知的发送机制,而且不需要在通知中心注册。两者之间必须共享的仅仅是通知的名字。

这样的机制既不会降低发送通知的对象和接收通知的观察者之间的独立性,又可以实现向多个对象发送消息。

某个对象向特定的多个对象发送消息的通信形式称为多播(multicast),向非特定的多个对象发送消息称为广播(brodcast)。

通知对象

向通知中心发送消息时,必要的信息会在NSNotification类实例中集中后发送给通知中心。这个对象就被称为通知对象,或者简称为通知。观察者从通知中心取得消息时也会取得附带的通知对象。

通知对象包含以下信息:

名字:

用来识别通知的短文本。用如下消息取出。

- (NSString *)name

对象:

和通知一起发送的附带信息的对象。多为发送通知的对象,也可以为nil。用如下消息取出。

- (id)object

用户字典(userInfo)

为了传递和通知相关的各种信息而使用NSDictionary字典。用如下消息取出。

- (NSDictionary *)userInfo

因为向通知中心发送消息时会自动创建通知对象,所以没有必要自己创建。创建通知对象,可以使用下面任意一种NSNotification的类方法。

+ (id)notificationWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)userInfo

//指定通知名,对象和用户字典,生成临时通知对象。

+ (id)notificationWithName:(NSString *)aName obect:(id)anObject

//指定通知名和对象生成临时通知对象。用户信息字典为nil。

通知中心

通知中心使用NSNotificationCenter的类接口实现。

1.默认的通知中心

各进程都会预先准备一个通知中心,一般不需要自己创建。这就是默认的通知中心,可以使用如下的类方法获得。

+ (id)defaultCenter

2.通知的发送

相关方法参考文档

3.观察者注册

相关方法参考文档

4.删除观察者注册

相关方法参考文档

这里需要格外注意的内存管理。首先,使用引用计数管理的情况下,通知中心在注册观察者时,并不保留(retain)观察者及发送源对象。所以,在释放这些对象之前,要确实从通知中心删除相关设置。如果不这样的话,指向释放对象的指针将成为空指针。

使用垃圾回收机制时,观察者和发送源对象使用弱引用在通知中心注册。无需显式删除观察者的注册。

通知队列

通知队列是将通知对象临时存储在等待队列中,然后按照FIFO的原则向通知中心发送消息。通知队列是通过NSNotificationQueue实现的。详细介绍参考相关文档。

1.异步发送

将通知追加到队列,然后在运行回路完成当前处理或者运行回路中的输入都被处理完成后,再发送通知。通过使用该方法,就可以直接终止队列中追加的方法并进行下一项处理。这样一来,通知的发送就会在一连串处理之后再执行。这种方式也称为异步(asychronous)发送。一般的处理方式是同步(synchronous)发送。

2.合并相同的通知

通知队列中有相同的通知时,将它们合并为一个,以此来删除多余的通知。

通知名或异常名的定义:

自定义使用通知名或异常名等时,一般都不直接使用常数字符串,而是用全局变量或宏名来定义。

反应链

反应链概述

反应链(responder chain)是具有层次结构的GUI组件间自动发送消息的一种方式。

处理消息的候补组件对象如算珠般串联在一起,将消息传递给这之间的某个对象时,在发现可以处理该消息的对象之前,会顺次向后后面的对象转交该消息。这样的构成就称为反应链。

在Mac OS 中,按键,滑块,文本域等组件以及窗体,面板等对象都是Application框架的NSResponder类的子类。同样,在iOS中是UIKit框架的UIResponder类的子类。

各窗体中,最初被发送消息的对象称为第一反应者(first responder)。第一反应者都是光标最近选择的对象。

应用中的反应链

在Mac OS 平台下,在应用窗口中显示在最前端的窗口称为主窗口。在包含面板等在内的其他窗口中,最前面的窗口称为关键窗口。主窗口也可以是关键窗口。

反应链内的搜索,首先从关键窗口的第一反应者开始。关键窗口中无法应对找不到时,如果存在其他主窗口,主窗口的反应链也会按同样的方式搜索。

有时主窗口也不能处理时,消息最终会被传给NSApplication(管理应用的全体的类)的接口。这样一来,不能处理的消息就会被忽视,或者响起警告音。

在iOS平台下,表示绘图区域的类UIView是其他GUI组件的超类,同时也是表示反应者的UIResponder的子类。接口由窗口(UIWindow类),UIView类及其子类GUI组件有层次地重叠构成。UIKit会自动将任意一个按键或文本域作为第一反应者。第一反应者不能处理的消息,将沿着重叠的GUI组件,被有序地向窗口甚至引用程序(UIApplication类)传递。

在Cocoa环境中,生成包含GUI的应用必须掌握反应链相关的知识。详细内容参考“Cocoa Event-Handing Guide” “Event-Handing Guide for iOS"

消息转送

消息转送的构成

将消息发送给没有实现该消息方法的对象时,通常会出现运行时错误。但是,我们可以将不能被处理的消息转送给其他对象,这样的功能是可以实现的。

将某消息发送到相应的接收者。如果接收者没有实现应对消息的方法,运行时系统就会发送如下消息给接收者。

- (void)forwardInvocation:(NSInvocation *)anInvocation

//这个方法在NSObject中定义,所有对象都可以处理。在NSObject中,这个方法可以调用如下方法

- (void)doesNotRecognizeSelector:(SEL)aSelector

//生成错误消息,表示异常NSInvalidArgumentException发生,无法处理参数选择器对应的消息。

也就是说,只要接收者重定义了forwardInvocation:方法,当被发送不能处理的消息时,就可以将其转送给其他对象,或者自己执行错误处理。

消息转送需要的信息

NSInvocation的对象中保存了目标,选择器,参数等消息发送所必须的全部元素。

下面是NSInvocation类的主要方法。

- (SEL)selector

//返回设定的选择器

- (id)target

//返回设定的目标

- (void)invokeWithTarget:(id)anObject

//向目标参数对象发送表示接收者的消息。将消息的结果返回给源发送者。

消息转送的定义

使用上述的NSInvocation信息,forwardInvocation:就可以被重新定义,从而实现消息转送。但是,可以转送的仅仅是参数个数固定的方法。

例如,调查不能处理的消息是否可以转送给对象fellow,当不能转送则交由超类处理,可采用如下方式定义:

- (void)forwardInvocation:(NSInvocation *)anInvocation {

SELsel = [anInvocation selector];

if([fellow respondToSelector:sel])

[anInvocation invokeWithTarget:fellow];

else

[superforwardInvocation];

}

再有为了使运行时系统能够使用转送目的地的对象信息生成NSInvocation实例,必须重新定义返回的方法签名(method signature)对象。

为了表示方法签名,定义了NSMethodSignature类。它的对象保存着方法的参数和返回值,但在消息转送或通信之外的情况下,程序是不处理的。

返回方法签名的方法在methodSignatureForSelector:和NSObject中定义。例如转送目的地是fellow,就可以按如下方式重新定义。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

if([superrespondsToSelector:aSelector])

return[supermethodSignatureForSelector:aSelector];

return[fellow methodSignatureForSelector:aSelector];

}

然而,使用转送方法处理的消息不能被respondsToSelector:等调用。当需要知道该消息是否为目标对象可处理的消息时,必须重定义respondsToSelector:等方法。

禁止使用消息

如前所述,方法doesNotRecognizedSelector:表示消息不能处理时所产生的异常。

利用此特性,在使用某个方法时就可以定义运行时产生的错误。一个典型的例子就是,当用超类定义的方法在子类不能使用时,可以写下该方法及时禁止其使用。

例如,想禁止使用方法setSize:时,可采用如下方式定义。这里_cmd为方法的隐藏参数,表示方法的选择器。所以写下@selector(setSize:)也是一样的。

- (void)setSize:(NSSize *)size {

[selfdoesNotRecognizeSelector:_cmd];

}

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 106,582评论 12 127
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 8,313评论 0 15
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 5,768评论 4 12
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 36,797评论 11 336
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 5,649评论 2 17