ReactiveCocoa 前奏 KVC. KVO. NSNotification. Delegate 梳理

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

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容