KVC、KVO笔记

526 阅读9分钟

KVC

KVC,Key-value coding,是一种可以实现对对象的属性进行非直接访问的一种方式,这种方式要求对象必须实现NSKeyValueCoding协议

NSObject实现了NSKeyValueCoding协议的方法,所以它的子类可以直接用KVC的方法

常用方法

// 直接获取或设置属性值
valueForKey:/valueForKeyPath:
setValue:forKey:/setValue:forKeyPath:
// 获取可变类型集合数据方法
mutableArrayValueForKey:/mutableArrayValueForKeyPath:
mutableSetValueForKey:/mutableSetValueForKeyPath:
  • 当书写keyPath时,第一个属性部分是相对于message receving object而言的
    • [department valueForKeyPath:@"employee.salary"]
    • employee是相对于department而言
  • 当获取可变的集合类型数据时,得到的是一个代理对象,可以直接对该集合进行操作,操作的结果会传递到真正的非可变集合数据中
  • KVC的方法对对象类型属性和非对象类型属性(如int)等同视之

集合操作符(CollectionOperator)

当获取集合类型数据时,支持加入集合操作符,这样可以对返回的集合数据进行合并、求平均、求最值等简单操作,最终返回的是计算的结果值

操作符格式是

keypathToCollection.@collectionOperator.keypathToProperty

举例

/// 获取交易信息中最早的时间
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];

/// 获取所有交易信息中的交易人(payee)信息,而且交易人不重复
/// 重复的判断需要`isEqual`方法的支持
NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
/// 对集合的集合使用操作符
NSArray* moreTransactions = @[<# transaction data #>];
NSArray* arrayOfArrays = @[self.transactions, moreTransactions];
NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
集合操作符功能
@count元素个数无需keypathToProperty
@avg/@sum求平均、求和
@max/@min求最值
@distinctUnionOfObjects聚合对象,对象不重复
@unionOfObjects聚合对象,允许重复
@distinctUnionOfArrays将外层数组中每个数组里的每个对象的属性进行非重复聚合
@unionOfArrays功能和上面类似,结果有重复
@distinctUnionOfSets对集合的集合进行非重复聚合

KVC原理

本质上,实现了NSKeyValueCoding协议的NSObject在执行KVC的方法时,就是通过key去找匹配的ivar(成员变量),然后再进行getset

那么最重要的也就是从keyivar的查找过程了,官方有做详细说明,但由于都是文字,可能比较晦涩,这里贴上掘金上一个大佬的总结图

setter

getter

发本文时图片一直转存失败,只能放上两张图片链接了

KVO

指定一个对象的某个属性,当该属性值发生变化时,可以通知给其他对象。这个机制就叫做KVO(Key-value observing)

使用

  • addObserver...方法注册observer,observeValueForKeyPath:...接收change通知
  • addObserver方法并没有对observing object、observer 和 context强引用,所以可能需要手动持有
  • 建议在注册observer时传递context
    • 传入的context,不做修改地在接收change回调时传回给observeValueForKeyPath:
    • 当子类和父类都KVO了属性时,子类的observeValueForKeyPath:方法的执行会覆盖掉父类的
    • 所以父类KVO的change都会走到子类的中,子类可能无法处理或者可能因为代码质量不高导致错误
    • 此时如果通过context判断,可以知道是否是当前类要处理的change,如果是父类的那可以直接交给父类处理,而不会覆盖父类了
    • 可以给每个要KVO属性的类都声明一个context,用静态变量的地址很合适--static void *xxxContext = &xxxContext;
    • 而且官方建议,如果通过context判断发现当前类无法处理,务必将事件交给super
  • 注册的KVO不会因为对象dealloc而自动解除,需要手动解除
    • 由于observer dealloc时不会自动解除KVO,所以observed object仍可能发送通知过来,导致给released object发送消息而crash

Options

注册observers时有哪些option可以传

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
	NSKeyValueObservingOptionNew = 0x01,
	NSKeyValueObservingOptionOld = 0x02,
	NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
	NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
  • NSKeyValueObservingOptionInitial,若设置该选项则表示,addObserver方法结束前,observeValueForKeyPath:方法就会执行一次
  • NSKeyValueObservingOptionPrior,表示property更改前进行一次通知

Notification

收到的更改通知是NSDictionary<NSKeyValueChangeKey, id> *类型

typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
/* Keys for entries in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;
  • NSKeyValueChangeKindKey对应value值有

    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    	NSKeyValueChangeSetting = 1,
    	NSKeyValueChangeInsertion = 2,
    	NSKeyValueChangeRemoval = 3,
    	NSKeyValueChangeReplacement = 4,
    };
    
  • NSKeyValueChangeIndexesKey对应的value值是NSIndexSet类型

  • 对于是集合类型的observed property,获取插入的新数据

    1. addObserver时指定NSKeyValueObservingOptionNew
    2. change[NSKeyValueChangeNewKey]即为插入对象组成的数组
  • 对于是集合类型的observed property,获取删除的数据

    1. addObserver时指定NSKeyValueObservingOptionOld
    2. change[NSKeyValueObservingOptionOld]即为被删除对象组成的数组

Dependent Key

dependent key是指,有一个属性A(通常是一个computed property)的值是由其他1个或多个属性值决定,那这些属性就是A的依赖属性,即为dependent key

当使用KVO监听A时,如果依赖属性的值发生变化,我们自然的也收到A变化的通知

本小节就是为了解决该问题

该问题的处理方法根据A和依赖属性之间的对应关系决定,具体关系可以是1对1,或者1对N

1对1

比如Person对象中fullName这个computed property要由firstNamelastName两个属性决定

有两种解决办法

Person中重写该系统方法

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

或者实现一个自定义方法keyPathsForValuesAffecting[Key]

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

1对N

比如有一个Department,拥有一个数组类型的属性employees,数组中是EmployeeEmployee有一个工资属性salaryDepartment有一个totalSalary的属性表示公寓内所有雇员的工资和

当用KVO监听totalSalary时,如果其中的雇员的工资发生变化,如何让监听totalSalary的对象收到更新通知呢?

totalSalary的取值由一个集合中的多个对象来确定

这种情况,1对1的方案是无法解决的

唯一的方法就是,在Department内部,要对集合中每个对象的相应属性,这里也就是Employeesalary进行KVO监听,同时对Departmentemployees集合属性监听

然后数据发生变化时统一告知外部监听totalSalary的对象

KVO Compliance

如何让类、属性支持KVO

  • 默认情况下,系统会自动为我们声明的property添加KVO支持的逻辑
  • 但根据自定义类的功能不同,可能也有一些特殊情况可能需要手动处理,比如前面提到的Dependent Key的情况

可以通过两种形式:自动手动

手动和自动是可以并存的,并非只能选择一种实现方式

系统自动KVO

系统借助KVC特性,在NSObject内部做了默认实现

  • NSObject内部有KVO进行发通知的默认实现
  • 这个默认实现是基于KVC的
  • 因为KVC内部会寻找设置属性或获取属性的方法,在属性发生变化时,NSObject的默认实现会第一时间捕捉到,并通知给Observer

并不是系统框架中所有类的属性都支持KVO,因为很多属性并不能满足KVC或者Dependent Key的要求。所以要以文档说明为准

手动KVO

对于一些特殊情况,或者不支持KVC的情况,系统也允许我们手动实现KVO

  • 重写+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法,通过key针对需要手动通知的属性,返回FALSE
  • 在属性发生修改时执行 willChangeValueForKey:didChangeValueForKey:
  • 对于集合类型的属性更改的情况,还要为上面两个方法传入修改的index信息

KVO in Swift

Automatic Change Notification依赖的是NSObject的默认实现,所以要求参与到KVO的对象必须是NSObject的子类

Manual Change Notification中重写的+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key方法也是NSObject中的

  • KVO本质是依赖于OC的Runtime,所以要求必须是NSObject的子类才可以被观察
  • 在Swift中也是如此,Swift并没有重新实现KVO,仍是基于OC的Runtime
  • 所以纯Swift的类不支持KVO(但可以使用属性的willSet和didSet特性)

如何实现

由于Swift兼容了Objective C语言的语法,所以在Swift中可以使用所有KVO的api,除此之外,也针对Swift有更简洁的API,如下所示:

首先要求被观察的属性,必须使用@objc@dynamic标记

class MyObjectToObserve: NSObject {
    @objc dynamic var myDate = NSDate(timeIntervalSince1970: 0) // 1970
    func updateDate() {
        myDate = myDate.addingTimeInterval(Double(2 << 30)) // Adds about 68 years.
    }
}

添加和移除监听代码如下所示:

class MyObserver: NSObject {
    @objc var objectToObserve: MyObjectToObserve
    var observation: NSKeyValueObservation?
    init(object: MyObjectToObserve) {
        objectToObserve = object
        super.init()
        observation = observe(
            \.objectToObserve.myDate,
            options: [.old, .new]
        ) { object, change in
            print("myDate changed from: \(change.oldValue!), updated to: \(change.newValue!)")

        }
    }

    func invalidateObservation() {
        observation?.invalidate()
    }
}

  • 添加观察者的方式可以使用closure
  • 观察的属性可以使用keypath
  • 移除观察使用invalidate方法,且即使忘记移除,在NSKeyValueObservation销毁时,也会自动执行invalidate

与传统KVO方式对比

对比维度Objective-C KVOSwift KVO(NSKeyValueObservation)
依赖机制完全依赖 Objective-C Runtime依然依赖 Objective-C Runtime(@objc dynamic
使用前提对象必须继承自 NSObject,属性无需特殊标记对象必须继承自 NSObject,属性需标记为 @objc dynamic
属性声明方式@property + 自动生成 setter@objc dynamic var(动态派发并暴露给 OC Runtime)
注册方式addObserver:forKeyPath:options:context:observe(_:options:changeHandler:)
回调方式重写 observeValueForKeyPath:...使用闭包(changeHandler)回调
移除监听方式必须手动调用 removeObserver:自动移除(释放时自动解绑)
类型安全性不安全(KeyPath 是字符串)类型安全(使用 Swift 的 KeyPath 表达式)
代码结构所有逻辑集中在一个方法中每次观察都是独立闭包,结构清晰
易错点忘记移除观察者会崩溃,字符串拼错难发现几乎无崩溃风险,编译期可检查类型和 KeyPath
对 Swift 支持度属于兼容方式,较为笨重更符合 Swift 风格,简洁且安全
脱离 OC Runtime 能力无法脱离(完全依赖)仍无法完全脱离(仍依赖 OC Runtime)
更现代的替代方案可替代为 Combine、SwiftUI 的 @Published

Swift KVO坑

很可惜的是,Swift的KVO写法有一个多年未解决的bug(2017-2025)

问题描述:如果观察的对象是一个RawRepresentable类型数据时(如enum),changenewValueoldValue始终是nil


timeControlStatusObservation = avPlayer.observe(
    \AVQueuePlayer.timeControlStatus,
    options: [.old, .new],
    changeHandler: {
        [weak self] (player, change) in

        print("timeControlState: \(change.oldValue), \(change.newValue), \(self?.avPlayer.timeControlStatus)")
    })

// 输出结果:
timeControlState: nil, nil, Optional(playing) 
timeControlState: nil, nil, Optional(paused)

问题链接:swift - KVO - change.newValue and change.oldValue are nil

苹果官方Github上的讨论:Fix KVO for RawRepresentable types

解决方案:

  1. 改回传统方式
  2. 使用Combine方式
let cancellable = player.currentItem?.publisher(for: \.status).sink { status in
      print("AVPlayer Status Change: \(status)")
    }
    let cancellable = player.publisher(for: \.timeControlStatus).sink { timeControlStatus in

      print("AVPlayer TimeControlStatus Change: \(timeControlStatus)")
    }

疑问

  1. notification.object是啥
    • 严格讲KVO中没有notification的概念,回调的数据存在一个dictionary中
    • observeValueForKeyPath:有个object参数,表示被观察者对象
  2. you're not supposed to override methods in categories.

参考