美文网首页设计模式程序员
设计模式第三篇、观察者设计模式

设计模式第三篇、观察者设计模式

作者: 意一ineyee | 来源:发表于2018-09-13 16:22 被阅读26次
    OC编程之道:设计模式

    目录

    一、通知
     1、什么是通知
     2、怎么使用通知
     3、通知的实现原理
     4、使用通知需要注意的地方
     5、通知的应用场景
    二、KVO
     1、什么是KVO
     2、怎么使用KVO
     3、KVO的实现原理
     4、使用KVO需要注意的地方
     5、KVO的应用场景

    一、通知


    1、什么是通知

    通知是一种基于观察者设计模式实现的、用来进行一对多通信或者跨层通信的机制,主要是为了App内部对象解耦而设计的,它有几个比较重要的关键词通知中心通知观察者发布者通知的回调

    2、怎么使用通知
    • 首先,在通知中心添加观察者,并实现观察者收到通知后要的回调。
    /**
     *  在通知中心添加观察者
     *
     *  @param  observer    通知的观察者
     *  @param  aSelector   观察者收到通知后要触发的方法
     *  @param  aName       通知的名字
     *  @param  anObject    通知的发送者(如果这个值为nil,代表观察者想要接收名为aName的通知,但并不关心aName通知到底是谁在通知中心发出的,所以只要通知中心发出了aName通知,观察者就会收到;如果这个值不为nil,代表观察者只想接收由anObject在通知中心发出的、名为aName的通知,anObject发出的其它通知观察者收不到,其它人发出的aName通知观察者也收不到;一般情况下,我们会把这个值设置为nil,有特殊场景时可以使用该值)
     */
    - (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
    
    // 实现观察者收到通知后要的回调
    ......
    
    • 然后,在需要发出通知的地方和时机,在通知中心发出通知。这样通知发出后,观察者就会收到通知并且触发收到通知的方法,我们就可以在这个方法里根据实际业务做相应的事情了。
    /**
     *  在通知中心发出通知
     *
     *  @param  aName       通知的名字
     *  @param  anObject    通知的发送者
     *  @param  aUserInfo   通知携带的额外信息
     */
    - (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
    
    • 最后,在通知中心移除观察者。
    
    /**
     *  在通知中心移除观察者(代表观察者不再想接收来自通知中心的任何通知)
     *
     *  @param  observer    通知的观察者
     */
    - (void)removeObserver:(id)observer;
    

    举一个简单的例子来看下:前一个界面要检测后一个界面是否被点击,我们用通知来实现。

    -----------前一个界面-----------
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 首先,在通知中心添加观察者
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationAction:) name:@"didTouch" object:nil];
    }
    
    // 并实现观察者收到通知后要触发的方法
    - (void)notificationAction:(NSNotification *)notification {
        
        NSLog(@"===========>收到了通知:%@", notification.userInfo);
    }
    
    - (void)dealloc {
        
        // 最后,在通知中心移除观察者
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    
    -----------后一个界面-----------
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        // 然后,在需要发出通知的地方和时机,在通知中心发出通知
        [[NSNotificationCenter defaultCenter] postNotificationName:@"didTouch" object:nil userInfo:@{@"11" : @"你好"}];
    }
    
    3、通知的实现原理

    通知的实现原理其实很简单,并没有我们想象的那么复杂,该博客模拟实现了一下通知机制,从中我们可以得到通知简化的实现原理:

    我们知道通知中心是个单例,这个单例里其实维护着一个字典,这个字典里的key就是一个个的通知,而每个key对应的value是一个数组,数组里存放的就是想要接收该通知的一个个大观察者,大观察者就是指用观察者和通知回调构建的一个对象。这就是通知实现的核心所在。

    那么,当我们调用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;在通知中心添加观察者的时候,这个方法内部会用通知的名字和通知的发送者构建一个通知作为key,用观察者和通知回调构建一个大观察者放在key对应的value数组里,然后把它们放进通知中心的字典里。

    然后,当我们调用- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;在通知中心发出通知的时候,这个方法内部也会根据通知的名字和通知的发送者构建一个通知,然后拿着这个通知去通知中心字典里去把它对应的value数组拿到,然后遍历这个数组,让数组中所有的观察者都perform一下这个aSelector。这样就实现了通知中心发送通知,众多观察者触发回调方法的效果。

    最后,当我们调用- (void)removeObserver:(id)observer;方法在通知中心移除观察者的时候,方法内部会拿着这个观察者去通知中心字典里遍历与对比,如果找到这个观察者,就把它从通知中心字典里移除掉。

    当然了,以上只是对通知实现原理的简化描述,系统内部的实现要比这个复杂的多,它要考虑很多其它的东西,比如性能、多线程发送通知等问题。

    4、使用通知应该注意的地方
    (1)在dealloc方法里remove掉观察者,避免向野指针发送消息导致程序崩掉

    为什么要remove呢?

    我们这里所说的主要是针对iOS9之前的系统,那个时候,我们调用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;在通知中心添加观察者的时候,通知中心对观察者的语义为unsafe_unretained,这个语义和weak差不多,不会持有对象不会使对象的引用计数加1,但是unsafe_unretained在对象释放之后不会把它置为nil,就会出现野指针,而weak会将释放掉的对象置为nil。所以如果我们不移除观察者,通知中心的字典里就还有这个观察者,此时发送一个通知,就是向野指针发送了一个消息,App会崩掉。

    而iOS9之后通知中心对观察者引用变成了weak,所以即便我们不移除观察者,App也不会崩掉,因为向nil发送消息只会立马返回,什么也不发生,更好的是系统在dealloc的时候会自动调用- (void)removeObserver:(id)observer;方法帮我们移除掉观察者。

    但是我们最好还是在dealloc里面手动移除观察者,因为有些情况系统是没办法帮到我们的,比如使用block的方式在通知中心添加观察者,又比如在分类里面添加观察者,这些情况下都需要我们自己来移除,确保程序的安全。

    (2)通知是同步执行的,要避免大量使用通知或者在通知的回调里处理很大的事情导致App卡死,或者你也可以把通知写成异步的

    还是上面的例子,我们简单改一下:

    -----------前一个界面-----------
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 首先,在通知中心添加观察者
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationAction:) name:@"didTouch" object:nil];
    }
    
    // 并实现观察者收到通知后要触发的方法
    - (void)notificationAction:(NSNotification *)notification {
        
        NSLog(@"===========>收到了通知:%@", notification.userInfo);
    }
    
    - (void)dealloc {
        
        // 最后,在通知中心移除观察者
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    
    -----------后一个界面-----------
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        NSLog(@"===========>通知发送前");
        
        // 然后,在需要发出通知的地方和时机,在通知中心发出通知
        [[NSNotificationCenter defaultCenter] postNotificationName:@"didTouch" object:nil userInfo:@{@"11" : @"你好"}];
        
        NSLog(@"===========>通知发送后");
    }
    

    这样,当我们点击后一个界面的时候,我们会发现控制台的打印为:

    ===========>通知发送前
    ===========>收到了通知:{
        11 = "你好";
    }
    ===========>通知发送后
    

    可以发现通知是同步执行的,也就是说当通知发出后,程序会等通知所有的观察者都收到通知并且执行完回调后,才会返回来执行发送通知后面的代码,因此一旦某个通知的观察者很多或者通知回调里要做很多的事情,那么程序就会卡死在这儿

    因此,如果我们想让通知异步执行,可以采用通知队列的方式或者自己写个子线程来发送通知。

    代码修改如下:

    -----------后一个界面-----------
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        NSLog(@"===========>通知发送前");
        
        // 通知
        NSNotification * notification = [NSNotification notificationWithName:@"didTouch" object:nil userInfo:@{@"11" : @"你好"}];
        
        // 获取当前线程下的通知队列,每个线程有一个默认的通知队列
        NSNotificationQueue * notificationQueue = [NSNotificationQueue defaultQueue];
        
        // 把通知加入队列
        // postingStyle(发送通知的时机):NSPostWhenIdle--空闲时发送,NSPostASAP--尽快发送,NSPostNow--立即发送,这种情况其实还是同步的
        // coalesceMask(通知合并的类型):NSNotificationNoCoalescing--不合并,NSNotificationCoalescingOnName,NSNotificationCoalescingOnName--按通知的名字合并,也就是说名字相同的通知会被合并成一个通知,仅发送一次,NSNotificationCoalescingOnSender--按通知的发送者合并
        [notificationQueue enqueueNotification:notification postingStyle:NSPostWhenIdle coalesceMask:NSNotificationNoCoalescing
                                      forModes:@[NSRunLoopCommonModes]];
        
        NSLog(@"===========>通知发送后");
    }
    

    控制台的打印为:

    ===========>通知发送前
    ===========>通知发送后
    ===========>收到了通知:{
        11 = "你好";
    }
    

    或者代码修改如下:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        NSLog(@"===========>通知发送前");
        
        [NSThread detachNewThreadSelector:@selector(subthreadAction) toTarget:self withObject:nil];
        
        NSLog(@"===========>通知发送后");
    }
    
    // 子线程
    - (void)subthreadAction {
        
        NSLog(@"当前线程===%@", [NSThread currentThread]);
        
        NSNotification * notification = [NSNotification notificationWithName:@"didTouch" object:nil userInfo:@{@"11" : @"你好"}];
        [[NSNotificationCenter defaultCenter] postNotification:notification];
    }
    

    控制台的打印也为:

    ===========>通知发送前
    ===========>通知发送后
    ===========>收到了通知:{
        11 = "你好";
    }
    
    5、通知的应用场景

    通知的应用场景主要就是一对多传值跨层传值,实现起来很简单,无非就是利用通知回调和通知的userInfo来传值嘛,就不多举例了。

    这里主要对比一下单例传值和通知传值,单例传值不也主要用于一对多传值和跨层传值嘛。

    • 单例传值一般都是定义一些属性来传值,因此它的值总是在别人使用前需要就提前预置好,然后供别人使用,一旦程序运行期间这个值改动了,在读取这个值的地方,我们必须手动的重新加载(比如重新打开界面或者刷新一下)才能读取到新值,然后做相应的处理。

    • 而通知传值由于是通过回调方法来传值的,虽然它那个共享的值在发送前肯定也是提前预置好的,但这个值在程序的运行期间是可以随时变化的,不管它变成什么,只要我们发出通知,通知的回调就会被触发,我们就能获取到这个最新的值,并立马做出处理,不需要我们手动加载这一步。

    这样给人的感觉就是单例传值的灵活性和实时性好像不如通知传值那么高,但是我们也不能一棒子把单例传值打死,因为有些场景其实我们根本不在意这种灵活性和实时性,在那种场景下单例传值反而会更加方便,所以这就要看实际的开发场景了,看看你当时什么感觉,想用什么就用什么。不过不要忽略单例传值的内存问题和通知传值的同步问题

    二、KVO


    1、什么是KVO

    KVO也是基于观察者设计模式实现的、专门用来观察某个对象属性变化的一种机制,它也有几个比较重要的关键词想要观察哪个对象的属性、观察者、观察者回调

    2、怎么使用KVO
    • 首先,我们要确定想要观察哪个对象的属性,确定下来后,我们还要确定一个合适的人选作为观察者,这样我们就可以给这个对象添加观察者来观察它的那个属性了,添加的时候我们也可以表明观察者想要观察该属性的新值还是旧值,并且在观察者的地盘里实现观察者回调。
    /**
     *  给对象添加观察者观察它的某个属性
     *
     *  @param  observer    观察者
     *  @param  keyPath     观察者想要观察对象的哪个属性
     *  @param  options     观察者想要观察该属性的新值还是旧值(如果想在添加观察者后,立即触发一次观察者回调,可以在这里添上NSKeyValueObservingOptionInitial这个值)
     *  @param  context     额外信息
     */
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    // 实现观察者回调
    
    /**
     *  观察者回调
     *
     *  @param  keyPath     观察者所观察的属性
     *  @param  object      观察者所观察属性所属的对象
     *  @param  change      观察者所观察属性的新值、旧值都存在这里(我们还可以通过NSKeyValueChangeKindKey来判断新旧值的变化是重设、新增、替换还是移除)
     *  @param  context     额外信息
     */
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
    
    • 然后,当被观察的属性变化的时候(注意KVO只能检测属性指针的变化,而检测不到属性值的变化,也就是说KVO只会检测属性是否指向了一个完全的新对象,而如果指针不变,仅仅是老对象的值变化了,KVO是检测不到的),观察者回调就会被触发了,我们就能做相应的处理。

    • 最后,移除观察者。

    /**
     *  移除观察者
     *
     *  @param  observer    观察者
     *  @param  keyPath     观察者所观察的属性
     */
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    

    举一个简单的例子来看下:假设现在有一个Person类,Person类有age和children属性,我们来观察这两个属性的变化。

    -----------ViewController.m-----------
    
    #import "ViewController.h"
    #import "Person.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) Person *kobe;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.kobe = [[Person alloc] init];
        self.kobe.age = 40;
        self.kobe.children = [@[@"first girl", @"second girl", @"third girl"] mutableCopy];
    
        [self.kobe addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil];
        [self.kobe addObserver:self forKeyPath:@"children" options:(NSKeyValueObservingOptionNew) context:nil];
    }
    
    - (void)dealloc {
        
        [self.kobe removeObserver:self forKeyPath:@"age"];
        [self.kobe removeObserver:self forKeyPath:@"children"];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        NSLog(@"===========>观察者回调:%@===%@", keyPath, change[NSKeyValueChangeNewKey]);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        self.kobe.age = 41;
        [self.kobe.children addObject:@"first boy"];
    }
    
    @end
    

    点击屏幕,我们发现观察者回调只被触发了一次,而且是age改变触发的,而children数组的改变并没有触发,这就是因为age的改变是一个指针改变,由40指向了41,而children数组仅仅是内容变化了,指针没有改变,所以要想检测大集合的变化,要这么写:

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        self.kobe.age = 41;
        [[self.kobe mutableArrayValueForKey:@"children"] addObject:@"first boy"];
    }
    
    3、KVO的实现原理

    知道了KVO的使用方法,我们不禁要问为什么被观察的属性改变后,观察者的回调就能被触发。接下来,我们就看下KVO的实现原理。

    KVO得益于对runtime的应用,它的实现原理其实是运行时类型的动态转换和setter方法的重写,而观察者回调被触发就是出现在重写的setter方法里。具体的说就是:

    • 当一个对象的某个属性被人观察后,那么虽然该对象在编译时还是原类,但是它在运行时其实已经不是原类了,而是一个新类NSKVONotifying_XXX。这个新类是继承于原类的,系统会自动帮我们创建,而且把对象的isa指针指向这个新类。

    • 新类会重写对象被观察了的属性的setter方法,而不会重写对象没被观察的属性的setter方法,getter方法不会重写。被重写的setter方法里会在调用原setter方法之前和之后让观察者perform一下观察者的回调

    还是上面的例子,我们简单改一下,来理解一下上面这两段话,还是Person类:

    -----------Person.h-----------
    
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, copy) NSString *sex;
    @property (nonatomic, assign) NSInteger age;
    
    @end
    
    
    
    -----------Person.m-----------
    
    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    - (NSString *)description {
        
        Class objectSourceClass = [self class];
        Class objectSourceSuperClass = class_getSuperclass(objectSourceClass);
        Class objectRuntimeClass = object_getClass(self);
        Class objectRuntimeSuperClass = class_getSuperclass(objectRuntimeClass);
        NSLog(@"当前实例编译时所属类:%@,当前实例编译时所属类的父类:%@,当前实例运行时所属类:%@,当前实例运行时所属类的父类:%@", objectSourceClass, objectSourceSuperClass, objectRuntimeClass, objectRuntimeSuperClass);
        
        NSLog(@"当前类的实例方法列表:");
        unsigned int count;
        Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
        for (NSInteger i = 0; i < count; i++) {
            
            Method method = methodList[I];
            NSString *methodName = NSStringFromSelector(method_getName(method));
            NSLog(@"%@", methodName);
        }
        
        IMP setSexImp = class_getMethodImplementation(object_getClass(self), @selector(setSex:));
        IMP sexImp = class_getMethodImplementation(object_getClass(self), @selector(sex));
        NSLog(@"setSex的实现:%p,sex的实现:%p", setSexImp, sexImp);
        
        IMP setAgeImp = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
        IMP ageImp = class_getMethodImplementation(object_getClass(self), @selector(age));
        NSLog(@"setAge的实现:%p,age的实现:%p", setAgeImp, ageImp);
        
        return nil;
    }
    
    @end
    

    然后,我们在ViewController里创建两个Person对象kobe和lebron,并且不观察科比的任何属性,观察詹姆斯的age属性,不观察詹姆斯的sex属性,以此来形成对照:

    -----------ViewController.m-----------
    
    #import "ViewController.h"
    #import "Person.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) Person *kobe;
    @property (nonatomic, strong) Person *lebron;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.kobe = [[Person alloc] init];
        self.kobe.sex = @"male";
        self.kobe.age = 40;
        
        self.lebron = [[Person alloc] init];
        self.lebron.sex = @"male";
        self.lebron.age = 34;
        [self.lebron addObserver:self forKeyPath:@"age" options:(NSKeyValueObservingOptionNew) context:nil];
    }
    
    - (void)dealloc {
        
        [self.lebron removeObserver:self forKeyPath:@"age"];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
        
        NSLog(@"===========>观察者回调:%@", change[NSKeyValueChangeNewKey]);
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        
        // 改变属性的值
        self.lebron.age = 35;
        
        NSLog(@"%@", self.kobe);
        NSLog(@"%@", self.lebron);
    }
    
    @end
    

    当我们点击ViewController的时候,就会打印出Person类description方法里的信息,我们来对科比和詹姆斯做一下对比:

    当前实例所属类 当前类的实例方法列表 setSex和sex的实现 setAge和age的实现
    科比 编译时所属类:Person
    编译时所属类的父类:NSObject

    运行时所属类:Person
    运行时所属类的父类:NSObject
    .cxx_destruct
    description
    setSex:
    sex
    setAge:
    age
    setSex:0x10ec02c60

    sex:0x10ec02c30
    setAge:0x10ec02cc0

    age:0x10ec02ca0
    詹姆斯 编译时所属类:Person
    编译时所属类的父类:NSObject

    运行时所属类:NSKVONotifying_Person
    运行时所属类的父类:Person
    class
    dealloc
    _isKVOA
    setAge:
    setSex:0x10ec02c60

    sex:0x10ec02c30
    setAge:0x10ef55bf4

    age:0x10ec02ca0

    通过对比我们发现:

    • 虽然科比和詹姆斯在编译时都是Person类,但其实在运行时科比照样保持Person类,而詹姆斯已经悄悄的把类型动态转换成NSKVONotifying_Person类了,而且我们也发现新类是继承于Person类的。

    • 再一个,我们从科比和詹姆斯setSex、sex、setAge、age实现的指针可以看出詹姆斯所属的新类仅仅是重写了setAge方法,因为我们仅仅观察了詹姆斯的age属性嘛,其它方法并没有重写,这从类的实例方法列表也可以得到印证。此外,新类还重写class方法,里面做了一堆操作后返回原类,目的是让我们不必知道新类,不知不觉的使用KVO,还重写了dealloc方法来做一些额外的清理工作,并且新增了_isKVOA私有方法供内部使用,我们也可以根据某个类是否有此方法来判断它是不是KVO出来的类。

    这样我们就理解了上面所说的KVO的实现原理。

    4、使用KVO需要注意的地方

    在dealloc方法里remove掉观察者,避免向野指针发送消息导致程序崩掉

    为什么要remove?这个其实和通知是一个道理,如果不释放的话,观察者一旦释放了,而且没被置为nil,就会导致向野指针发消息,就会崩掉。

    5、KVO的应用场景

    KVO的应用场景主要就是用来观察某个对象属性的变化,遇到这这种需求就可以用,比如观察webView的加载进度展示进度条等等,我平常用的好像不太多,待慢慢发掘。

    相关文章

      网友评论

        本文标题:设计模式第三篇、观察者设计模式

        本文链接:https://www.haomeiwen.com/subject/voyxgftx.html