iOS设计模式之观察者模式.docx

上传人:b****4 文档编号:4930741 上传时间:2022-12-11 格式:DOCX 页数:14 大小:519.75KB
下载 相关 举报
iOS设计模式之观察者模式.docx_第1页
第1页 / 共14页
iOS设计模式之观察者模式.docx_第2页
第2页 / 共14页
iOS设计模式之观察者模式.docx_第3页
第3页 / 共14页
iOS设计模式之观察者模式.docx_第4页
第4页 / 共14页
iOS设计模式之观察者模式.docx_第5页
第5页 / 共14页
点击查看更多>>
下载资源
资源描述

iOS设计模式之观察者模式.docx

《iOS设计模式之观察者模式.docx》由会员分享,可在线阅读,更多相关《iOS设计模式之观察者模式.docx(14页珍藏版)》请在冰豆网上搜索。

iOS设计模式之观察者模式.docx

iOS设计模式之观察者模式

iOS设计模式之观察者模式

什么是观察者模式?

我们先打个比方,这就像你订报纸。

比如你想知道美国最近放生了些新闻,你可能会订阅一份美国周刊,然后一旦美国有了新的故事,美国周刊就发一刊,并邮寄给你,当你收到这份报刊,然后你就能够了解美国最新的动态。

其实这就是观察者模式,A对B的变化感兴趣,就注册为B的观察者,当B发生变化时通知A,告知B发生了变化。

这是一种非常典型的观察者的用法,我把这种使用方法叫做经典观察者模式。

当然与之相对的还有另外一种观察者模式——广义观察者模式。

从经典的角度看,观察者模式是一种通知变化的模式,一般认为只在对象发生变化感兴趣的场合有用。

主题对象知道有观察者存在,设置会维护观察者的一个队列;而从广义的角度看,观察者模式是中传递变化数据的模式,需要查看对象属性时就会使用的一种模式,主题对象不知道观察者的存在,更像是围观者。

需要知道主题对象的状态,所以即使在主题对象没有发生改变的时候,观察者也可能会去访问主题对象。

换句话说广义观察者模式,是在不同的对象之间传递数据的一种模式。

观察者模式应当是在面向对象编程中被大规模使用的设计模式之一。

从方法论的角度出发,传统的认知论认为,世界是由对象组成的,我们通过不停的观察和了解就能够了解对象的本质。

整个人类的认知模型就是建立在“观察”这种行为之上的。

我们通过不停与世界中的其他对象交互,并观察之来了解这个世界。

同样,在程序的世界中,我们构建的每一个实例,也是通过不不停的与其他对象交互(查看其他对象的状态,或者改变其他对象的状态),并通过观察其他实例的变化并作出响应,以来完成功能。

这也就是,为什么会把观察模式单独提出来,做一个专门的剖析的原因——在我看来他是很多其他设计模式的基础模式,并且是编程中极其重要的一种设计模式。

经典观察者模式

经典观察者模式被认为是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

经典观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。

这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己或者做出相应的一些动作。

在文章一开始举的例子就是典型观察者模式的应用。

而在IOS开发中我们可能会接触到的经典观察者模式的实现方式,有这么几种:

NSNotificationCenter、KVO、Delegate等

感知通知方式

在经典观察者模式中,因为观察者感知到主题对象变化方式的不同,又分为推模型和拉模型两种方式。

推模型

主题对象向观察者推送主题的详细信息,不管观察者是否需要,推送的信息通常是主题对象的全部或者部分数据。

推模型实现了观察者和主题对象的解耦,两者之间没有过度的依赖关系。

但是推模型每次都会以广播的方式,向所有观察者发送通知。

所有观察者被动的接受通知。

当通知的内容过多时,多个观察者同时接收,可能会对网络、内存(有些时候还会涉及IO)有较大影响。

在IOS中典型的推模型实现方式为NSNotificationCenter和KVO。

NSNotificationCenter

NSnotificationCenter是一种典型的有调度中心的观察者模式实现方式。

以NSNotificationCenter为中心,观察者往Center中注册对某个主题对象的变化感兴趣,主题对象通过NSNotificationCenter进行变化广播。

这种模型就是文章开始发布订阅报纸在OC中的一种类似实现。

所有的观察和监听行为都向同一个中心注册,所有对象的变化也都通过同一个中心向外广播。

SNotificationCenter就像一个枢纽一样,处在整个观察者模式的核心位置,调度着消息在观察者和监听者之间传递。

一次完整的观察过程如上图所示。

整个过程中,关键的类有这么几个(介绍顺序按照完成顺序):

1.观察者Observer,一般继承自NSObject,通过NSNotificationCenter的addObserver:

selector:

name:

object接口来注册对某一类型通知感兴趣.在注册时候一定要注意,NSNotificationCenter不会对观察者进行引用计数+1的操作,我们在程序中释放观察者的时候,一定要去报从center中将其注销了。

1

2

3

4

5

6

7

8

9

-(void)handleMessage:

(NSNotification*)nc{

     //解析消息内容

 NSDictionary*userInfo=[ncuserInfo];

 }

 -(void)commonInit

 {

     //注册观察者

     [[NSNotificationCenterdefaultCenter]addObserver:

selfselector:

@selector(handleMessage:

)name:

kDZTestNotificatonMessageobject:

nil];

 }

2.通知中心NSNotificationCenter,通知的枢纽。

3.主题对象,被观察的对象,通过postNotificationName:

object:

userInfo:

发送某一类型通知,广播改变。

1

2

3

4

-(void)postMessage

 {

     [[NSNotificationCenterdefaultCenter]postNotificationName:

kDZTestNotificatonMessageobject:

NiluserInfo:

@{}];

 }

4.通知对象NSNotification,当有通知来的时候,Center会调用观察者注册的接口来广播通知,同时传递存储着更改内容的NSNotification对象。

apple版实现的NotificationCenter让我用起来不太爽的几个小问题

在使用NSNotificationCenter的时候,从编程的角度来讲我们往往不止是希望能够做到功能实现,还能希望编码效率和整个工程的可维护性良好。

而Apple提供的以NSNotificationCenter为中心的观察者模式实现,在可维护性和效率上存在以下缺点:

1.每个注册的地方需要同时注册一个函数,这将会带来大量的编码工作。

仔细分析能够发现,其实我们每个观察者每次注册的函数几乎都是雷同的。

这就是种变相的CtrlCV,是典型的丑陋和难维护的代码。

2.每个观察者的回调函数,都需要对主题对象发送来的消息进行解包的操作。

从UserInfo中通过KeyValue的方式,将消息解析出来,而后进行操作。

试想一下,工程中有100个地方,同时对前面中在响应变化的函数中进行了解包的操作。

而后期需求变化需要多传一个内容的时候,将会是一场维护上的灾难。

3.当大规模使用观察者模式的时候,我们往往在dealloc处加上一句:

[[NSNotificationCenterdefaultCenter]removeObserver:

self]

而在实际使用过程中,会发现该函数的性能是比较低下的。

在整个启动过程中,进行了10000次RemoveObserver操作,

1

2

3

4

5

6

@implementationDZMessage

 -(void)dealloc

 {

    [[NSNotificationCenterdefaultCenter]removeObserver:

self];

 }

 ....

1

2

3

for(inti=0;i<10000;i++){

      DZMessage*message=[DZMessagenew];

  }

4.通过下图可以看出这一过程消耗了23.4%的CPU,说明这一函数的效率还是很低的。

这还是只有一种消息类型的存在下有这样的结果,如果整个NotificationCenter中混杂着多种消息类型,那么恐怕对于性能来说将会是灾难性的。

1

2

3

4

for(inti=0;i<10000;i++){

      DZMessage*message=[DZMessagenew];

      [[NSNotificationCenterdefaultCenter]addObserver:

selfselector:

@selector(handle)name:

[@(i)stringValue]object:

nil];

  }

5.增加了多种消息类型之后,RemoveObserver占用了启动过程中63.9%的CPU消耗。

而由于Apple没有提供Center的源码,所以修改这个Center几乎不可能了。

改进版的有中心观察者模式(DZNotificationCenter)

GitHub地址 在设计的时候考虑到以上用起来不爽的地方,进行了优化:

1.将解包到执行函数的操作进行了封装,只需要提供某消息类型的解包block和消息类型对应的protocol,当有消息到达的时候,消息中心会进行统一解包,并直接调用观察者相应的函数。

2.对观察者的维护机制进行优化(还未做完),提升查找和删除观察者的效率。

DZNotificationCenter的用法和NSNotificationCenter在注册和注销观察者的地方是一样的,不一样的地方在于,你在使用的时候需要提供解析消息的block。

你可以通过两种方式来提供。

∙直接注册的方式

1

2

3

4

5

6

7

8

[DZDefaultNotificationCenteraddDecodeNotificationBlock:

^SEL(NSDictionary*userInfo,NSMutableArray*__autoreleasing*params){

      NSString*key=userInfo[@"key"];

      if(params!

=NULL){

          *params=[NSMutableArraynew];

      }

      [*params addObject:

key];

      return@selector(handleTestMessageWithKey:

);

  }forMessage:

kDZMessageTest];

∙实现DZNotificationInitDelegaete协议,当整个工程中大规模使用观察者的时候,建议使用该方式。

这样有利于统一管理所有的解析方式。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

-(DZDecodeNotificationBlock)decodeNotification:

(NSString*)messageforCenter:

(DZNotificationCenter*)center

{

    if(message==kDZMessageTest){

        return^(NSDictionary*userInfo,NSMutableArray*__autoreleasing*params){

            NSString*key=userInfo[@"key"];

            if(params!

=NULL){

                *params=[NSMutableArraynew];

            }

            [*params addObject:

key];

            return@selector(handlePortMessage:

);

        };

    }

    returnnil;

}

在使用的过程中为了,能够保证在观察者处能够回调相同的函数,可以实现针对某一消息类型的protocol

1

2

3

@protocolDZTestMessageInterface<NSObject>

-(void)handleTestMessageWithKey:

(NSString*)key;

@end

这样就能够保证,在使用观察者的地方不用反复的拼函数名和解析消息内容了。

1

2

3

4

5

6

7

8

9

@interfaceDZViewController()<DZTestMessageInterface>

@end

@implementationDZViewController

....

-(void)handleTestMessageWithKey:

(NSString*)key

{

    self.showLabel.text=[NSStringstringWithFormat:

@"getmessagewith%@",key];

}

....

KVO

KVO的全称是Key-ValueObserver,即键值观察。

是一种没有中心枢纽的观察者模式的实现方式。

一个主题对象管理所有依赖于它的观察者对象,并且在自身状态发生改变的时候主动通知观察者对象。

让我们先看一个完整的示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

staticNSString*constkKVOPathKey=@"key";

@implementationDZKVOTest

-(void)setMessage:

(DZMessage*)message

{

    if(message!

=_message){

        if(_message){

            [_messageremoveObserver:

selfforKeyPath:

kKVOPathKey];

        }

        if(message){

            [messageaddObserver:

selfforKeyPath:

kKVOPathKeyoptions:

NSKeyValueObservingOptionNewcontext:

Nil];

        }

        _message=message;

    }

}

-(void)observeValueForKeyPath:

(NSString*)keyPathofObject:

(id)objectchange:

(NSDictionary*)changecontext:

(void*)context

{

    if([keyPathisEqual:

kKVOPathKey]&&object==_message){

        NSLog(@"get%@",change);

    }

}

-(void)postMessage

{

    _message.key=@"asdfasd";

}

@end

完成一次完整的改变通知过程,经过以下几次过程:

1.注册观察者[messageaddObserver:

selfforKeyPath:

kKVOPathKeyoptions:

NSKeyValueObservingOptionNewcontext:

Nil];

2.更改主题对象属性的值,即触发发送更改的通知 _message.key=@"asdfasd";

3.在制定的回调函数中,处理收到的更改通知

1

2

3

4

5

6

-(void)observeValueForKeyPath:

(NSString*)keyPathofObject:

(id)objectchange:

(NSDictionary*)changecontext:

(void*)context

{

 if([keyPathisEqual:

kKVOPathKey]&&object==_message){

     NSLog(@"get%@",change);

 }

}

4.注销观察者 [_messageremoveObserver:

selfforKeyPath:

kKVOPathKey];

KVO实现原理

一般情况下对于使用Property的属性,objc会为其自动添加键值观察功能,你只需要写一句@property(noatomic,assign)floatage 就能够获得age的键值观察功能。

而为了更深入的探讨一下,KVO的实现原理我们先手动实现一下KVO:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

@implementationDZKVOManual

-(void)setAge:

(int)age

{

    [selfwillChangeValueForKey:

kKVOPathAge];

    if(age!

=_age){

        _age=age;

    }

    [selfdidChangeValueForKey:

kKVOPathAge];

}

//经验证 会先去调用automaticallyNotifiesObserversForKey:

当该函数没有时才会调用automaticallyNotifiesObserversOfAge。

这个函数应该是编译器,自动增加的一个函数,使用xcode能够自动提示出来。

的确很强大。

//+(BOOL)automaticallyNotifiesObserversOfAge

//{

//   returnNO;

//}

+(BOOL)automaticallyNotifiesObserversForKey:

(NSString*)key

{

    if(key==kKVOPathAge){

        returnNO;

    }

    return[superautomaticallyNotifiesObserversForKey:

key];

}

@end

首先,需要手动实现属性的setter方法,并在设置操作的前后分别调用willChangeValueForKey:

和didChangeValueForKey方法,这两个方法用于通知系统该key的属性值即将和已经变更了;

其次,要实现类方法automaticallyNotifiesObserversForKey,并在其中设置对该key不自动发送通知(返回NO即可)。

这里要注意,对其它非手动实现的key,要转交给super来处理。

在这里的手动实现,主要是手动实现了主题对象变更向外广播的过程。

后续如何广播到观察者和观察者如何响应我们没有实现,其实这两个过程apple已经封装的很好了,猜测一下的话,应该是主题对象会维护一个观察者的队列,当本身属性发生变动,接受到通知的时候,找到相关属性的观察者队列,依次调用observeValueForKeyPath:

(NSString*)keyPathofObject:

(id)objectchange:

(NSDictionary*)changecontext:

(void*)context来广播更改。

还有一个疑问,就是在自动实现KVO的时候,系统是否和我们手动实现做了同样的事情呢?

自动实现KVO及其原理

我们仔细来观察一下在使用KVO的过程中类DZMessage的一个实例发生了什么变化:

在使用KVO之前:

当调用Setter方法,并打了断点的时候:

神奇的发现类的isa指针发生了变化,我们原本的类叫做DZMessage,而使用KVO后类名变成了NSKVONotifying_DZMessage。

这说明objc在运行时对我们的类做了些什么。

我们从Apple的文档Key-ValueObservingImplementationDetails找到了一些线索。

Automatickey-valueobservingisimplementedusingatechniquecalledisa-swizzling.Theisapointer,asthenamesuggests,pointstotheobject’sclasswhichmaintainsadispatchtable.Thisdispatchtableessentiallycontainspointerstothemethodstheclassimplements,amongotherdata.Whenanobserverisregisteredforanattributeofanobjecttheisapointeroftheobservedobjectismodified,pointingtoanintermediateclassratherthanatthetrueclass.Asaresultthevalueoftheisapointerdoesnotnecessarilyreflecttheactualclassoftheinstance.Youshouldneverrelyontheisapointertodetermineclassmembership.Instead,youshouldusetheclassmethodtodeterminetheclassofanobjectinstance.

当某一个类的实例第一次使用KVO的时候,系统就会在运行期间动态的创建该类的一个派生类,该类的命名规则一般是以NSKVONotifying为前缀,以原本的类名为后缀。

并且将原型的对象的isa指针指向该派生类。

同时在派生类中重载了使用KVO的属性的setter方法,在重载的setter方法中实现真正的通知机制,正如前面我们手动实现KVO一样。

这么做是基于设置属性会调用setter方法,而通过重写就获得了KVO需要的通知机制。

当然前提是要通过遵循KVO的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现KVO的。

同时派生类还重写了class方法以“欺骗”外部调用者它就是起初的那个类。

因此这个对象就成为该派生类的对象了,因而在该对象上对setter的调用就会调用重写的setter,从而激活键值通知机制。

此外,派生类还重写了dealloc方法来释放资源。

拉模型

拉模型是指主题对象在通知观察者的时候,只传递少量信息或者只是通知变化。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1