[iOS开发]KVO+KVC

KVO

什么是KVO

KVO全称Key Value Observing,其是苹果提供的一套事件通知机制。允许对象监听另一个对象特定属性的改变,并在改变时接收到事件。观察者模式

由于KVO的实现机制,只针对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO

KVO可以监听单个属性的变化,也可以监听集合对象的变化。集合对象包含NSArray和NSSet。通过KVC的mutableArrayValueForKey:等方法获得代理对象,当代理对象的内部对象发生改变时,会回调KVC监听的方法。

KVO的基本使用

主要分三个步骤

  • 通过addObserver:forKeyPath:options:context:方法注册观察者
    • observer:观察者,监听属性变化的对象。该对象必须必须实现observeValueForKeyPath:ofObject:change:context: 方法。
    • keyPath:要观察的属性名称。要和属性声明的名称一致
    • options:回调方法里收到被观察的属性的旧值或新值,枚举类型,系统为我们提供了4个方法
      • NSKeyValueObservingOptionOld:change中会包含key变化之前的值old
      • NSKeyValueObservingOptionNew:change中会包含key变化之后的值new
      • NSKeyValueObservingOptionInitial:change中不包含key的值,会在kvo注册时候立即发通知
      • NSKeyValueObservingOptionPrior:会在值发生改变前发出一次通知,改变后通知依然发出,也就是每个change会有两个通知。值变化之前发送通知的 change 中包含notificationIsPrior = 1; 值发生变化之后的的通知 change 不包含上面提到的notificationIsPrior ,可以跟 willChange 手动通知搭配使用
      • 我们也可以中间以竖线来进行多种选择NSKeyValueObservingOptionOld |。NSKeyValueObservingOptionNew这样change既有new又有old
  • 观察对象发生改变,回调方法observeValueForKeyPath:ofObject:change:context:
    • keyPath:被观察对象的属性
    • object:被观察的对象
    • change:字典类型,存放相关的值,根据options传入的枚举来返回新值旧值或者noticationlsPrior = 1
    • context:注册观察者时候context传入的值
  • 当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除,我们需要在观察者消失之前进行处理,否则就crash了

dealloc这里其实还有点问题

通知部分我们使用addObserver:selector:name:object:会自动帮我们删除观察者,所以我们就没有必要自己写[[NSNotificationCenter defaultCenter]removeObserver:self];来删除观察者

但是我在官方文档中并没有找到KVO是否会为我们进行自动删除的功能

进行一下测试 正常的项目确实有没有没有什么问题

不移除观察者,系统不会直接报错,但是存在隐患,==如果观察者已经销毁了,被观察的对象没有销毁==(比如我们对单例中的一个属性进行观察),然后又产生了KVO message,这时候就抛异常了,EXC_BAD_ACCESS

所以我们使用KVO时最好将dealloc方法写上,移除观察者 [single removeObserver:self forKeyPath:@"height"];

    • observer:观察者
    • keyPath:被观察对象的属性

手动调用KVO

KVO没法实现对数组元素内部的监听,此时就需要我们手动调用KVO

KVO在属性发生改变时的调用时自动的,如果想要手动控制这个调用时机,或想要自己实现KVO属性的调用,则可以通过KVO提供的方法进行调用。

  1. 如果想要手抖调用或者自己实现KVO需要重写下面的方法。该方法返回YES表示允许系统自动调用KVO,NO表示不允许系统自动调用
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 BOOL automatic = NO;
 if ([theKey isEqualToString:@"name"]) {
 automatic = NO;//对该key禁用系统自动通知,若要直接禁用该类的KVO则直接返回NO;
 }
 else {
 automatic = [super automaticallyNotifiesObserversForKey:theKey];
 }
 return automatic;
}
  1. 需要重写setter方法
- (void)setName:(NSString *)name {
 if (name != _name) {
 [self willChangeValueForKey:@"name"];
 _name = name;
 [self didChangeValueForKey:@"name"];
 }
}

不过一般情况下 手动触发KVO感觉没有什么必要 这样会调用两次KVO的响应事件 所以我们不使用这两种方法 直接在需要的地方加will和did 手动触发

KVO的本质

KVO是基于runtime机制实现的

在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指针指向中间类。并且将class方法重写,返回原类的class。

NSLog(@"类对象 -%@", object_getClass(self.person));
 NSLog(@"方法实现 -%p", [self.person methodForSelector:@selector(setName:)]);
 NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.person)));
 
 [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
 
 NSLog(@"类对象 -%@", object_getClass(self.person));
 NSLog(@"方法实现 -%p", [self.person methodForSelector:@selector(setName:)]);
 NSLog(@"元类对象 -%@", object_getClass(object_getClass(self.person)));

我们添加KVO前后

  • person指向的类对象和元类对象以及对应监听的属性的set方法都发生了改变
  • 添加KVO后,person中的isa指向了-NSKVONotifying_Person类对象
  • 添加KVO后,setName:的实现调用是:Foundation中_NSSetLongLongValueAndNotify方法

isa-swizzling(类指针交换) 就是把当前某个实例对象的isa指针指向一个新建造的中间类,在这个新建造的中间类上面做hook方法或者别的事情,这样不会影响这个类的其他实例对象,仅仅影响当前的实例对象。

NSKVONotifying_Person内部实现


- setName:最主要的重写方法,set值时调用通知函数
- class:返回原来类的class
- dealloc
- _isKVOA判断这个类有没有被KVO动态生成子类
- (void)setName:(int)name {
}
- (Class)class {
 return [LDPerson class];
}
- (void)dealloc {
 // 收尾工作
}
- (BOOL)_isKVOA {
 returnYES;
}

isa混写之后如何调用方法

  • 调用监听的属性设置方法,例如setAge:,都会先调用NSKVONotify_Person对应的属性设置方法
  • 调用非监听属性设置方法,如test,会通过NSKVONotify_Person的superclass来找到Person类对象,再调用起Person test方法

为什么重写class方法

  • 如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Person,就会将该类暴露出来

setter的实现不同

截图中我们可以看到set方法的实现在调用KVO后变成调用_NSSetIntValueAndNotify这样一个C函数 我们不知道其本身是什么样 不过我们可以进行测试

- (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");
}

  • 先调用will
  • 然后调用原来的setAge
  • 最后调用did这个方法,并且通知监听者属性值已变,然后监听者执行observe这个方法

KVO部分相关问题

  1. KVO的本质是什么?
  • 利用runtime的API动态生成一个子类,并让实例对象的isa指向这个全新的子类
  • 当修改实例变量对象的属性时候,在全新子类的set方法中会调用Foundation的_NSSetXXXValueAndNotify函数
  • willChangeValueForKey
  • 调用原来的setter
  • didChangeValueForKey:内部会触发监听器的监听方法
  1. 手动触发KVO

上面有写 3. 直接修改成员变量会触发KVO么? 不会触发KVO 4. 通过赋值语句直接打印两个数组的地址是一样的,这是因为我们只用了strong修饰,相当于指针拷贝,所有操作都是对于指针来说的 我们给其中一个设置KVO,修改数组中的值,此时数组地址发生改变,因为KVO的缘故,还是replaceObjectAtIndex这个方法的缘故?

经过测试 mutableArrayValueForKey这一部分应该是导致触发KVO监听的过程 这个方法返回了一个新的数组,导致了原数组地址的改变,触发了KVO的监听

KVC

什么是KVC

定义在NSKeyValueCoding.h中,是一个非正式的协议。KVC提供了一种间接访问其属性方法或成员变量的机制,可以通过==字符串==来访问对应的属性方法或成员变量

在NSKeyValueCoding中提供了KVC通用的访问方法,分别是getter方法valueForKey和setter方法setValue:forKey,以及其衍生的keyPath方法,这两个方法是各个类通用的。并且由KVC提供默认的实现,我们也可以自己重写对应的方法来改变实现。

基础操作

KVC主要对三种类型进行操作,基础数据类型及常量、对象类型、集合类型。 在使用KVC时,直接将属性名当作key,并设置value,即可对属性进行赋值

多级访问

除了对当前对象的属性进行赋值外,还可以对其更深层的对象进行赋值。例如对当前对象的address属性的street属性进行赋值。

KVC进行多级访问时,类似于属性调用一样用点语法进行访问即可 myAccount setValue:@"qwe" forKeyPath@"address.street"

传参nil

如果对非对象传递一个nil值,KVC会调用setNIlValueForKey方法 我们可以重写这个方法来避免

处理非对象

  • setValue时,如果要赋值的对象是基本类型,需要将值封装成NSNumber或者NSValue类型
  • valueForKey时,返回的是id类型的对象,基本数据类型也会被封装成NSNumber或者NSValue

valueForKey可以自动将值封装成对象,但是setValue:forKey:却不行。

我们必须手动讲值类型转换成NSNumber/NSValue类型才能进行传递 initWithBool:(BOOL)value

KVC获取值的过程

我们在KVO里面已经遇到这个问题了 使用ValueForKey这一部分导致了触发KVO监听的过程

现在来详细学习一下

setValue:forKey

  • 程序回先通过setter方法对属性进行设置
  • 如果没有找到set方法,KVC机制会检查+(Bool)accessInstanceVariablesDirectly(直接访问实例变量)方法有没有返回YES(默认返回YES)
    • 如果重写方法成了NO,调用-setValueForUndefinedKey:(为未定义项设置值)抛出异常
    • 返回YES就去找成员变量并直接赋值,按照_key,_isKey,key,iskey的顺讯找,没找到就抛出异常

嫖一张爱尔兰提贝的图

valueForKey

  • 先后顺序搜索getKey、key 、isKey、_getKey、_key五个方法,若某一个方法被实现,取到的即是方法返回的值,后面的方法不再运行。如果是BOOL或者int等类型,会将其包装成一个NSNumber对象

  • 如果五个方法都没有,还是会访问accessInstanceVariablesDirectly方法有没有返回YES(该方法默认返回YES)
    • 如果重写方法成了NO,抛异常
    • 返回YES就去找成员变量并取值,取值顺序为_key、_isKey、key、isKey

KVC操作使用场景

动态的取值和设值

利用KVC动态的取值和设值是最基本的用途

多值操作

KVC可以根据给定的一组key,获取到一组value,并且以字典的形式返回,获取到字典后可以通过key从字典中获取到value

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

同样,也可以通过KVC进行批量赋值。在对象调用setValuesForKeysWithDictionary:方法时,可以传入一个包含key、value的字典进去,KVC可以将所有数据按照属性名和字典的key进行匹配,并将value给User对象的属性赋值。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
NSDictionary *dic = @{@"name" : @"book", @"age" : @"66", @"sex" : @"male"};
 StudentModel *model = [[StudentModel alloc] init];
 
 [model setValuesForKeysWithDictionary:dic];
 NSLog(@"%@",model);
 NSDictionary *modelDic = [model dictionaryWithValuesForKeys:@[@"name", @"age", @"studentSex"]];
 NSLog(@"modelDic : %@", modelDic);

如果 model 属性和 dic 不匹配,可以重写方法 -(void)setValue:(id)value forUndefinedKey:(NSString *)key

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
 if([key isEqualToString:@"sex"]) {
 self.studentSex = (NSString *)value;
 }
}

用KVC来访问和修改私有变量

KVC的本质是操作方法列表以及在内存中查找实例变量。 我们可以利用这个特性访问类的私有变量。

同样如果不想让外界使用KVC的方法访问类的成员变量,可以将accessInstanceVariablesDirectly属性设置为NO

修改一些控件的内部属性

很多UI控件都是由内部UI控件组合而成的,但是Apple中没有提供访问这些控件的API,这样我们就无法正常地访问和修改这些空间的样式。而KVC在大多数情况下可以解决这个问题

作者:复杂化

%s 个评论

要回复文章请先登录注册