2018.4.26提问:请描述一下你使用KVC,KVO的具体案例,以及你认为使用的时候需要注意的地方?
KVC:
具体案例:属性赋值,添加私有成员变量,字典转模型
注意的地方:
- KVC,使用setValuesForKeysWithDictionary:方法,该方法默认根据字典中每个键值对,调用setValue:forKey方法
缺点:
-1. 字典中的键值对必须与模型中的键值对完全对应,否则程序会崩溃 - 2.字段比较多的话,手写字典转模型就很累
解决方案:
- 1.使用一个第三方控件MJExtension;可以做到字典转模型,模型里面还可以套结模型,也可以套接模型数组,功能比较完善和强大。
- 2.处理- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
- 封装一个BaseModel 实现下面两个方法,创建如下转模型方法
// 将所有的数据转换为字符串
-(void)setValue:(id)value forKey:(NSString *)key{
if([value isKindOfClass:[NSNull class]]){
value=nil;
}else if([value isKindOfClass:[NSArray class]]){
}else{
value = [NSString stringWithFormat:@"%@",value];
}
[super setValue:value forKey:key];
}
// 对特殊字符 id 进行处理
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"Undefined Key: %@", key);
}
// 字典转模型
-(id)initWithDic:(NSDictionary *)modelDic{
self = [super init];
if(self){
[self setValuesForKeysWithDictionary:modelDic];
}
return self;
}
使用:
EmployeModel *aEmploye = [[EmployeModel alloc]initWithDic:employeeDic];
底层实现:
当一个对象调用setValue:forKey: 方法时,方法内部会做以下操作:
- 1.判断有没有指定key的set方法,如果有set方法,就会调用set方法,给该属性赋值
2.如果没有set方法,判断有没有跟key值相同且带有下划线的成员属性(_key).如果有,直接给该成员属性进行赋值
3.如果没有成员属性_key,判断有没有跟key相同名称的属性.如果有,直接给该属性进行赋值
- 4.如果都没有,就会调用 valueforUndefinedKey 和setValue:forUndefinedKey:方法
KVO:
具体案例:
- KVO有显著的使用场景,当你希望监视一个属性的时候,当处理属性层的消息的事件时候,使用KVO,其他的尽量使用delegate。
优点:
- 1.能够提供一种简单的方法实现两个对象间的同步。例如:model和view之间同步;
- 2.能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SDK对象)的实现;
- 3.能够提供观察的属性的最新值以及先前值;
- 4.用key paths来观察属性,因此也可以观察嵌套对象;
- 5.完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
- 1.我们观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
- 2.对属性重构将导致我们的观察代码不再可用;
- 3.复杂的“IF”语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向;
- 4.当释放观察者时不需要移除观察者。
注意事项:
- 潜在的问题有可能出现在dealloc中对KVO的注销上。KVO的一种缺陷(其实不能称为缺陷,应该称为特性)是,当对同一个keypath进行两次removeObserver时会导致程序crash,这种情况常常出现在父类有一个kvo,父类在dealloc中remove了一次,子类又remove了一次的情况下。不要以为这种情况很少出现!当你封装framework开源给别人用或者多人协作开发时是有可能出现的,而且这种crash很难发现。不知道你发现没,目前的代码中context字段都是nil,那能否利用该字段来标识出到底kvo是superClass注册的,还是self注册的?
回答是可以的。我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@”ThisIsMyKVOContextNotSuper”;然后在dealloc中remove
observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash
KVC:键值编码
主要作用是:
(1)通过键值路径为对象的属性赋值。主要是可以为私有的属性赋值。
AppleViewController *appleVC = [[AppleViewController alloc]init];
[appleVC setValue:@"橘子" forKey:@"name"];
如果对象A的属性是一个对象B,要设置对象B的属性
[person setValue:@"旺财" forKeyPath:@"dog.name"];
(2)通过键值路径获取属性的值。主要是可以通过key获得私有属性的值。
NSString *nameStr = [appleVC valueForKey:@"name"];
也可以通过keypath获得值
NSString *dName = [person valueForKeyPath:@"dog.name"];
(3)将字典转型成Model,方法:setValuesForKeysWithDictionary:
// 定义一个字典
NSDictionary *dict = @{
@"name" : @"jack",
@"money" : @"20.7",
};
// 创建模型
Person *p = [[Person alloc] init];
// 字典转模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"person's name is the %@",p.name);
注意:字典的key和Model的属性一定要一一对应。否则会出现错误。比如person里没有name的属性,系统报错如下:
‘[ setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.’
二. KVO
1.KVO:键值观察(key-value-observing)
KVO提供了一种观察者的机制,通过对某个对象的某个属性添加观察者,当该属性改变,就会调用”observeValueForKeyPath:”方法,为我们提供一个“对象值改变了!”的时机进行一些操作。
2.KVO原理
当某个类的对象第一次被观察时,系统在运行时会创建该类的派生类,改派生类中重写了该对象的setter方法,并且在setter方法中实现了通知的机制。派生类重写了class方法,以“欺骗”外部调用者他就是原先那个类。系统将这个类的isa指针指向新的派生类,因此改对象也就是新的派生类的对象了。因而改对象调用setter就会调用重写的setter,从而激活键值通知机制。此外派生类还重写了delloc方法来释放资源。
3.KVO的使用
(1)给对象的属性添加观察者
[appleVC addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
注: options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 返回未改变之前的值和改变之后的值 context可以为空
(2)若该属性发生改变,系统自动调用下面的方法:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
//拿到新值/旧值,进行操作
NSLog(@"newValue----%@",change[@"new"]);
NSLog(@"oldValue----%@",change[@"old"]);
}
(3)取消监听
-(void)dealloc
{
[person removeObserver:self forKeyPath:@"test"];
}
4.KVO的使用场景
KVO用于监听对象属性的改变。
(1)下拉刷新、下拉加载监听UIScrollView的contentoffsize;
(2)webview混排监听contentsize;
(3)监听模型属性实时更新UI;
(4)监听控制器frame改变,实现抽屉效果。
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@property(nonatomic,strong)Person * p;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_p =[[Person alloc]init];
//添加观察者
[_p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"观察到了");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_p.name =@"hank";
}
成员变量是对成员属性的一个封装,每个成员变量都包含_name + getter + setter 方法
如果我们自己一个成员属性,那么使用observeValueForKeyPath就观察不到值的变化,所以我们可以知道,观察者观察的是setter方法。
#import <Foundation/Foundation.h>
@interface Person : NSObject{
@public
NSString * _name;
}
//_name + getter + setter 方法!
//@property(nonatomic,copy)NSString * name;
@end
ViewController.h
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// _p.name =@"hank";
_p->_name=@"hank";
}
KVO的底层实现原理:1.动态创建一个类NSKVONotyfing_Person,继承自被继承的类Person,重写setter方法
2.改变P对象的类型!(子类类型)
#import "NSKVONotyfing_Person.h"
@implementation NSKVONotyfing_Person
-(void)setName:(NSString *)name{
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
@end