通知/KVO/代理
三者优缺点比较
delegate
- 优势 :
1.非常严格的语法。所有监听到的事件必须是在delegate协议中有清晰的定义。
2.如果delegate中的一个方法没有实现那么就会出现编译警告/错误 3.协议必须在controller的作用域范围内定义
4.在一个应用中的控制流程是可跟踪的并且是可识别的;
5.在一个控制器中可以定义多个不同的协议,每个协议有不同的delegate
6.没有第三方对象要求保持/监视通信过程。
7.能够接收调用的协议方法的返回值。这意味着delegate能够提供反馈信息给controller
8.经常被用在存在父子关系的对象之间通信,例如控制器和控制器的view(自己加的理解) - 缺点
1.需要定义很多代码:1.协议定义;2.controller的delegate属性;3.在delegate本身中实现delegate方法定义
2.在释放代理对象时,需要小心的将delegate改为nil。一旦设定失败,那么调用释放对象的方法将会出现内存crash
3.在一个controller中有多个delegate对象,并且delegate是遵守同一个协议,但还是很难告诉多个对象同一个事件,不过有可能。
4.经常用在一对一的通信。(不知道是缺点还是优点,只能算是特点)(自己加的理解)
notification
- 优势 :
1.不需要编写多少代码,实现比较简单
2.对于一个发出的通知,多个对象能够做出反应,即一对多的方式实现简单
3.controller能够传递context对象(dictionary),context对象携带了关于发送通知的自定义的信息 - 缺点 :
1.在编译期不会检查通知是否能够被观察者正确的处理;
2.在释放注册的对象时,需要在通知中心取消注册;
3.在调试的时候应用的工作以及控制过程难跟踪;
4.需要第三方对象来管理controller与观察者对象之间的联系;
5.controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况;
6.通知发出后,controller不能从观察者获得任何的反馈信息(相比较delegate)。
KVO
- 优势
1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
3.能够提供观察的属性的最新值以及先前值;
4.用key paths来观察属性,因此也可以观察嵌套对象;
5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
6.可以一对多。 - 缺点
1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
2.对属性重构将导致我们的观察代码不再可用;
3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向;
4.当释放观察者时不需要移除观察者。
delegate与NSNotification
通知:广播机制,可以将一个通知发送给多个监听者。随处都可以添加订阅
代理:一对一,每个对象的代理却只能有一个。用代理代码分布结构更加清晰
- 效率 肯定是delegate比NSNotification高。
delegate方法比notification更加直接,最典型的特征是,delegate方法往往需要关注返回值, 也就是delegate方法的结果。比如-windowShouldClose:,需要关心返回的是yes还是no。所以delegate方法往往包含 should这个很传神的词。也就是好比你做我的delegate,我会问你我想关闭窗口你愿意吗?你需要给我一个答案,我根据你的答案来决定如何做下一 步。相反的,notification最大的特色就是不关心接受者的态度, 我只管把通告放出来,你接受不接受就是你的事情,同时我也不关心结果。所以notification往往用did这个词汇,比如 NSWindowDidResizeNotification,那么NSWindow对象放出这个notification后就什么都不管了也不会等待接 受者的反应。
KVO和NSNotification
和delegate一样,KVO和NSNotification的作用也是类与类之间的通信,与delegate不同的是
1)这两个都是负责发出通知,剩下的事情就不管了,所以没有返回值;
2)delegate只是一对一,而这两个可以一对多。这两者也有各自的特点。
通知
1 | //接受消息的类 |
多线程下的通知
NSNotificationCenter消息的接受线程是基于发送消息的线程的,也就是同步的。
KVO相关
KVO
使用场景:当需要检测其他类的属性值变化,但又不想被观察的类知道,就可以使用KVO了。
很适合实现 mode 模型和 view 视图之间的通讯。触发
如果赋值没有通过 setter 方法或者 KVC,而是直接修改属性对应的成员变量,例如:仅调用 _name = @”newName”,这时是不会触发 KVO 机制,更加不会调用回调方法的。
所以使用 KVO 机制的前提是遵循 KVO 的属性设置方式来变更属性值。底层实现原理
系统实现KVO有以下几个步骤:
1.当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类NSKVONotifying_A。该类继承自对象A的本类。
2.在派生类NSKVONotifying_A中重写类A的setter方法,NSKVONotifying_A类在被重写的setter方法中实现通知机制。setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
3.类NSKVONotifying_A会重写 class方法,将自己伪装成类A。类NSKVONotifying_A还会重写dealloc方法释放资源。
4.系统将所有指向类A对象的isa指针指向类NSKVONotifying_A的对象。
KVC和KVO都属于键值编程而且底层实现机制都是isa-swizzing,依赖于Runtime机制。当观察者被注册为一个对象的属性的观察对象的isa指针被修改,指向一个中间类,而不是在真实的类。其结果是,isa指针的值并不一定反映实例的实际类。所以不能依靠isa指针来确定对象是否是一个类的成员。应该使用class方法来确定对象实例的类。
isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。
KVO与线程
KVO的响应和KVO观察的值变化是在一个线程上的,所以,大多数时候,不要把KVO与多线程混合起来。除非能够保证所有的观察者都能线程安全的处理KVO。手动KVO
首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;1
2
3
4
5
6
7
8
9
10
11
12
13
14-(void)setName:(NSString *)newName{
[self willChangeValueForKey:@"name"]; //KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; //调用父类的存取方法
[self didChangeValueForKey:@"name"]; //KVO 在调用存取方法之后总调用
}
-(void)setAge:(NSUInteger)age{
if (age < 22) {
return;
}
[self willChangeValueForKey:@age];
_age = age;
[self didChangeValueForKey:@age];
}
其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。1
2
3+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
KVC
- 可以通过字符串的名字(key)来访问类属性的机制。而不是通过调用Setter、Getter方法访问。
- 可以访问私有成员变量的值,可以间接修改私有成员变量的值。可以修改readonly属性。
- 关键方法定义在 NSKeyValueCodingProtocol
KVC支持类对象和内建基本数据类型。
使用:
获取值
valueForKey: 传入NSString属性的名字。
valueForKeyPath: 属性的路径,xx.xx
valueForUndefinedKey 默认实现是抛出异常,可重写这个函数做错误处理
修改值
setValue:forKey:
setValue:forKeyPath:
setValue:forUnderfinedKey:
setNilValueForKey: 对非类对象属性设置nil时调用,默认抛出异常。
- 解析json
重写2个方法 和 一个处理类型的方法;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26#pragma mark ---- 利用kvc解析json,一定要实现下面的两个方法!(属性名字用这个方法)
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
//找到和属性不一致名字的key,然后赋值给self的属性
if ([key isEqualToString:@"description"]) {
// self.descriptionStr = value; // 不推荐使用
[self setValue:value forKey:@"descriptionStr"]; // 推荐
}
}
-(id)valueForUndefinedKey:(NSString *)key{
return nil;
}
#pragma mark --- 对于处理“类型”,就用下面的方法
// 处理特殊 ----(类型)例如:NSNumber--> NSString
- (void)setValue:(id)value forKey:(NSString *)key
{
// price 服务器是 NSNumber
// 服务器是 NSNumber ,模型表里是 NSString类型,所以,要处理
if ([value isKindOfClass:[NSNumber class]]) {
// NSNumber--> NSString
[self setValue:[NSString stringWithFormat:@"%@",value] forKey:key];
}else{
[super setValue:value forKey:key];
}
}
底层实现原理
KVC运用了isa-swizzing技术。isa-swizzing就是类型混合指针机制。KVC通过isa-swizzing实现其内部查找定位。isa指针(is kind of 的意思)指向维护分发表的对象的类,该分发表实际上包含了指向实现类中的方法的指针和其他数据。
比如说如下的一行KVC代码:1
2
3
4
5[site setValue:@"sitename" forKey:@"name"];
//会被编译器处理成
SEL sel = sel_get_uid(setValue:forKey);
IMP method = objc_msg_loopup(site->isa,sel);
method(site,sel,@"sitename",@"name");
每个类都有一张方法表,是一个hash表,值是函数指针IMP,SEL的名称就是查表时所用的键。
SEL数据类型:查找方法表时所用的键。定义成char*,实质上可以理解成int值。
IMP数据类型:他其实就是一个编译器内部实现时候的函数指针。当Objective-C编译器去处理实现一个方法的时候,就会指向一个IMP对象,这个对象是C语言表述的类型。
KVC的内部机制:
一个对象在调用setValue的时候进行了如下操作:
1.根据方法名找到运行方法的时候需要的环境参数
2.他会从自己的isa指针结合环境参数,找到具体的方法实现接口。
3.再直接查找得来的具体的实现方法苹果为什么要用子类(就是C语言创建的那个子类)监听setter方法,而不用分类(Person+AWKVO)呢?
回答:原因是当你用分类监听setter方法的时候,Person类中setter方法就不会走了,这样不好,所以苹果使用了子类监听setter方法。
比较objectForKey与objectForKeyPath
- 区别
1.如:sum/average/max/min1
2
3
4
5
6
7
8
9
10
11
12NSArray *array1 = @[@1, @3, @5, @7, @9,@11, @13];
NSInteger sum = [[array1 valueForKeyPath:@"@sum.floatValue"] integerValue];
NSInteger avg = [[array1 valueForKeyPath:@"@avg.floatValue"] integerValue];
NSInteger max = [[array1 valueForKeyPath:@"@max.floatValue"] integerValue];
NSInteger min = [[array1 valueForKeyPath:@"@min.floatValue"] integerValue];
NSLog(@"sum--%ld--avg--%ld-max--%ld-min--%ld",(long)sum,(long)avg,(long)max,(long)min);
//注意:此种写法将引起崩溃
// NSInteger sum = [[array1 valueForKey:@"@sum.floatValue"] integerValue];
// NSInteger avg = [[array1 valueForKey:@"@avg.floatValue"] integerValue];
// NSInteger max = [[array1 valueForKey:@"@max.floatValue"] integerValue];
// NSInteger min = [[array1 valueForKey:@"@min.floatValue"] integerValue];
// NSLog(@"sum--%ld--avg--%ld-max--%ld-min--%ld",(long)sum,(long)avg,(long)max,(long)min);
2.删除数组中重复的数据1
2
3
4NSArray *array2 = @[@1, @3, @5, @7, @9,@11, @13, @7, @9,@11];
NSLog(@"deleteKeyPath---%@",[array2 valueForKeyPath:@"@distinctUnionOfObjects.self"]);
//下述写法不可取,会引起崩溃
//NSLog(@"deleteKey---%@",[array2 valueForKey:@"@distinctUnionOfObjects.self"]);
3.深层次取字典中出子属性
- valueForKeyPath:可以深层次取到子属性,不管隐藏的多深,不只是字典套字典,对象套对象/对象套对象再套字典等情况,都可以通过链式调用到深层的值
valueForKey:无法取到深层次子属性
1
2
3
4
5NSDictionary *dic = @{@"dic1":@{@"dic2":@{@"name":@"zhangsanfeng",@"info":@{@"age":@"13"}}}};
//可以深层次的取到子层级属性
NSLog(@"KeyPath---%@",[dic valueForKeyPath:@"dic1.dic2.info.age"]); //13
//无法深层次取到子层级属性
NSLog(@"Key---%@",[dic valueForKey:@"dic1.dic2.info.age"]); //null相同
1.快速找到字典数组中key所对应的值
对于@[@{key:value},@{key:value},@{key:value}]的数组(数组元素是字典的),通过同一个key可以取到value的集合(数组)1
2
3
4
5
6
7
8
9
10
11
12
13
14NSArray *dicArray = @[
@{@"company":@"baidu",@"person":@{@"name":@"zhangsanfeng"}},
@{@"company":@"tencent",@"position":@"chengdu"}];
NSLog(@"keyPath---%@", [dicArray valueForKeyPath:@"company"]);
NSLog(@"key-- -%@", [dicArray valueForKey:@"company"]);
keyPath---(
baidu,
tencent
)
key-- -(
baidu,
tencent
)
2.大小写字母转换1
2
3
4NSArray *array3 = @[@"name",@"w",@"b",@"h",@"g",@"d",@"r",@"p"];
NSLog(@"KeyPath---%@",[array3 valueForKeyPath:@"uppercaseString"]);
NSArray *newArray = [array3 valueForKey:@"uppercaseString"];
NSLog(@"newArray---%@",newArray);
比较objectForKey与valueForKey
- valueforkey:是KVC的方法, Key只允许使用NSString类型。
- objectforkey:是NSDictionary的方法,Key可以是任意类型。
- 在NSDictionary中的差异:一般来说 key 可以是任意字符串组合,如果 key 不是以 @ 符号开头,这时候 valueForKey: 等同于 objectForKey:,如果是以 @ 开头,objectForKey 可以正确取值,但是 valueForKey 取值会直接 crash 掉,所以在使用NSDictionary取值时,尽量使用objectForKey。
1
2
3NSDictionary *dict = [NSDictionary dictionaryWithObject:@"theValue" forKey:@"@theKey"];// 注意这个 key 是以 @ 开头
NSString *value1 = [dict objectForKey:@"@theKey"];
NSString *value2 = [dict valueForKey:@"@theKey"];
APNs
远程推送
我们的设备和APNS服务器之间的通讯是基于SSL协议的TCP流通讯,二者之间维持一个长连接。
远程推送的实现原理
1.打开App时: 发送UDID和BundleID给APNs加密后返回deviceToken
2.获取Token后,App调用接口,将用户身份信息和deviceToken发给服务器,服务器记录
3.当推送消息时, 服务器按照用户身份信息找到存储的deviceToken,将消息和deviToken发送给APNs
4.苹果的APNs通过deviceToken, 找到指定设备的指定程序, 并将消息推送给用户
Feedback service
APNS会持续的更新Feedback service的列表,当我们的Provider将信息发给APNS推送到我们的设备时,如果这时设备无法将消息推送到指定的应用,就会向APNS服务器 报告一个反馈信息,而这个信息就记录在feedback service中。
参考文章:
iOS中如何选择delegate、通知、KVO(以及三者的区别)
深入理解 KVC\KVO 实现机制
iOS开发系列–通知与消息机制