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
(成员变量),然后再进行get
或set
那么最重要的也就是从key
到ivar
的查找过程了,官方有做详细说明,但由于都是文字,可能比较晦涩,这里贴上掘金上一个大佬的总结图
发本文时图片一直转存失败,只能放上两张图片链接了
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
- 传入的context,不做修改地在接收change回调时传回给
- 注册的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
,获取插入的新数据addObserver
时指定NSKeyValueObservingOptionNew
change[NSKeyValueChangeNewKey]
即为插入对象组成的数组
-
对于是集合类型的
observed property
,获取删除的数据addObserver
时指定NSKeyValueObservingOptionOld
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
要由firstName
和lastName
两个属性决定
有两种解决办法
在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
,数组中是Employee
,Employee
有一个工资属性salary
,Department
有一个totalSalary
的属性表示公寓内所有雇员的工资和
当用KVO监听totalSalary
时,如果其中的雇员的工资发生变化,如何让监听totalSalary
的对象收到更新通知呢?
totalSalary
的取值由一个集合中的多个对象来确定
这种情况,1对1的方案是无法解决的
唯一的方法就是,在Department
内部,要对集合中每个对象的相应属性,这里也就是Employee
的salary
进行KVO监听,同时对Department
的employees
集合属性监听
然后数据发生变化时统一告知外部监听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 KVO | Swift 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
),change
中newValue
和oldValue
始终是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
解决方案:
- 改回传统方式
- 使用
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)")
}
疑问
- notification.object是啥
- 严格讲KVO中没有notification的概念,回调的数据存在一个dictionary中
observeValueForKeyPath:
有个object
参数,表示被观察者对象
- you're not supposed to override methods in categories.