iOS八股文(十九)KVC、KVO

589 阅读6分钟

KVC(Key-Value Coding)健值编码

在iOS开发中,允许直接通过Key来访问成员变量,动态去查找访问成员变量。

@interface OSBankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) NSObject* owner;                         // A to-one relation
@property (nonatomic) NSArray< NSObject* >* transactions; // A to-many relation
@end
- (void)test1 {
    OSBankAccount *account = [[OSBankAccount alloc] init];
    // kvc
    [account setValue:@(100.0) forKey:@"currentBalance"];
    // set方法设置
    [account setCurrentBalance:@(100.0)];
}

相对于调用set方法设置属性值,使用kvc更加灵活,其中的key可以在运行时动态确定。在一些应用场景kvc使用起来非常的方便。

NSObject(NSKeyValueCoding)

KVC的Api都声明在NSObject的分类NSKeyValueCoding中,所以如果想使用KVC务必确认该对象是NSObject的子类。

其中比较重要的API有:

@property (class, readonly) BOOL accessInstanceVariablesDirectly;

- (void)setValue:(nullable id)value forKey:(NSString *)key;

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

- (id)valueForKey:(NSString *)key;

- (nullable id)valueForKeyPath:(NSString *)keyPath;

key&keyPath的区别,如果某个属性是一个对象,需要设置该属性的某个属性的时候,就可以使用keyPath,一步到位来设置或者获取属性。such as:

@interface OSBankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance;              // An attribute
@property (nonatomic) OSOwer* owner;                         // A to-one relation
@property (nonatomic) NSArray< NSObject* >* transactions; // A to-many relation
@end


@interface OSOwer : NSObject
@property (nonatomic, copy) NSString *name;
@end

如果想要获取或者设置owner的name属性,可以使用keyPath:

-(void)test2 {
    OSBankAccount *account = [[OSBankAccount alloc] init];
    // kvc
    [account setValue:@"kitty" forKeyPath:@"owner.name"];
    
    NSLog(@"name is %@",account.owner.name);
}

设置属性查找顺序

setValue:forKey:的默认实现,给定keyvalue参数作为输入,尝试使用以下过程将名为key的属性设置为接收调用的对象中的value

  1. 按顺序查找第一个名为set<Key>:_set<Key>的访问器。如果找到,请使用输入值(或根据需要打开的值)调用它并完成。
  2. 如果没有找到简单的访问器,并且类方法accessInstanceVariablesDirectly返回YES,请按此顺序查找名称为_<key>_is<Key><key>is<Key>的实例变量。如果找到,请直接使用输入值(或未包装的值)设置变量并完成。
  3. 找不到访问器或实例变量后,调用setValue:forUndefinedKey:。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于键的行为。

访问属性查找顺序

valueForKey:的默认实现,给定一个key参数作为输入,执行以下过程,从接收valueForKey:调用的类实例中操作:

  1. 按该顺序在实例中搜索第一个以get<Key><key>is<Key>_<key>的名字找到的第一个访问器方法。如果找到,请调用它,然后继续执行第5步。否则,请继续下一步。

  2. 如果没有找到简单的访问器方法或集合访问方法组,并且接收器的类方法accessInstanceVariablesDirectly返回YES,请按此顺序搜索名为_<key>_is<Key><key>is<Key>的实例变量。如果找到,请直接获取实例变量的值,然后继续执行第3步。否则,请继续执行第4步。

  3. 如果检索到的属性值是对象指针,只需返回结果即可。如果该值是NSNumber支持的标量类型,请将其存储在NSNumber实例中并返回。如果结果是NSNumber不支持的标量类型,请转换为NSValue对象并返回。

  4. 如果所有其他方法都失败,请调用valueForUndefinedKey:。默认情况下,这会引发异常,但NSObject的子类可能会提供特定于键的行为

KVC的使用场景

  1. crash防护,可以自定义valueForUndefinedKey:从而实现crash到控制台打印的友好处理方式。
  2. json转model
  3. KVO的实现

KVO(NSKeyValueObserving)观察者模式

Key-Value Observing 可翻译成健值观察,是观察者模式再iOS开发中的具体体现,同时也是Object-C动态性的具体表现。在开发过程中,如果需要外部动态的获得对象的某个属性变化的时机以及变化前后的值,这时候就可以使用KVO来完成。显然KVO也属于信息传递的一种方式。

KVO使用实例

需求:

image.png 在实现红框内的colloectionView的时候,其中item的个数是动态变化的(根据服务器返回的数据),所以高度有可能动态变化,这时候如果使用kvo来观察colloectionView的contentSize,从而来更新高度就非常巧妙。

AVFounditon中获取AVPlayer的播放进度,播放状态,也需要使用KVO来观察。

#pragma mark - 监听
- (void)currentItemAddObserver {
    // 监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
    [self.player.currentItem addObserver:self forKeyPath:@"status" options: (NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew) context:nil];
    // 监控缓冲加载情况属性
    [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    // 缓冲不足暂停了
    [self.player.currentItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    // playbackLikelyToKeepUp
    [self.player.currentItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    // rate
    [self.player addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil];
    }

使用注意事项

在使用KVO的时候容易引起⚠️crash。所以需要多多注意。

  1. keyPath 不能为空字符串
  2. 注意在适合的地方removeObersver,如果观察实例比被观察实例先释放,这时候改变观察属性,会产生崩溃。
  3. 没有添加,直接移除观察关系,也会产生崩溃

KVO实现原理

  1. addObserver:forKeyPath:options:context:context调用的时候,会自动生成并注册一个该对象(被观察的对象)对应类的子类,取名NSKVONotify_Class,并且将该对象的isa指针指向这个新的类。
  2. 在该子类内部实现4个方法-被观察属性的set方法class方法isKVOdelloc
  3. 最关键的是set方法中,先调用willChangeValueForKey,再给成员变量赋值,最后调用didChangeValueForKeywillChangeValueForKeydidChangeValueForKey需要成对出现才能生效,在didChangeValueForKey中会去调用观察者的observeValueForKeyPath: ofObject: 方法。
  4. 重写class方法,这样避免外部感知子类的存在,同时防止在一些使用isKindOfClass判断的时候出错。
  5. isKVO方法作为实现KVO功能的一个标识。
  6. delloc里面还原isa指针

FBKVOController浅析

在了解了KVO原理和KVO的注意事项之后,可谓对KVO是又爱又恨,因为其原理让我们在类之前通信增加了一种方式,但确实容易crash,针对这个问题,FaceBook的小伙伴的第三方库FBKVOController就巧妙的解决了这一问题。

  1. 统一观察者 在FBKVOController中有个单例-[_FBKVOSharedController sharedController]来统一观察所有的对象,所有的观察回调也都先来到_FBKVOSharedController中的observeValueForKeyPath:ofObject: change:context:方法中,然后再派发给对应KVOControllerBlock或者Action(Seletor)

image.png

在添加观察者的时候,会把添加的信息生成一个_FBKVOInfo对象。

image.png

  1. 移除观察者时机 在使用FBKVOController添加观察者的时候会动态关联对象,该对象的类为FBKVOController,而在类释放的时候,会调用类的delloc方法,关联的对象也会走delloc方法,而在这个时候统一去移除观察者。

image.png

image.png

  1. 防止重复添加

在添加的时候会把_FBKVOInfo对象存起来,再次添加的时候去比较,如果存在,就不继续添加,在判断重复的时候重写了_FBKVOInfo中的hash方法,即keyPath重复就不继续添加。

image.png

image.png

手动关闭/打开KVO

在被观察的类中,重写automaticallyNotifiesObserversForKey:方法,对应被观察的key返回No,这时候就会不再调用观察者的observeValueForKeyPath:ofObject: change:context:方法。

/// 手动关闭KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

而在被观察的类中,成对的调用willChangeValueForKeydidChangeValueForKey,即使被观察的key没有发生变化也会手动的触发KVO的回调

- (void)invokeKVO{
    [self willChangeValueForKey:@"name"];
    
    [self didChangeValueForKey:@"name"];
}

参考链接🔗