ReactiveCocoa 前奏 KVC. KVO. NSNot
KVC
KVC
(key-value observing
) 是基于NSKeyValueCoding
的一个非正式Protocol
,它提供了一种无需通过Setter
,Getter
间接访问对象属性的方式。
常用的方法:
- (id)valueForKey:(NSString *)key;
- (void)setValue:(id)value
forKey:(NSString *)key;
Tips:
- key 值错误或不存在会导致 crash,为了防止这种潜在的风险可以重写 valueForUndefinedKey 方法来截获错误。
- key 值是可以嵌套的,所以可以根据 keyPath 层层寻找,比如 Demo.Person.name
KVO
KVO
(key-value observing
) 键值观察,观察一个对象属性的变化,观察者在键值改变时会得到通知。
1.当一个
object
有观察者时,动态创建这个object
的类的子类。
2.对于每个被观察的property
,重写其set
方法。
3.在重写的set方法中调用- willChangeValueForKey:
和- didChangeValueForKey:
通知观察者。
4.当一个property
没有观察者时,删除重写的方法。
5.当没有observer
观察任何一个property
时,删除动态创建的子类。
常用方法:
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...
被调用之前,exchangeRate
的accessor
方法都会返回同样的值。
- 被观察属性必须是通过 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
语法严格,使用规范,对没有实现的方法会出现编译警告/错误,可以利用方法传递参数,可以通过不同的协议实现多个代理。 - 相对的,代理的使用需要些很多代码,协议定义,方法实现,代理实现等。
- 对于传递参数来说,如果页面层级比较深就很麻烦,不如通知方便。