1.KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。可以用来监听对象属性改变。
使用步骤:1. 注册,指定被观察者的属性,2. 实现回调方法3. 移除观察
小实例1:假设一个场景,股票的价格显示在当前屏幕上,当股票价格更改的时候,实时显示更新其价格。
点击按钮,label实时更新label上的值。
#import"ViewController.h"
#import"test.h"
@interfaceViewController()
{
UILabel*label;
test*testKVO;
}
@end
@implementationViewController
- (void)viewDidLoad {
[superviewDidLoad];
testKVO= [[testalloc]init];
[testKVOsetValue:@"test"forKey:@"name"];
[testKVOsetValue:@"20"forKey:@"price"];
//添加observers栏监听price的变化
[testKVOaddObserver:selfforKeyPath:@"price"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];
label= [[UILabelalloc]initWithFrame:CGRectMake(100,100,100,30)];
label.textColor= [UIColorredColor];
label.text= [testKVOvalueForKey:@"price"];
[self.viewaddSubview:label];
UIButton* b = [UIButtonbuttonWithType:UIButtonTypeCustom];
b.backgroundColor= [UIColorblueColor];
b.frame=CGRectMake(0,0,100,30);
[baddTarget:selfaction:@selector(buttonAction)forControlEvents:UIControlEventTouchUpInside];
[self.viewaddSubview:b];
}
- (void)buttonAction{
[testKVOsetValue:@"100"forKey:@"price"];
}
//回调方法,即监听到price的改变后走这方法
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{
if([keyPathisEqualToString:@"price"]){
label.text= [testKVOvalueForKey:@"price"];
}
}
- (void)dealloc{
[testKVOremoveObserver:selfforKeyPath:@"price"];
}
@end
小实例2:监听scrollView的contentOffset属性,来完成用户滚动时动态改变某些控件的属性实现效果,包括渐变导航栏、下拉刷新控件,隐藏导航栏等效果。
#import"ViewController.h"
@interfaceViewController()
@property(weak,nonatomic)IBOutletUINavigationBar*nav;
@end
@implementationViewController
- (void)viewDidLoad {
[superviewDidLoad];
//NSKeyValueObservingOptionNew把更改之前的值提供给处理方法
//
//NSKeyValueObservingOptionOld把更改之后的值提供给处理方法
//
//NSKeyValueObservingOptionInitial把初始化的值提供给处理方法,一旦注册,立马就会调用一次。通常它会带有新值,而不会带有旧值。
//
//NSKeyValueObservingOptionPrior分2次调用。在值改变之前和值改变之后。
[self.tableViewaddObserver:selfforKeyPath:@"contentOffset"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:NULL];
}
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{
CGFloatoffset =self.tableView.contentOffset.y;
CGFloatdelta = offset /64.f+1.f;
delta =MAX(0, delta);
self.nav.alpha=MIN(1, delta);
}
@end
2.Notification
通知可以是一对多个对象,并且不再需要返回值。通知是个对象,它封装了通知发送者想要传递给监听者的的信息。它包含三个属性:
@property(readonly,copy)NSString*name;
@property(nullable,readonly,retain)idobject;
@property(nullable,readonly,copy)NSDictionary*userInfo;
name:通知的名称,用来标示一个通知,一般为字符串
object:任意想要携带的对象,通常为发送者自己
userInfo:附加信息
通知中心NSNotificationCenter:
NSNotificationCenter 单进程通知管理
NSDistributedNotificationCenter 一台机器中多个进程的通知管理
通知中心是整个通知机制的关键所在,它管理着监听者的注册和注销,通知的发送和接收。通知中心维护着一个通知的分发表,把所有发送者发送的通知,转发给对应的监听者们。每一个iOS程序都有一个唯一的通知中心,你不必自己去创建一个,它是一个单例,通过[NSNotificationCenter defaultCenter]方法获取。
1.通知中心分发给观察者处理采用同步机制,也就是说,当某一对象发送一个通知时,会一直阻塞在发送方法内,直到通知中心将该通知分发给所有观察者并且全部成功处理返回后,发送者才能执行其所在线程内的后续代码。如果需要异步发送通知,可以使用文章后面的“通知队列”。
2.在多线程的应用程序中,通知总是在发送的线程中传送,这个线程可能不同于观察者注册所在的线程。
发送通知方法:
-(void)postNotification:(NSNotification *)notification;
-(void)postNotificationName:(NSString *)aName object:(id)anObject;
-(void)postNotificationName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary*)aUserInfo;
注册通知方法:
-(void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;
--(void)removeObserver:(id)observer;
-(void)removeObserver:(id)observer name:(NSString *)aName object:(id)anObject;
小实例:比如说监听键盘的收起弹出:
#import"ViewController.h"
@interfaceViewController()
@property(weak,nonatomic)IBOutletUITextField*text;
@end
@implementationViewController
- (void)viewDidLoad {
[superviewDidLoad];
self.view.backgroundColor= [UIColorblueColor];
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(Show:)name:UIKeyboardDidShowNotificationobject:nil];
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(Hide:)name:UIKeyboardDidHideNotificationobject:nil];
//增加手势,点击弹回
UITapGestureRecognizer*tap = [[UITapGestureRecognizeralloc]initWithTarget:selfaction:@selector(click:)];
[self.viewaddGestureRecognizer:tap];
}
- (void)click:(UITapGestureRecognizer*)tap{
[self.viewendEditing:YES];
}
-(void)Show:(NSNotification*)noti{
//NSLog(@"%@",noti);
//打印结果
/*{name = UIKeyboardDidShowNotification; userInfo = {
UIKeyboardAnimationCurveUserInfoKey = 7;
UIKeyboardAnimationDurationUserInfoKey = "0.25";
UIKeyboardBoundsUserInfoKey = "NSRect: {{0, 0}, {375, 258}}";
UIKeyboardCenterBeginUserInfoKey = "NSPoint: {187.5, 796}";
UIKeyboardCenterEndUserInfoKey = "NSPoint: {187.5, 538}";
UIKeyboardFrameBeginUserInfoKey = "NSRect: {{0, 667}, {375, 258}}";
UIKeyboardFrameEndUserInfoKey = "NSRect: {{0, 409}, {375, 258}}";
UIKeyboardIsLocalUserInfoKey = 1;
}}
*/
CGRectrect = [noti.userInfo[UIKeyboardFrameEndUserInfoKey]CGRectValue];
CGRecttemp =self.view.frame;
temp.origin.y= -rect.size.height;
[UIViewanimateWithDuration:[noti.userInfo[UIKeyboardAnimationDurationUserInfoKey]doubleValue]animations:^{
self.view.frame= temp;
}];
}
-(void)Hide:(NSNotification*)noti{
//NSLog(@"%@",noti);
[UIViewanimateWithDuration:[noti.userInfo[UIKeyboardAnimationDurationUserInfoKey]doubleValue]animations:^{
self.view.frame=[UIScreenmainScreen].bounds;
}];
}
- (void)dealloc{
[[NSNotificationCenterdefaultCenter]removeObserver:self];
}
@end
通知队列
通知队列(Notification queue)更像是通知中心的缓冲区,通知队列通常以先进先出(FIFO)的顺序管理通知。当一个通知上升到队列的前面时,队列就将它发送给通知中心,通知中心随后将它派发给所有注册为观察者的对象。
每个线程都有一个缺省的通知队列[NSNotificationQueue defaultQueue],与进程的缺省通知中心相关联。开发者可以自己创建通知队列,并且每个通知中心和线程都可以有多个队列。
向队列投递一个异步通知方法如下:
-(void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;-(void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle coalesceMask:(NSUInteger)coalesceMask forModes:(NSArray *)modes;
NSNotificationQueue类为Foundation Kit的通知机制增加了两个重要的特性:合并通知和异步发送
异步发送
向通知队列发送通知可以有三种风格:NSPostASAP、NSPostWhenIdle、和NSPostNow,这些风格将在接下来的部分中进行说明。
1.尽快发送
以NSPostASAP风格进入队列的通知会在运行循环的当前迭代完成时被发送给通知中心,如果当前运行循环模式和请求的模式相匹配的话(如果请求的模式和当前模式不同,则通知在进入请求的模式时被发出)。由于运行循环在每个迭代过程中可能进行多个调用分支(callout),所以在当前调用分支退出及控制权返回运行循环时,通知可能被分发,也可能不被分发。其它的调用分支可能先发生,比如定时器或由其它源触发了事件,或者其它异步的通知被分发了。
开发者通常可以将NSPostASAP风格用于开销昂贵的资源,比如显示服务器。如果在运行循环的一个调用分支过程中有很多客户代码在窗口缓冲区中进行描画,在每次描画之后将缓冲区的内容刷新到显示服务器的开销是很昂贵的。在这种情况下,每个draw...方法都会将诸如“FlushTheServer” 这样的通知排入队列,并指定按名称和对象进行合并,以及使用NSPostASAP风格。结果,在运行循环的最后,那些通知中只有一个被派发,而窗口缓冲区也只被刷新一次。
2.空闲时发送
以NSPostWhenIdle风格进入队列的通知只在运行循环处于等待状态时才被发出。在这种状态下,运行循环的输入通道中没有任何事件,包括定时器和异步事件。以NSPostWhenIdle风格进入队列的一个典型的例子是当用户键入文本、而程序的其它地方需要显示文本字节长度的时候。在用户输入每一个字符后都对文本输入框的尺寸进行更新的开销是很大的(而且不是特别有用),特别是当用户快速输入的时候。在这种情况下,Cocoa会在每个字符键入之后,将诸如“ChangeTheDisplayedSize”这样的通知进行排队,同时把合并开关打开,并使用NSPostWhenIdle风格。当用户停止输入的时候,队列中只有一个“ChangeTheDisplayedSize”通知(由于合并的原因)会在运行循环进入等待状态时被发出,显示部分也因此被刷新。请注意,运行循环即将退出(当所有的输入通道都过时的时候,会发生这种情况)时并不处于等待状态,因此也不会发出通知。
3.立即发送
以NSPostNow风格进入队列的通知会在合并之后,立即发送到通知中心。开发者可以在不需要异步调用行为的时候 使用NSPostNow风格(或者通过NSNotificationCenter的postNotification:方法来发送)。在很多编程环境下,我们不仅允许同步的行为,而且希望使用这种行为:即开发者希望通知中心在通知派发之后返回,以便确定观察者对象收到通知并进行了处理。当然,当开发者希望通过合并移除队列中类似的通知时,应该用enqueueNotification...方法,且使用NSPostNow风格,而不是使用postNotification:方法。
合并通知
一些情况下,某一事件一旦发生,开发者可能只想要至少发送一次通知,即使该事件可能频繁多次发生。此时即可采用合并通知,合并是把和刚进入队列的通知相类似的其它通知从队列中移除的过程。如果一个新的通知和已经在队列中的通知相类似,则新的通知不进入队列,而所有类似的通知(除了队列中的第一个通知以外)都被移除。但是,为安全起见,开发者不应该完全依赖于这个特殊的合并行为。
分发通知到指定线程
一般通知中心只在投递所发生的线程内分发通知,分布式通知中心在主线程内分发通知。但是有时候,开发者可能需要将通知放在指定线程内以自己的通知中心来分发处理,例如一个对象运行在后台线程里并监听像窗口关闭这个的UI触发事件时,开发者可能想要在后台线程里接收该通知而非主线程,这种情况下,开发者需要捕获到在默认线程中传递的通知,并且将该通知重定向到恰当的线程中去。
重定向通知方法是,通过使用定制的通知队列(不是NSNotificationQueue)在非目标线程中捕获所有通知并且将它们重新投递给正确的线程。流程如下,开发者先注册一个通知,当通知到达时,验证其是否应该在当前线程中被处理,如果不是,需要将此通知存入队列中,并且发送一个信号给目标线程以示有新通知需要处理,目标线程收到信号后,从该通知队列中取出并且处理通知。
下面是官方文档给出的基本思路
观察者对象需要实现的变量及delegate
@interfaceMyThreadedClass: NSObject/* Threaded notification support. */@propertyNSMutableArray *notifications;@propertyNSThread *notificationThread;@propertyNSLock *notificationLock;@propertyNSMachPort *notificationPort;-(void)setUpThreadingSupport;-(void)processNotification:(NSNotification*)notification;@end
注册通知之前先初始化这些变量
- (void) setUpThreadingSupport {if(self.notifications) {return; }self.notifications = [[NSMutableArrayalloc] init];self.notificationLock = [[NSLockalloc] init];self.notificationThread = [NSThreadcurrentThread];self.notificationPort = [[NSMachPortalloc] init]; [self.notificationPort setDelegate:self]; [[NSRunLoopcurrentRunLoop] addPort:self.notificationPort}
在NSMacPortDelegate回调中,对线程中捕获到的消息进行处理
- (void) handleMachMessage:(void*)msg { [self.notificationLock lock];while([self.notifications count]) {NSNotification*notification = [self.notifications objectAtIndex:0]; [self.notifications removeObjectAtIndex:0]; [self.notificationLock unlock]; [selfprocessNotification:notification]; [self.notificationLock lock]; }; [self.notificationLock unlock]; }
检测消息是否应该在此线程被处理,如果是,正常处理,否则将其入队并触发信号
- (void)processNotification:(NSNotification*)notification {if([NSThreadcurrentThread] != notificationThread) {// Forward the notification to the correct thread.[self.notificationLock lock]; [self.notifications addObject:notification]; [self.notificationLock unlock]; [self.notificationPort sendBeforeDate:[NSDatedate] components:nilfrom:nilreserved:0]; }else{// Process the notification here;}}
最后,在真正需要处理通知的目标线程里注册通知,注册之前需要先调用setupThreadingSupport以初始化通知属性,然后像一般注册一样指定通知响应方法即可。
[self setupThreadingSupport];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(processNotification:) name:@"NotificationName"object:nil];
3.delegate:委托代理模式这个就不讲了
4.block:可以看我的文章《Block 由浅入深》
这四者的区别,及各自的优缺点
delegate的优势:
1.很严格的语法,所有能响应的时间必须在协议中有清晰的定义
2.因为有严格的语法,所以编译器能帮你检查是否实现了所有应该实现的方法,不容易遗忘和出错
3.使用delegate的时候,逻辑很清楚,控制流程可跟踪和识别
4.在一个controller中可以定义多个协议,每个协议有不同的delegate
5.没有第三方要求保持/监视通信过程,所以假如出了问题,那我们可以比较方便的定位错误代码。
6.能够接受调用的协议方法的返回值,意味着delegate能够提供反馈信息给controller
delegate的缺点:
需要写的代码比较多
有一个“Notification Center”的概念,他是一个单例对象,允许当事件发生的时候通知一些对象,满足控制器与一个任意的对象进行通信的目的,这种模式的基本特征就是接收到在该controller中发生某种事件而产生的消息,controller用一个key(通知名称),这样对于controller是匿名的,其他的使用同样地key来注册了该通知的对象能对通知的事件作出反应。
notification的优势:
1.不需要写多少代码,实现比较简单
2.一个对象发出的通知,多个对象能进行反应,一对多的方式实现很简单
缺点:
1.编译期不会接茬通知是否能被正确处理
2.释放注册的对象时候,需要在通知中心取消注册
3.调试的时候,程序的工作以及控制流程难跟踪
4.需要第三方来管理controller和观察者的联系
5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况
6.通知发出后,发出通知的对象不能从观察者获得任何反馈。
KVO
KVO是一个对象能观察另一个对象属性的值,前两种模式更适合一个controller和其他的对象进行通信,而KVO适合任何对象监听另一个对象的改变,这是一个对象与另外一个对象保持同步的一种方法。KVO只能对属性做出反应,不会用来对方法或者动作做出反应。
优点:
1.提供一个简单地方法来实现两个对象的同步
2.能对非我们创建的对象做出反应
3.能够提供观察的属性的最新值和先前值
4.用keypaths 来观察属性,因此也可以观察嵌套对象
缺点:
1.观察的属性必须使用string来定义,因此编译器不会出现警告和检查
2.对属性的重构将导致观察不可用
3.复杂的“if”语句要求对象正在观察多个值,这是因为所有的观察都通过一个方法来指向
KVO有显著的使用场景,当你希望监视一个属性的时候,我们选用KVO
而notification和delegate有比较相似的用处,
当处理属性层的消息的事件时候,使用KVO,其他的尽量使用delegate,除非代码需要处理的东西确实很简单,那么用通知很方便
如何手动通知 KVO
重写Controller里面某个属性的setter方法,联动给View赋值,使用Controller监控Model里面某个值的变化,在controller的dealloc函数中用一行代码了结:removeObserver。