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 *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
//就会在下面的方法中监听到变化的具体信息,哪个对象的哪个属性,变化前后的属性值.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"监听到了%@对象的%@属性由%@变成了%@属性",object,keyPath,change,change);
}
//NSLog信息:
监听到了对象的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监听的对象:

KVO & KVC_第1张图片
image.png

使用了KVO监听的对象:
KVO & KVC_第2张图片
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];
}
KVO & KVC_第3张图片
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");
}

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


KVO & KVC_第4张图片
image.png

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

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

KVO & KVC_第5张图片
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`.
    KVO & KVC_第6张图片
    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 *)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 *)touches withEvent:(UIEvent *)event{
    [self.person1 setValue:@20 forKey:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)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的赋值和取值过程是怎样的,原理是什么?

赋值

KVO & KVC_第7张图片
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:取值过程

KVO & KVC_第8张图片
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

你可能感兴趣的:(KVO & KVC)