美文网首页iOS Developer程序员iOS Developer
ReactiveCocoa 前奏 KVC. KVO. NSNot

ReactiveCocoa 前奏 KVC. KVO. NSNot

作者: 漂泊海上的大土豆 | 来源:发表于2017-04-01 09:19 被阅读54次

    KVC

    KVC (key-value observing) 是基于 NSKeyValueCoding 的一个非正式 Protocol,它提供了一种无需通过SetterGetter间接访问对象属性的方式。

    常用的方法:

    - (id)valueForKey:(NSString *)key;

    - (void)setValue:(id)value 
              forKey:(NSString *)key;
    

    Tips:

    • key 值错误或不存在会导致 crash,为了防止这种潜在的风险可以重写 valueForUndefinedKey 方法来截获错误。
    • key 值是可以嵌套的,所以可以根据 keyPath 层层寻找,比如 Demo.Person.name

    KVO

    KVOkey-value observing) 键值观察,观察一个对象属性的变化,观察者在键值改变时会得到通知。

    1.当一个 object 有观察者时,动态创建这个 object 的类的子类。
    2.对于每个被观察的 property ,重写其 set 方法。
    3.在重写的set方法中调用 - willChangeValueForKey:- didChangeValueForKey: 通知观察者。
    4.当一个 property 没有观察者时,删除重写的方法。
    5.当没有 observer 观察任何一个 property 时,删除动态创建的子类。

    Apple 官方解释

    常用方法:

    1.注册一个观察者

    - (void)addObserver:(NSObject *)anObserver
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(void *)context
    

    2.当观察者发生变化会得到此方法的响应

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    

    3.然后你还要记得移除观察者

    - (void)removeObserver:(NSObject *)anObserver
                forKeyPath:(NSString *)keyPath
    

    如果我们观察很多个键的话,开销可能会变得明显,如果我们在实现一个类的时候把它自己注册为观察者的话,一定要传入一个这个类唯一的 context。比如:

    static int const PrivateKVOContext;

    可以写在这个类 .m 文件的顶端,然后我们像这样调用 API 并传入 PrivateKVOContext 的指针:

    [otherObject addObserver:self 
                  forKeyPath:@"someKey" 
                     options:someOptions 
                     context:&PrivateKVOContext];
    
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    {
        if (context == &PrivateKVOContext) {
            // 这里写相关的观察代码
        } else {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    

    这将确保我们写的子类都是正确的。如此一来,子类和父类都能安全的观察同样的键值而不会冲突。否则我们将会碰到难以 debug 的奇怪行为

    Tips:

    • KVO 是同步的 ,一对一的观察对象的属性变化并做出反应。

    KVO 是同步的,并且发生与所观察的值发生变化的同样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 KVO 通知。

    只要我们在单一线程上面运行, KVO 能保证所有 exchangeRate 的观察者在 setter 方法返回前被通知到。 其次,如果某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用之前, exchangeRateaccessor 方法都会返回同样的值。

    • 被观察属性必须是通过 kvc 方式修改的,否则观察者不会收到通知。

    NSNotification

    一个 NSNotificationCenter 对象提供了在程序中广播消息的机制,它实质上就是一个通知分发表。这个分发表负责维护为各个通知注册的观察者,并在通知到达时,去查找相应的观察者,将通知转发给他们进行处理。

    使用方式:

    每一个程序都有一个默认的通知中心,并提供了单例方法来获取它,如果不是出于必要,使用默认的就很方便。

    + (NSNotificationCenter *)defaultCenter

    通知中心添加观察者

    - (void)addObserver:(id)notificationObserver     // 通知的观察者
               selector:(SEL)notificationSelector    // 处理通知的回调
                   name:(NSString *)notificationName // 通知名
                 object:(id)notificationSender       // 通知的发送对象
    

    举个🌰

    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleNotification:)
                                                     name:nil
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] postNotificationName:@"post"
                                                            object:nil];
    }
    - (void)handleNotification:(NSNotification *)notification {
        NSLog(@"notification = %@", notification.name);
    }
    @end
    

    1.notificationObserver 不能为 nil。
    notificationSelector 回调方法有且只有一个参数(NSNotification对象)。

    2.如果 notificationName 为 nil,则会接收所有的通知(如果 notificationSender 不为空,则接收所有来自于 notificationSender 的所有通知)。

    3.如果 notificationSender 为 nil,则会接收所有 notificationName 定义的通知。否则,接收由notificationSender发送的通知。

    4.监听同一条通知的多个观察者,在通知到达时,它们执行回调的顺序是不确定的,所以我们不能去假设操作的执行会按照添加观察者的顺序来执行。

    在 iOS 4.0 之后 NSNotificationCenter 有了 block 的添加观察者方式

    使用方式:

    - (id<NSObject>)addObserverForName:(NSString *)name        // 通知名
                                object:(id)obj                 // 通知发送的对象
                                 queue:(NSOperationQueue *)queue // 接收通知的线程
                            usingBlock:(void (^)(NSNotification *note))block
    

    举个🌰

    [[NSNotificationCenter defaultCenter] addObserverForName:@"postOne"
                                                          object:nil
                                                           queue:[NSOperationQueue mainQueue]
                                                      usingBlock:^(NSNotification * _Nonnull note) {
                                                          NSLog(@"receive thread = %@", [NSThread currentThread]);
                                                      }];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"post thread = %@", [NSThread currentThread]);
            [[NSNotificationCenter defaultCenter] postNotificationName:@"postOne"
                                                                object:nil];
        });
    

    前者使用一个现存的对象作为观察者,而这个方法会创建一个匿名的对象作为观察者(即方法返回的id<NSObject>对象),这个匿名对象会在指定的队列(queue)上去执行我们的 block。

    1.name 和 obj 为 nil 时的情形与前面一个方法是相同的。

    2.如果 queue 为 nil,则消息是默认在 post 线程中同步处理,即通知的 post 与转发是在同一线程中。

    3.block 块会被通知中心拷贝一份(执行 copy 操作),以在堆中维护一个 block 对象,直到观察者被从通知中心中移除。所以,应该特别注意在 block 中使用外部对象,避免出现对象的循环引用。

    4.如果一个给定的通知触发了多个观察者的 block 操作,则这些操作会在各自的 Operation Queue 中被并发执行。所以我们不能去假设操作的执行会按照添加观察者的顺序来执行。

    5.该方法会返回一个表示观察者的对象,记得在不用时释放这个对象。

    无论使用哪种方式, 一定要记得移除观察者,block 要一如既往地注意循环引用问题

    - (void)removeObserver:(id)notificationObserver
    - (void)removeObserver:(id)notificationObserver name:(NSString *)notificationName object:(id)notificationSender
    

    前一个方法会将 notificationObserver 从通知中心中移除,这样 notificationObserver 就无法再监听任何消息。而后一个会根据三个参数来移除相应的观察者。

    1.由于注册观察者时(不管是哪个方法),通知中心会维护一个观察者的弱引用,所以在释放对象时,要确保移除对象所有监听的通知。否则,可能会导致程序崩溃或一些莫名其妙的问题。

    2.对于第二个方法,如果 notificationName 为 nil,则会移除所有匹配 notificationObserver 和notificationSender 的通知,同理 notificationSender 也是一样的。而如果 notificationName 和notificationSender 都为 nil,则其效果就与第一个方法是一样的了。

    3.–removeObserver: 适合于在类的 dealloc 方法中调用,这样可以确保将对象从通知中心中清除;
    而在 viewWillDisappear: 这样的方法中,则适合于使用 -removeObserver:name:object: 方法,以避免不知情的情况下移除了不应该移除的通知观察者。

    4.每次调用 addObserver 时,都会在通知中心重新注册一次,即使是同一对象监听同一个消息,而不是去覆盖原来的监听,如果同一对象多次注册了这个通知的观察者,则会收到多个通知。

    Post postNotification

    使用方式:

    - postNotification:
    – postNotificationName:object:
    – postNotificationName:object:userInfo:
    

    1.通知的发送与处理是同步的,在某个地方post一个消息时,会等到所有观察者对象执行完处理操作后,才回到post的地方,继续执行后面的代码。

    2.可以根据需要指定通知的发送者(object)并附带一些与通知相关的信息(userInfo)。

      • postNotification: 的参数不能为空,否则会引发一个异常

    举个🌰

     [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleNotification:)
                                                     name:nil
                                                   object:nil];
        
        [[NSNotificationCenter defaultCenter] postNotificationName:@"post"
                                                            object:nil];
        
        NSLog(@"continue");
    
    2017-03-24 10:51:45.145 simpleRacDemo[11432:6734903] notification = post
    2017-03-24 10:51:45.145 simpleRacDemo[11432:6734903] continue
    

    Tips

    • 一对多,多个对象同时对一个通知做出反应
    • 在层级比较深的时候使用通知比较方便,但是全局只有一个,虽然方便但不要乱用
    • 只负责发出通知,不会检查能否被观察者正确接收并处理
    • 有了观察就一定要记得移除
    • 通知的发送和处理是在同一个线程中。使用 -addObserverForName:object:queue:usingBlock: 务必处理好内存问题,避免出现循环引用。NSNotificationCenter 是线程安全的,但并不意味着在多线程环境中不需要关注线程安全问题。不恰当的使用仍然会引发线程问题。
    • 添加观察者时,通知中心没有对观察者做 retain 操作,即不会使观察者的引用计数加1,通知中心维护观察者使用的是 unsafe_unretained,之所以使用 unsafe_unretained,而不使用 weak,是为了兼容老版本的系统。

    Delegate

    Delegate 本身就是一种设计模式,简而言之就是委托者将一件事情交给被委托者来做。

    使用方式:

    代理的使用在 iOS 开发中 与 protocol(协议)密不可分,通常在协议中定义好你期望委托对象实现的方法,@required 是被委托者必须实现的, @optional 则是可选择的。

    直接举个🌰

    这是一个个人中心的 headerView,定义了很多需要委托对象可选择来实现的方法。

    @property (nonatomic, weak) id <MyCenterTableHeaderViewDelegate> delegate;这里对 delegate 一定要 weak弱引用,避免互相持有循环引用的问题。

    @class MyCenterTableHeaderView;
    
    @protocol MyCenterTableHeaderViewDelegate <NSObject>
    
    @required // 必须实现
    @optional // 可选择实现
    - (void)myCenterTableHeaderViewChatBtnClicked:(MyCenterTableHeaderView *)view;      // 聊天按钮点击
    - (void)myCenterTableHeaderViewFollowTopBtnClicked:(MyCenterTableHeaderView *)view; // 关注用户按钮点击
    - (void)myCenterTableHeaderViewFollowBtnClicked:(MyCenterTableHeaderView *)view;    // 关注数点击
    - (void)myCenterTableHeaderViewSnsBtnClicked:(MyCenterTableHeaderView *)view;       // 动态数点击
    - (void)myCenterTableHeaderViewFansBtnClicked:(MyCenterTableHeaderView *)view;      // 粉丝数点击
    - (void)myCenterTableHeaderViewAvatarBtnClicked:(MyCenterTableHeaderView *)view;    // 头像点击
    - (void)myCenterTableHeaderViewEditBtnClicked:(MyCenterTableHeaderView *)view;      // 编辑信息点击
    
    @end
    
    
    
    @interface MyCenterTableHeaderView : UIView
    
    @property (nonatomic, weak) id <MyCenterTableHeaderViewDelegate> delegate;
    
    @end
    

    方法的实现,加强了安全性判断,养成良好的代码习惯,防止调用没有实现的方法而崩溃。

    #pragma mark - Control Events
    - (void)clickedAvatarImgV:(UITapGestureRecognizer *)sender {
        if ([self.delegate respondsToSelector:@selector(myCenterTableHeaderViewAvatarBtnClicked:)]) {
            [self.delegate myCenterTableHeaderViewAvatarBtnClicked:self];
        }
    }
    

    接下来轮到被委托者来实现代理

    首先实现代理对应的协议

    @interface MyCenterViewController () <MyCenterTableHeaderViewDelegate>
    
    ...
    
    @end
    

    这是 MyCenterTableHeaderView 的懒加载方式

    我们需要关注的是这一行代码 [_mainTableViewHeader setDelegate:self]; 被委托者就是 MyCenterViewController 它来实现 MyCenterTableHeaderView 的委托。

    - (MyCenterTableHeaderView *)mainTableViewHeader {
        if (_mainTableViewHeader == nil) {
            ...
            [_mainTableViewHeader setDelegate:self];
        }
        
        return _mainTableViewHeader;
    }
    

    部分方法的实现,这里我们将委托者本身 MyCenterTableHeaderView 作为参数传递了出来,当然也可以根据具体的业务逻辑传递你期望的参数。

    #pragma mark   MyCenterTableHeaderViewDelegate
    - (void)myCenterTableHeaderViewAvatarBtnClicked:(MyCenterTableHeaderView *)view {// 头像
        ...
    }
    

    Tips:

    • delegate 语法严格,使用规范,对没有实现的方法会出现编译警告/错误,可以利用方法传递参数,可以通过不同的协议实现多个代理。
    • 相对的,代理的使用需要些很多代码,协议定义,方法实现,代理实现等。
    • 对于传递参数来说,如果页面层级比较深就很麻烦,不如通知方便。

    参考资料:

    相关文章

      网友评论

        本文标题:ReactiveCocoa 前奏 KVC. KVO. NSNot

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