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.2 return 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.2 return 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

补充:被观察的对象释放以后,记得移除监听

上面都是自己查找资料以及自己试验之后的总结,如有不对敬请指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,083评论 1 32
  • KVO (Key-Value Observing),俗称“键值监听”,能够用来监听对象属性的变化,也是 Objec...
    valentizx阅读 340评论 0 3
  • 源码加翻译 #import <Foundation/NSArray.h> #import <Foundation/...
    CAICAI0阅读 1,146评论 0 50
  • 上面一篇文章没有分析完yymodel 。 接着上篇接着分析 static void ModelSetValueFo...
    充满活力的早晨阅读 1,033评论 0 0
  • 上学时,冬天总是很漫长,冬天上学很冷,我讨厌穿很多衣服。特别怕母亲要我穿上厚厚的棉裤,那么肥,又笨重,母亲拿着棉裤...
    树兜把阅读 363评论 0 6