KVO & KVC

KVO:

KVO的全称是Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变.

    self.person = [[LDPerson alloc] init];
    self.person.age = 10;
//添加键值监听
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
//在touchesBegan方法中修改age属性值. 
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
//就会在下面的方法中监听到变化的具体信息,哪个对象的哪个属性,变化前后的属性值.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
}
//NSLog信息:
监听到了<LDPerson: 0x60c00000fab0>对象的age属性由{
    kind = 1;
    new = 20;
    old = 10;
}变成了{
    kind = 1;
    new = 20;
    old = 10;
}属性
//需要在不使用的时候,移除监听
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"age"];
}

KVO原理:

通过修改instance对象的isa指针,指向一个通过runtime动态生成的子类,例如self.personisa指针并不是指向LDPersonclass对象,而是指向NSKVONotifying_LDPerson,该类是LDPerson的一个子类.
未使用KVO监听的对象:

image.png

使用了KVO监听的对象:
image.png

_NSSet*ValueAndNotify的内部实现

- (void)setAge:(int)age{
    _NSSet*ValueAndNotify();
}
void _NSSet*ValueAndNotify()
{
      [self willChangeValueForKey:@"age"];
      [super setAge:age];
      [self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key{
          //通过监听器,监听属性发生了改变
  [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
image.png

1.调用willChangeValueForKey:
2.调用原来的setter实现
3.调用didChangeValueForKey:

  • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

代码验证上述流程

//LDPerson内部代码实现
- (void)setAge:(int)age{
    _age = age;
    NSLog(@"setAge");
}
- (void)willChangeValueForKey:(NSString *)key{
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");

}
- (void)didChangeValueForKey:(NSString *)key{
    NSLog(@"didChangeValueForKey == begin");

    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey == end");
}

通过打印信息可以看到,流程如上所述:


image.png

通过runtime查看isa指针指向的 class对象

可以看到添加KVO之后,instacne对象的isa所指向的class对象发生了变化.

image.png

通过LLDB查看- (void)setAge:(int)age方法.

  • 如果LDPerson实现了setAge会提示(KVO-[LDPerson setAge:] at LDPerson.m:12)setAge在.m文件当中第12行.
  • 如果LDPerson只是通过@property则会提示在.h文件中.
  • 可以观察到添加了KVO属性的setter实现不同.添加之前是在LDPerson文件中,添加KVO之后setter实现是在Foundation框架中,_NSSet*ValueAndNotify
  • runtime动态生成的类NSKVONotifying_LDPerson,其isa指向该类的元类对象NSKVONotifying_LDPerson,其superClass指针指向LDPerson,即生成的class对象有自己的meta-class对象.其父类为添加KVO对象的class对象LDPerson`.
    image.png

动态生成的NSKVONotifying_LDPerson类

runtime动态生成的这个类内部有几个特殊的方法,并重写了setAge,class,dealloc方法

- (void)setAge:(int)age
{
    _NSSetIntValueAndNotify();
}

// 屏幕内部实现,隐藏了NSKVONotifying_LDPerson类的存在
- (Class)class
{
    return [LDPerson class];
}

- (void)dealloc
{
    // 收尾工作
}

- (BOOL)_isKVOA
{
    return YES;
}

为何重写class方法:
屏幕内部实现,隐藏了NSKVONotifying_LDPerson类的存在.从开发者的使用角度来看,并不知道NSKVONotifying_LDPerson类的存在,如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_LDPerson,就会将该类暴露出来,也给开发者造成困扰,写的是LDPerson,添加KVO之后class方法返回会改变.
猜测NSObjectclass方法的实现:
self为添加了KVO后的LDPerson,最红返回可能是NSKVONotifying_LDPerson.

- (Class)class
{
    return object_getClass(self);
}

通过runtime 检验class,dealloc``isKVOA方法的存在

- (void)printMethodNameOfClass:(Class)cls{
    unsigned int count;
    //获取方法列表数组
    Method * methodList = class_copyMethodList(cls, &count);
    
    //存储方法名
    NSMutableString * methodsNames = [NSMutableString string];
    
    //遍历方法数组
    for (int i = 0; i < count; i++) {
        //获取方法
        Method  method = methodList[I];
        //获取方法名字
        NSString * methodName = NSStringFromSelector(method_getName(method));
        //拼接方法
        [methodsNames appendString:methodName];
        [methodsNames appendString:@", "];
    }
    //释放 如果C语言的数据结构是是通过copy
    //creat 等创建出来的,一般都需要释放
    free(methodList);
    
    //打印方法名
    NSLog(@"%@ %@",cls,methodsNames);
}
self.person1 = [[LDPerson alloc] init];
    self.person1.age = 10;
  
    self.person2 = [[LDPerson alloc] init];
    self.person2.age = 20;
    
    [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

    //person1 添加了KVO
    [self printMethodNameOfClass:object_getClass(self.person1)];
    //person2 没有添加了KVO
    [self printMethodNameOfClass:object_getClass(self.person2)];

可以看到:

  • 添加了KVO的NSKVONotifying_LDPerson类的内部有class,dealloc``isKVOAsetter方法.没有agegetter方法.因为NSKVONotifying_LDPerson类只会重写setter方法,该setter方法内部会调用willChangeValueForKey,didChangeValueForKey来实现KVO监听.而getter方法存在NSKVONotifying_LDPerson类的父类LDPerson中.
  • 没有添加KVO的LDPerson只有一个property属性,还属性会生成settergetter方法.
    image.png

iOS用什么方式实现对一个对象的KVO?(KVO本质是什么?)

  • 利用runtimeAPI动态生成一个子类,并让instance对象的isa指向这个全新的子类.
  • 当修改instance对象的属性时,会调用Foundation_NSSetxxxValueAndNotify函数该函数会调用以下函数:
    1. willChangeValueForKey
    2. 父类原来的setter方法
    3. didChangeValueForKey该函数内部会出发监听器(Oberser)的监听方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

如何手动触发KVO

解读:添加KVO之后正常情况下触发KVO的条件是:修改了KVO监听的属性值,此时会触发KVO监听方法.此题的意思是在没有修改KVO监听的属性值时,如果触发KVO监听方法?
思路:动态生成的子类NSKVONotifying_LDPerson内部主要的操作是重写了setter方法,方法内部调用了
willChangeValueForKey,didChangeValueForKey方法.
所以:
我们只需要被监听的 instance对象手动调用两个方法即可:

  [self.person1 willChangeValueForKey:@"age"];
  [self.person1 didChangeValueForKey:@"age"];

哪些对象可以使用KVO监听?核心是重写了setter,如果有setter就可以实现监听,如果没有setter就无法使用KVO监听.

直接修改成员变量会触发KVO吗

不会,没有在setter中调用willChangeValueForKey, didChangeValueForKey核心方法.

KVC

KVC的全称是Key-Value Coding,俗称“键值编码”,可以通过一个key来访问某个属性

常见的API有:

  • -(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
  • - (void)setValue:(id)value forKey:(NSString *)key;
  • - (id)valueForKeyPath:(NSString *)keyPath;
  • - (id)valueForKey:(NSString *)key;

setValue:(id)value forKey和setValue:(id)value forKeyPath有什么不同?

value forKeyPath会通过路径向下级继续查找并赋值
value forKey只会在当前对象的路径下赋值,不会继续查找

@interface LDCat : NSObject
@property (assign, nonatomic) int weight;
@end
@interface ldPerson : NSObject
@property (assign, nonatomic) int age;
@property (assign, nonatomic) LDCat *cat;
@end

 [person valueForKey:@"age"];
 [person setValue:@10 forKeyPath:@"cat.weight"];

通过KVC修改属性会触发KVO吗?(会)

解读:该题的意思是如下代码会不会出发KVO.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person1 = [[LDPerson alloc] init];
    self.person1.age = 10;
    [self.person1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person1 setValue:@20 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"监听到了%@对象的%@属性改变%@",object,keyPath,change);
}
- (void)dealloc{
    [self.person1 removeObserver:self forKeyPath:@"age"];
}
KVC的核心是在`setter`中调用`willChangeValueForKey, didChangeValueForKey`方法,上图代码中不用`@property`关键字生成`setter方法,可以看到依然会触发KVO,可知KVC内部会自动调用上述`willChangeValueForKey, didChangeValueForKey`方法来触发KVO监听

KVC的赋值和取值过程是怎样的,原理是什么?

赋值

image.png

解读:如果调用了setValue: forKey:方法
1.会首先查找setKey:方法,如果没有setKey:方法,接下来会查找_setKey:方法.至此如果查找到两个方法中的热议一个,就会传递参数,调用方法,出发KVO.
2.如果上述两个方法都没有查找到.会查看accessInstanceVariablesDirectly的返回值.该方法标识是都可以直接访问成员变量,如果返会NO即不能直接访问成员变量,就会抛出异常,找不到合适的Key赋值.如果可以直接访问成员变量,接下来就会查找属性名,按照如下顺序查找:_key _isKey key isKey如果找到成员变量直接赋值,触发KVO.

KVC过程总结:

  1. 首先按照顺序查找setter方法
    1.1: setKey: --->_setKey:

2:如果没有查找到setter方法,会查看accessInstanceVariablesDirectly(是否可以直接访问成员变量,默认返回YES)函数返回值.如果返回值为NO就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException.如果返回值为YES,

3: 接下来会进入查找成员变量,直接赋值的流程
查找顺序为:_Key --> _isKey ---> key --->isKey

4.如果没有查找到成员变量,就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException

valueForKey:取值过程

image.png

1.查找getter方法
getKey ---> Key ---> isKey---> _key
找到该方法,调用并返回对应的值,如果没找到
2.查看accessInstanceVariablesDirectly方法返回值
如果返回YES进入下个流程,如果返回NO就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException
3.返回值为YES即可直接访问成员变量,就会俺书讯查找成员变量: _key ---> _iskey ---> key ---> iskey
4.如果查找到调用并返回对应的值.如果没找到就会调用setValue:forUndefinedKey:
并抛出异常NSUnknownKeyException

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

推荐阅读更多精彩内容