KVO和KVC的本质

一、KVO

问题

  1. iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
  2. 如何手动触发KVO

1. KVO使用

KVO的全称Key-Value Observing,俗称“键值监听”,可以用于监听某个对象属性值的改变。通过方法(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context给对象添加监听

Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];
p1.age = 2;
p2.age = 5;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
p1.age = 10;
p2.age = 20;

当对象某个属性的值发生了改变之后会回调方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    
}
监听到对象的age属性改变了,{
    kind = 1;
    new = 10;
    old = 2;
}

上述代码中可以看出,在添加监听之后,age属性的值在发生改变时,就会通知到监听者,执行监听者的observeValueForKeyPath方法。

记得在合适的时机移除对对象的监听(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath

2. KVO实现原理

2.1 分析

通过上述代码我们发现,一旦age属性的值发生改变时,就会通知到监听者(观察者),并且我们知道赋值操作都是调用set方法,我们可以来到Persn类中重写ageset方法,观察是否是KVOset方法内部做了一些操作来通知监听者。

我们发现即使重写了set方法,p1对象和p2对象调用同样的set方法,但是我们发现p1除了调用set方法之外还会另外执行监听器的observeValueForKeyPath方法。

说明KVO在运行时会对p1对象做了一些改变。相当于在程序运行过程中,对p1对象做了一些变化,使得p1对象在调用setage方法的时候可能做了一些额外的操作,所以问题出在对象本身。

2.2 本质

首先我们对上述代码中添加监听的地方打断点,看观察一下,addObserver方法对p1对象做了什么处理?也就是说p1对象在经过addObserver方法之后发生了什么改变,我们通过打印isa指针如下所示

// 添加监听之前的isa
(lldb) po p1->isa
Person

(lldb) po p2->isa
Person

// 添加监听之后的isa
(lldb) po p1->isa
NSKVONotifying_Person

(lldb) po p2->isa
Person

我们发现,p1对象执行过addObserver: forKeyPath:操作之后,p1实例对象的isa指针由之前的指向类对象Person变为指向NSKVONotifyin_Person类对象,而p2对象没有任何改变。也就是说一旦p1对象添加了KVO监听以后,其isa指针就会发生变化,因此类对象里面的的set方法的执行效果就不一样了。

那么我们先来观察p2对象在内容中是如何存储的,然后对比p2来观察p1

p2在调用setage方法的时候,首先会通过p2实例对象中的isa指针找到Person类对象,然后在类对象中找到setage方法。然后找到方法对应的实现。如下图所示

kvo_set_before

2.3 添加KVO之后的实例对象isa指向

p1实例对象的isa指针在经过KVO监听之后已经指向了NSKVONotifyin_Person类对象。

NSKVONotifyin_Person其实是Person的子类,那么也就是说其super_class指针是指向Person类对象的,NSKVONotifyin_Person类是runtime在运行时生成的。

那么p1实例对象在调用setage实例方法的时候,会根据p1isa找到NSKVONotifyin_Person类对象,在NSKVONotifyin_Person中找setage的方法及实现。

2.4 NSKVONotifyin_xxx内部做的操作

NSKVONotifyin_Person中的setage方法中其实调用了Fundation框架中C语言函数 _NSsetIntValueAndNotify

_NSsetIntValueAndNotify函数内部做的操作相当于:

  1. 调用willChangeValueForKey 将要改变方法,类似于[self willChangeValueForKey:]
  2. 调用父类的setage方法对成员变量赋值,类似于[super setage:]
  3. 最后调用didChangeValueForKey已经改变方法,类似于[self didChangeValueForKey:]

didChangeValueForKey中会调用监听者的监听方法,最终来到监听者的observeValueForKeyPath方法中。

3. 验证KVO的内部实现

我们已经验证了,在执行添加监听的方法时,会将isa指针指向一个通过runtime创建的Person的子类NSKVONotifyin_Person的类对象。

另外我们可以通过打印方法实现的地址来看一下p1p2setage:的方法实现的地址在添加KVO前后有什么变化。

// 通过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听之前 p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
NSLog(@"添加KVO监听之后 p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);

进入lldb

2020-01-09 16:27:09.786420+0800 KVO的本质[98033:1408550] 添加KVO监听之前 p1 = 0x109df9040, p2 = 0x109df9040
2020-01-09 16:27:09.786912+0800 KVO的本质[98033:1408550] 添加KVO监听之后 p1 = 0x7fff2564d626, p2 = 0x109df9040
(lldb) p (IMP)0x109df9040
(IMP) $0 = 0x0000000109df9040 (KVO的本质`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d626
(IMP) $1 = 0x00007fff2564d626 (Foundation`_NSSetIntValueAndNotify)
(lldb) 

我们发现在添加KVO监听之前,p1p2setAge:方法实现的地址相同,而经过KVO之后,p1setAge:方法实现的地址发生了变化。

我们通过打印方法实现来看一下前后的变化发现,确实如我们上面所讲的一样,p1setAge:方法的实现由Person类对象中的setAge:方法转换为了C语言Foundation框架的_NSsetIntValueAndNotify函数。

Foundation框架中会根据属性的类型,调用不同的方法。例如我们之前定义的int类型的age属性,那么我们看到Foundation框架中调用的_NSsetIntValueAndNotify函数。那么我们把age的属性类型变为double重新打印一遍:

2020-01-09 16:30:30.633548+0800 KVO的本质[98184:1412183] 添加KVO监听之前 p1 = 0x10567c010, p2 = 0x10567c010
2020-01-09 16:30:30.633898+0800 KVO的本质[98184:1412183] 添加KVO监听之后 p1 = 0x7fff2564d3a8, p2 = 0x10567c010
(lldb) p (IMP)0x10567c010
(IMP) $0 = 0x000000010567c010 (KVO的本质`-[Person setAge:] at Person.h:15)
(lldb) p (IMP)0x7fff2564d3a8
(IMP) $1 = 0x00007fff2564d3a8 (Foundation`_NSSetDoubleValueAndNotify)
(lldb) 

我们发现调用的函数变为了_NSSetDoubleValueAndNotify,那么这说明Foundation框架中有许多此类型的函数,通过属性的不同类型调用不同的函数。
那么我们可以推测Foundation框架中还有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函数。

我们可以找到Foundation框架文件,通过命令行查询关键字找到相关函数

foundation_kvo

4. Foundation框架的_NSSet*ValueAndNotify函数的内部逻辑

我们在Person类中重写willChangeValueForKey:didChangeValueForKey:方法,模拟他们的实现。

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}

再次运行来查看didChangeValueForKey:的方法内运行过程,通过打印的顺序可以看到,确实在didChangeValueForKey:方法内部已经调用了observerobserveValueForKeyPath:ofObject:change:context:方法。

2020-01-09 17:07:17.044803+0800 KVO的本质[890:1450834] willChangeValueForKey: - begin
2020-01-09 17:07:17.044941+0800 KVO的本质[890:1450834] willChangeValueForKey: - end
2020-01-09 17:07:17.045039+0800 KVO的本质[890:1450834] 调用了setAge
2020-01-09 17:07:17.045133+0800 KVO的本质[890:1450834] didChangeValueForKey: - begin
2020-01-09 17:07:17.045323+0800 KVO的本质[890:1450834] 监听到对象的age属性改变了,{
    kind = 1;
    new = 249421248;
    old = 249421248;
}
2020-01-09 17:07:17.045423+0800 KVO的本质[890:1450834] didChangeValueForKey: - end

5. NSKVONotifyin_Person类对象存储的实例方法

NSKVONotifyin_Person作为Person的子类,其super_class指针指向Person类对象,并且NSKVONotifyin_Person内部一定对setAge:方法做了单独的实现,那么NSKVONotifyin_PersonPerson类的差别可能就在于其存储的实例方法及方法实现不同。

我们通过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的实例方法

// 通过runtime打印类对象的方法名
- (void)printMethods: (Class)cls {
    unsigned int count;
    Method *methods = class_copyMethodList(cls, &count);
    
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@类的方法 - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@","];
    }
    
    NSLog(@"%@",methodNames);
    free(methods);
}

// 通过methodForSelector找到方法实现的地址
NSLog(@"添加KVO监听之前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
// self 监听 p1的 age属性
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
NSLog(@"添加KVO监听之后 - p1 = %p, p2 = %p", [p1 methodForSelector:@selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
[self printMethods: object_getClass(p2)];
[self printMethods: object_getClass(p1)];

结果

2020-01-09 16:48:16.718818+0800 KVO的本质[99651:1433359] Person类的方法 - age,setAge:,
2020-01-09 16:48:16.718958+0800 KVO的本质[99651:1433359] NSKVONotifying_Person类的方法 - setAge:,class,dealloc,_isKVOA,

通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge:classdealloc_isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。

nskvonotifyin

这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1p2对象的class方法可以发现他们都返回Person

NSLog(@"%@,%@",[p1 class],[p2 class]);
// Person,Person

如果NSKVONotifyin_Person不重写class方法,那么当对象要调用class对象方法的时候就会一直向上找来到NSObject,而NSObjectclass的实现大致为返回自己真实isa指向的类,就是p1isa指向的类那么打印出来的类就是NSKVONotifyin_Person.

但是官方不希望将NSKVONotifyin_Person类暴露出来,并且不希望我们知道NSKVONotifyin_Person内部实现,所以在内部重写了class类,直接返回Person类,所以外界在调用p1class对象方法时是Person类。这样p1给外界的感还是Person类,并不知道NSKVONotifyin_Person子类的存在。

那么我们可以猜测NSKVONotifyin_Person内重写的class方法内部实现大致为:

- (Class) class {
     // 得到自己的类对象,再找到类对象父类
     return class_getSuperclass(object_getClass(self));
}

6. 手动触发KVO

通过手动触发对象的didChangeValueForKey:方法可以触发KVO:

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
    
[p1 removeObserver:self forKeyPath:@"age"];

打印结果:

监听到对象的age属性改变了,{
    kind = 1;
    new = 2;
    old = 2;
}

通过打印我们可以发现,didChangeValueForKey方法内部成功调用了observeValueForKeyPath:ofObject:change:context:,并且age的值并没有发生改变。

7. 问题:

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

当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会调用Foundation_NSSet*ValueAndNotify方法。

_NSSet*ValueAndNotify方法内部顺序调用willChangeValueForKey方法、原来父类的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

如何手动触发KVO

被监听的属性的值被修改时,就会自动触发KVO。如果想要手动触发KVO,则需要我们自己调用willChangeValueForKeydidChangeValueForKey方法即可在不改变属性值的情况下手动触发KVO,并且这两个方法缺一不可。

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

KVO是重写了set方法,直接修改成员变量不会触发set方法,所以不会触发KVO

person->_age = 2;

二、KVC

问题

  1. 通过KVC修改属性会触发KVO吗?
  2. KVC的赋值和取值过程是怎么样的?原理是什么?

1. 使用

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; 
Person *person = [[Person alloc]init];
        
[person setValue:@10 forKey:@"age"];
      
// person对象的cat属性的weight属性
person.cat = [[Cat alloc] init];
[person setValue:@20 forKeyPath:@"cat.weight"];
        
NSLog(@"%@",[person valueForKey:@"age"]);
NSLog(@"%@",[person valueForKeyPath:@"cat.weight"]);

2. setValue:forKey:设值的原理

2.1 KVC会触发KVO - 通过访问属性

Observer *observer = [[Observer alloc]init];
Person *person = [[Person alloc]init];
            
[person addObserver:observer forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
            
// 通过KVC修改age属性的值
[person setValue:@10 forKey:@"age"];
            
[person removeObserver:observer forKeyPath:@"age"];

我们发现是触发了KVO的:

2020-04-10 21:34:18.284166+0800 KVC的本质[29149:8625107] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}

需要注意的是这里Person类是有属性age``的`,所以系统会自动生成set```方法。

2.2 原理

通过KVC的方式赋值的时候其实通过访问属性的set方法和访问成员变量来达到设值的。

  1. 首先会调用属性的set方法来赋值:
- (void)setAge:(int)age {
    NSLog(@"setAge: %d", _age);
}
  1. 如果没有找到属性的set方法,那么会来到_setAge方法来赋值:
- (void)_setAge:(int)age {
    NSLog(@"_setAge: %d", _age);
}
  1. 如果没有找到_setAge方法,那么会来到accessInstanceVariablesDirectly方法,来询问是否可以访问成员变量来赋值:
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

如果返回NO,那么会抛出异常,如果返回YES,则表示可以通过访问成员变量来赋值。

  1. 访问成员变量会按照_age_isAgeageisAge的顺序来逐个访问赋值,如果这4个成员变量都没有找到,就抛出异常。
@interface Person : NSObject
{
    @public
    int _age;
    int _isAge;
    int age;
    int isAge;
}

2.3 KVC会触发KVO - 通过访问成员变量

现在我们删除Person的属性,添加成员变量_age

@interface Person : NSObject
{
    @public
    int _age;
}

我们发现通过KVC改变属性照样可以触发KVO

KVC的本质[30832:8656264] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}

我们来重写PersonwillChangeValueForKeydidChangeValueForKey方法:

- (void)willChangeValueForKey:(NSString *)key {
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey");
}


- (void)didChangeValueForKey:(NSString *)key {
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey");
}
KVC的本质[30911:8657993] willChangeValueForKey
2020-04-10 22:14:44.426343+0800 KVC的本质[30911:8657993] observeValueForKeyPath - {
    kind = 1;
    new = 10;
    old = 0;
}
2020-04-10 22:14:44.426514+0800 KVC的本质[30911:8657993] didChangeValueForKey

我们发现KVC通过访问成员变量来赋值的时候,其内部会主动触发KVO。但是我们自己访问成员变量不会触发KVO。原因就是通过KVC访问成员变量的时候,系统会主动触发KVO,相当于:

[person willChangeValueForKey:@"age"];
person->_age = 10;
[person didChangeValueForKey:@"age"];

3. valueForKey:取值的原理

和赋值一样的,也是按顺序访问方法和成员变量获得值:

  1. 通过getAge方法取值
- (int)getAge {
    return 10;
}
  1. 如果没有getAge方法,通过age方法取值
- (int)age {
    return 11;
}
  1. 如果没有age方法,通过isAge方法取值
- (int)isAge {
    return 12;
}
  1. 如果没有isAge方法,通过_age方法取值
- (int)_age {
   return 13;
}
  1. 如果以上方法都没有找到,那么会通过accessInstanceVariablesDirectly方法的返回值来确定是否可以访问成员变量取值
_age
_isAge
age
isAge
kvogetvalue

你可能感兴趣的:(KVO和KVC的本质)