iOS KVC KVO的简单使用
一、 kvc
1. KVC(Key-value coding)键值编码
通过对象的属性名(不管该属性是否暴露)直接访问该属性,或者给该对象赋值
这边获和赋值我这边分开来写。方便理解
简单使用的话这几个方法就行了
//直接通过Key来取值
- (nullable id)valueForKey:(NSString *)key;
//通过Key来设值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//通过KeyPath来取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通过KeyPath来设值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
//返回一个布尔值,该值指示键值编码方法在没有找到属性的访问器方法时是否应该直接访问对应的实例变量。
+ (BOOL)accessInstanceVariablesDirectly;
//当value(forKey:)没有发现与给定键相对应的属性时调用。
-(id)valueForUndefinedKey:(NSString *)key;
//当setValue:(forKey:)没有发现与给定键相对应的属性时调用。
-(void)setValue:(id)value forUndefinedKey:(NSString *)key
2. 调用 - (void)setValue:(nullable id)value forKey:(NSString *)key;
当调用 - (void)setValue:(nullable id)value forKey:(NSString *)key; 的时候程序都干了些什么呢?
下面是测试代码
#import <Foundation/Foundation.h>
//KVC给属性赋值
@interface Test: NSObject {
NSString *name;
NSString *_name;
NSString *isName;
NSString *_isName;
}
-(void)backName;
@end
@implementation Test
+(BOOL)accessInstanceVariablesDirectly{
NSLog(@"调用了accessInstanceVariablesDirectly");
return YES;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"调用了setValue:forUndefinedKey:");
}
-(void)setName:(NSString *)name{
NSLog(@"调用了setName");
_name = name;
}
-(void)setIsName:(NSString *)isName{
NSLog(@"调用了setIsName");
_isName = isName;
}
-(void)backName{
NSLog(@"name - %@",name);
NSLog(@"_name - %@",_name);
NSLog(@"isName - %@",isName);
NSLog(@"_isName - %@",_isName);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Test * s = [Test new];
[s setValue:@"Jobs" forKey:@"name"];
[s backName];
}
return 0;
}
当运行到
[s setValue:@"Jobs" forKey:@"name"]
的时候程序处理和调用顺序
1、先找-(void)setName:(NSString *)name
,找到赋值结束
2、再找-(void)setIsName:(NSString *)isName
,找到赋值结束
3、上面两个方法都找不到的时候调用+ (BOOL)accessInstanceVariablesDirectly
,3.1
return NO;
的时候 ,不让访问属性 。异常处理调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key
。
3.2return YES;
的时候。 先查找_name
,找不到则查找_isName
,还没有找到则找name
,最后找isName
,找到赋值结束。
3.3、以上都找不`到则异常处理调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key
3. 调用 - (nullable id)valueForKey:(NSString *)key;
其实和调用setValue(forkey:)是一样的。
下面是测试代码以
#import <Foundation/Foundation.h>
@interface Test: NSObject {
NSString *name;
NSString *_name;
NSString *isName;
NSString *_isName;
}
-(void)backName;
@end
@implementation Test
-(instancetype)init{
if (self = [super init]) {
name = @"Jobs1";
_name = @"Jobs2";
isName = @"Jobs3";
_isName = @"Jobs4";
}
return self;
}
+(BOOL)accessInstanceVariablesDirectly{
NSLog(@"调用了accessInstanceVariablesDirectly");
return YES;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"调用了valueForUndefinedKey");
return nil;
}
-(NSString *)getName{
NSLog(@"调用了getName"); //1
return _name;
}
-(NSString *)name{
NSLog(@"调用了name");//2
return _name;
}
-(NSString *)isName{
NSLog(@"调用了isName");//3
return _isName;
}
-(void)backName{
NSLog(@"name - %@",name);
NSLog(@"_name - %@",_name);
NSLog(@"isName - %@",isName);
NSLog(@"_isName - %@",_isName);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Test * s = [Test new];
NSLog(@"[s valueForKey:@\"name\"] :: %@",[s valueForKey:@"name"]);
[s backName];
}
return 0;
}
当运行到
[s valueForKey:@"name"]
的时候程序处理和调用顺序
1、先找-(NSString *)getName
, 找到获取结束
2、再找-(NSString *)name
,找到获取结束
3、再找-(NSString *)isName
,找到获取结束
4、上面两个方法都找不到的时候调用+ (BOOL)accessInstanceVariablesDirectly
,4.1
return NO;
的时候 ,不让访问属性 。异常处理调用-(id)valueForUndefinedKey:(NSString *)key
。
4.2return YES;
的时候。 先查找_name
,找不到则查找_isName
,还没有找到则找name
,最后找isName
,找到获取结束。
4.3、以上都找不`到则异常处理调用-(id)valueForUndefinedKey:(NSString *)key
4. 调用- (nullable id)valueForKeyPath:(NSString *)keyPath; 和- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
上面的讲的很明白 ,这两个我就放在一起讲 这个和上面的原理是一样的
下面是一段测试代码
#import <Foundation/Foundation.h>
@interface Hand : NSObject{
NSString *_desc;
}
@end
@implementation Hand
@end
//----------------------
@interface People: NSObject{
Hand *_hand;
}
@end
@implementation People
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *p = [People new];
Hand *h = [Hand new];
[p setValue:h forKey:@"hand"];
[p setValue:@"这是我的手" forKeyPath:@"hand.desc"];
NSLog(@"%@",[p valueForKeyPath:@"hand.desc"]);
}
return 0;
}
原理就是根据hand.desc 中的 '.' ,来分割;两个key。其他的是还是和
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
这两个方法是一样的原理
二、KVO
1. KVO 即 Key-Value Observing
键值观察,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。
主要方法
//注册监听
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
//移除监听
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
//监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
//value将要改变
- (void)willChangeValueForKey:(NSString *)key;
//value已经改变
- (void)didChangeValueForKey:(NSString *)key;
实现原理:
KVO 是通过 isa-swizzling 实现的。
基本的流程就是编译器自动为被观察对象创造一个派生类,并将被观察对象的isa 指向这个派生类。如果用户注册了对某此目标对象的某一个属性的观察,那么此派生类会重写这个方法,并在其中添加进行通知的代码。Objective-C 在发送消息的时候,会通过 isa 指针找到当前对象所属的类对象。而类对象中保存着当前对象的实例方法,因此在向此对象发送消息时候,实际上是发送到了派生类对象的方法。由于编译器对派生类的方法进行了 override,并添加了通知代码,因此会向注册的对象发送通知。注意派生类只重写注册了观察者的属性方法。
普通的属性赋值 以及打印结果:
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface People: NSObject
@property (nonatomic,assign) NSInteger age;
@end
@implementation People
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"keyPath - object : %@ - %@",keyPath,object);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *p = [People new];
// [p addObserver:p forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
p.age = 12;
//打印p指向的对象
NSLog(@"p指向的对象 :%@", [p class]);
//打印p中isa指针指向的对象
NSLog(@"p中isa指针指向的对象 :%@", object_getClass(p));
}
return 0;
}
打印结果:
MyTextKVCKVO[94189:4661264] p指向的对象 :People
MyTextKVCKVO[94189:4661264] p中isa指针指向的对象 :People
添加监听之后的赋值
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface People: NSObject
@property (nonatomic,assign) NSInteger age;
@end
@implementation People
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"keyPath - object : %@ - %@",keyPath,object);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People *p = [People new];
[p addObserver:p forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
p.age = 12;
//打印p指向的对象
NSLog(@"p指向的对象 :%@", [p class]);
//打印p中isa指针指向的对象
NSLog(@"p中isa指针指向的对象 :%@", object_getClass(p));
}
return 0;
}
打印结果:
MyTextKVCKVO[94199:4662213] keyPath - object : age - <People: 0x100760f90>
MyTextKVCKVO[94199:4662213] p指向的对象 :People
MyTextKVCKVO[94199:4662213] p中isa指针指向的对象 :NSKVONotifying_People
而我们也知道,所谓的OC的消息机制是通过isa去查找实现的,那么我们现在可以进行大胆的猜想:
其实KVO的实现可能是:
添加Observer通过runtime偷偷实现了一个子类,并且以NSKVONotifying_+类名来命名,将之前那个对象的isa指针指向了这个子类。,重写了观察的对象setter方法,并且在重写的中添加了willChangeValueForKey:以及didChangeValueForKey:
屏幕快照 2019-08-22 上午11.59.54.png
补充:被观察的对象释放以后,记得移除监听
上面都是自己查找资料以及自己试验之后的总结,如有不对敬请指正。