KVO 简介
-
相关文档
Key-Value Observing Programming Guide
Objective-C 的 KVO(二):NSKeyValueObserving.h 代码注释
-
KVO 的概念
KVO(Key-Value Observing),翻译成中文叫:键值观察,是苹果提供的一套事件通知机制,允许观察者监听被观察者给定属性的值的改变。当被观察者给定属性的值发生改变时,会触发观察者的监听方法来通知观察者。KVO 是在 MVC 架构中各层之间进行通信的一种特别有用的技术
KVO 可以监听被观察者中单个属性的改变,也可以监听被观察中集合元素的改变。KVO 监听集合对象的元素的改变时,需要通过 KVC 的集合代理方法获取可变集合代理对象,并使用可变集合代理对象进行操作。当可变集合代理对象内部的元素发生改变时,会触发 KVO 的监听方法。集合对象包括
NSArray
、NSOrderedSet
、NSSet
KVO 和 KVC 有着密切的联系,如果想要深入了解 KVO,建议先学习 KVC
-
KVO 的相关方法
#pragma mark - 相关枚举值与常量的定义 // 被观察者给定属性的观察配置选项,包括观察的内容与发送通知的时机 typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) { NSKeyValueObservingOptionNew = 0x01, // 观察属性的新值,默认在属性改变之后发送通知 NSKeyValueObservingOptionOld = 0x02, // 观察属性的旧值,默认在属性改变之后发送通知 NSKeyValueObservingOptionInitial = 0x04, // 在添加观察者时,立即发送一次通知。在每次属性改变之后,也会发送一次通知 // 如果需要观察属性的新值,则配合 NSKeyValueObservingOptionNew 使用 // 如果需要观察属性的旧值,则配合 NSKeyValueObservingOptionOld 使用 NSKeyValueObservingOptionPrior = 0x08 // 在属性改变之前和属性改变之后,各发一次通知 // 在属性改变之前发送的通知中,始终包含 NSKeyValueChangeNotificationIsPriorKey 条目,始终不包含 NSKeyValueChangeNewKey 条目 // 如果需要观察属性的新值,则配合 NSKeyValueObservingOptionNew 使用。属性的新值只包含在属性改变之后的通知中 // 如果需要观察属性的旧值,则配合 NSKeyValueObservingOptionOld 使用。属性的旧值既会包含在属性改变之前的通知中,也会包含在属性改变之后的通知中 }; // 被观察者给定属性的改变类型(即 change 字典中 NSKeyValueChangeKindKey 条目的可能值) typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, // 对属性进行了设值操作(用于非集合类型和集合类型) NSKeyValueChangeInsertion = 2, // 对属性进行了插入操作(仅用于集合类型) NSKeyValueChangeRemoval = 3, // 对属性进行了移除操作(仅用于集合类型) NSKeyValueChangeReplacement = 4, // 对属性进行了替换操作(仅用于集合类型) }; // 被观察者给定集合属性的事件类型 typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) { NSKeyValueUnionSetMutation = 1, // 对应于属性的改变类型 NSKeyValueChangeInsertion, 对应于调用方法 -[NSMutableSet unionSet] NSKeyValueMinusSetMutation = 2, // 对应于属性的改变类型 NSKeyValueChangeRemoval, 对应于调用方法 -[NSMutableSet minusSet] NSKeyValueIntersectSetMutation = 3, // 对应于属性的改变类型 NSKeyValueChangeRemoval, 对应于调用方法 -[NSMutableSet intersectSet] NSKeyValueSetSetMutation = 4 // 对应于属性的改变类型 NSKeyValueChangeReplacement, 对应于调用方法 -[NSMutableSet setSet] }; // change 字典中所包含的键 typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM; // 将 NSString* 取别名为 NSKeyValueChangeKey,即 change 字典中键的类型为 NSString* FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey; // 用于标识被观察者给定属性的改变类型,对应字符串 @"kind" FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey; // 用于标识被观察者给定属性的新值,对应字符串 @"new" FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey; // 用于标识被观察者给定属性的旧值,对应字符串 @"old" FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey; // 用于标识被观察者给定集合属性被更改的索引,对应字符串 @"indexes" FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey; // 用于标识是否为给定属性改变之前所发送的通知,对应字符串 @"notificationIsPrior" #pragma mark - KVO 使用 3 步曲 // 注册观察者 // @param.observer 观察者 // @param.keyPath 被观察者给定属性的关键路径 // @param.options 观察的配置选项,用于确定观察者收到的通知中所包含的内容,以及通知的发送时间 // @param.context 用于在通知触发时传递给观察者的上下文,在观察者的监听方法中可以接收到这个数据,是 KVO 中的一种传值方式 // 可以传入任意类型的 Objective-C 对象或者 C 指针。如果传入的是一个 Objective-C 对象,则必须在移除观察者之前持有它的强引用,否则在观察者的监听方法中访问 context 就可能导致程序 Crash // @note 方法的调用者,即为被观察者 -(void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; // 通知触发时的回调 // @param.keyPath 被观察者给定属性的关键路径 // @param.object 被观察者 // @param.change 包含被观察者给定属性详细的更改信息 // @param.context 在注册观察者时传递的上下文 // @note 观察者需要实现此回调以监听被观察者中给定属性的值的改变 -(void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; // 注销观察者 // @param.observer 观察者 // @param.keyPath 被观察者给定属性的关键路径 // @param.context 在注册观察者时传递的上下文 // @note 方法的调用者,即为被观察者 -(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; -(void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath; #pragma mark - KVO 与 NSArray // 注册观察者 // @param.observer 观察者 // @param.indexes 数组中被观察的元素的索引 // @param.keyPath 数组元素被观察属性的关键路径 // @param.options 观察的配置选项,用于确定观察者收到的通知中所包含的内容,以及通知的发送时间 // @param.context 用于在通知触发时传递给观察者的上下文,在观察者的监听方法中可以接收到这个数据,是 KVO 中的一种传值方式 // 可以传入任意类型的 Objective-C 对象或者 C 指针。如果传入的是一个 Objective-C 对象,则必须在移除观察者之前持有它的强引用,否则在观察者的监听方法中访问 context 就可能导致程序 Crash // @note 在此方法中,被观察者不是方法的调用者(NSArray、NSMutableArray) // 在此方法中,被观察者是数组中的元素 -(void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; // 注销观察者 // @param.observer 观察者 // @param.indexes 数组中要注销观察者的元素的索引 // @param.keyPath 要注销观察者的数组元素给定属性的关键路径 // @param.context 在注册观察者时传递的上下文 // @note 在此方法中,要注销观察者的不是方法的调用者(NSArray、NSMutableArray) // 在此方法中,要注销观察者的是数组中的元素 -(void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath context:(nullable void *)context; -(void)removeObserver:(NSObject *)observer fromObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath; #pragma mark - 手动触发 KVO // 用于手动触发非集合类型的被观察者给定属性的 KVO 通知,以下两个方法必须始终成对地的调用 // @param.key 被观察者中需要手动触发 KVO 通知的属性的关键路径 -(void)willChangeValueForKey:(NSString *)key; -(void)didChangeValueForKey:(NSString *)key; // 用于手动触发有序集合类型的被观察者给定属性的 KVO 通知,以下两个方法必须始终成对地的调用 // @param.changeKind 集合元素的改变类型(插入、移除、替换) // @param.indexes 需要手动触发 KVO 通知的集合元素的索引 // @param.key 集合元素中需要手动触发 KVO 通知的属性的关键路径 -(void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key; -(void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key; // 用于手动触发无序集合类型的被观察者给定属性的 KVO 通知,以下两个方法必须始终成对地的调用 // @param.key 集合元素中需要手动触发 KVO 通知的属性的关键路径 // @param.mutationKind 集合事件的类型(union、minus、intersect、set) // @param.objects 用于集合事件的集合元素 -(void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects; -(void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects; #pragma mark - KVO 的触发控制:自动触发 or 手动触发 // 用于控制被观察者中给定 key 所标识的属性的触发模式 // @param.key 需要控制触发模式的属性的关键路径 // @return YES - 自动触发,NO - 手动触发 // @note 此方法的默认实现会在被观察者所属的类中搜索名称为 +automaticallyNotifiesObserversOf<Key> 的方法 // 如果找到,则返回 +automaticallyNotifiesObserversOf<Key> 的调用结果 // 如果找不到,则返回 YES +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key; #pragma mark - KVO 的属性依赖 // 用于控制被观察者中给定 key 所标识的属性依赖的其他属性 // @param.key 用于标识依赖属性的关键路径 // @return 用于标识被依赖属性的关键路径的集合 // @note 此方法的默认实现会在被观察者所属的类中搜索名称为 +keyPathsForValuesAffecting<Key> 的方法 // 如果找到,则返回 +keyPathsForValuesAffecting<Key> 的调用结果 // 如果找不到,为了向后的二进制兼容性,则返回根据之前已弃用的 +setKeys:triggerChangeNotificationsForDependentKey: 的调用所提供的信息计算出的关键路径的集合 +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key; #pragma mark - 被观察者中全部的观察信息 // 包括每个观察者的 observer、keyPath、options、context @property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;
KVO 的基本使用
- KVO 使用三步曲
-
被观察者添加(注册)观察者,即
被观察者调用-addObserver:forKeyPath:options:context:
方法为自己添加观察者 -
观察者实现监听方法以接收被观察者给定属性改变的通知,即
在观察者所属的类中实现-observeValueForKeyPath:ofObject:change:context:
方法以接收被观察者给定属性改变的通知
如果一个对象被注册为观察者,则该对象必须能响应此回调方法,即该对象所属类中必须实现此回调方法。当被观察者给定属性发生改变时就会调用此回调方法,没有实现会导致程序 Crash -
被观察者移除(注销)观察者,即
被观察者调用-removeObserver:forKeyPath:context:
方法移除观察者。因为被观察者在调用 KVO 注册方法添加观察者之后,并不会对观察者进行强引用,所以需要注意观察者的生命周期,被观察者需要在观察者被销毁之前调用此方法以移除观察者,否则当被观察者给定属性的值再次改变时,KVO 向已释放的观察者再次发送属性值改变的通知,可能会导致程序 Crash代码示例:
// ViewController.m #import "ViewController.h" #import "Person.h" @interface ViewController () @property (nonatomic, strong) Person* aPerson; @end @implementation ViewController // 懒加载 -(Person *)aPerson { if (!_aPerson) { _aPerson = [[Person alloc] init]; } return _aPerson; } -(void)viewDidLoad { [super viewDidLoad]; // 1.被观察者添加(注册)观察者 [self.aPerson addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; } // 2.观察者实现监听方法以接收被观察者给定属性改变的通知 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"keyPath = %@", keyPath); NSLog(@"object = %@", object); NSLog(@"change = %@", change); NSLog(@"context = %@", context); } // 修改 self.aPerson 对象的 name 属性 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { static int i = 0; NSString* name = [NSString stringWithFormat:@"hcg%02d", i++]; self.aPerson.name = name; } -(void)dealloc { // 3.被观察者移除(注销)观察者 [self.aPerson removeObserver:self forKeyPath:@"name" context:NULL]; } @end
// keyPath = name // object = <Person: 0x60000309f940> // change = { // kind = 1; // new = hcg00; // } // context = (null)
-
KVO 中 change 字典的使用
/* change 字典用于存储被观察者给定属性详细的更改信息,其可能包含以下 5 个 key: 1.NSKeyValueChangeKey const NSKeyValueChangeKindKey = @"kind"; 用于标识被观察者给定属性的改变类型,change 字典里默认会包含这个 key,其 value 为枚举类型 NSKeyValueChange typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, // 对属性进行了设值操作(用于非集合类型和集合类型) NSKeyValueChangeInsertion = 2, // 对属性进行了插入操作(仅用于集合类型) NSKeyValueChangeRemoval = 3, // 对属性进行了移除操作(仅用于集合类型) NSKeyValueChangeReplacement = 4, // 对属性进行了替换操作(仅用于集合类型) }; 2.NSKeyValueChangeKey const NSKeyValueChangeNewKey = @"new"; 用于存储被观察者给定属性的新值 如果观察选项 options 中传入了 NSKeyValueObservingOptionNew,则 change 字典里就会包含这个 key 3.NSKeyValueChangeKey const NSKeyValueChangeOldKey = @"old"; 用于存储被观察者给定属性的旧值 如果观察选项 options 中传入了 NSKeyValueObservingOptionOld,则 change 字典里就会包含这个 key 4.NSKeyValueChangeKey const NSKeyValueChangeIndexesKey = @"indexes"; 用于存储被观察者集合属性被更改的索引 如果被观察者是集合类型的对象,且进行的是(插入、移除、替换)操作,则 change 字典里就会包含这个 key 这个 key 对应的 value 是一个 NSIndexSet 类型的对象,包含集合中被更改的元素的索引 5.NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey = @"notificationIsPrior"; 用于标识是否为给定属性改变之前所发送的通知 如果观察选项 options 中传入了 NSKeyValueObservingOptionPrior,则在给定属性改变之前通知的 change 字典里就会包含这个 key 这个 key 对应的 value 是 NSNumber 包装的 YES,开发者可以这样来判断是不是在给定属性改变之前发送的通知 [change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES] */ // 将 NSString* 取别名为 NSKeyValueChangeKey,即 change 字典中键的类型为 NSString* typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM; NSDictionary<NSKeyValueChangeKey, id> * change;
-
KVO 中 context(上下文)的使用
KVO 注册观察者的方法
-addObserver:forKeyPath:options:context:
中的context
参数是用于在通知触发时传递给观察者的上下文,可以传入任意类型的 Objective-C 对象或者 C 指针
并且在观察者的监听方法-observeValueForKeyPath:ofObject:change:context:
中可以接收到这个数据context
参数是 KVO 中传值的一种方式,如果传入的是一个 Objective-C 对象,则必须在移除观察者之前持有它的强引用,否则在观察者的监听方法中访问context
就可能导致程序 CrashKVO 的每个观察者中只有一个监听的方法,通常情况下开发者可以在注册观察者时指定
context
为NULL
,并在监听的方法中通过object
和keyPath
来判断触发 KVO 的来源。但是如果同一个观察者同时监听多个不同对象的不同属性,则开发者需要在监听的方法中写大量的if-else
嵌套来判断触发 KVO 的来源,如下所示:
这时候通过使用context
就可以很好地解决这个问题:- 在每个被观察者所属的类中为每个被观察的属性声明一个独一无二的
context
值,用于精确地标识被观察的属性(这里苹果推荐使用唯一命名的静态变量的地址作为context
的值) - 在注册观察者的方法中传递该
context
的值 - 在观察者监听的方法中通过
context
的值区分不同的被观察者的不同属性
context
的优点有:嵌套少、性能高、安全性好、扩展性强、可以更精确的定位被观察者的属性使用
context
时的注意点:- 如果传入的
context
是一个 Objective-C 对象,则必须在移除观察者之前持有它的强引用,否则在观察者的监听方法中访问context
就可能导致程序 Crash - 因为
context
是void *
类型的指针,所以如果context
为空,则应该传NULL
而不应该传nil
- 在每个被观察者所属的类中为每个被观察的属性声明一个独一无二的
KVO 触发监听的方式
-
自动触发监听 && 手动触发监听
① 如何自动触发监听
-
如果观察者监听的是被观察者非集合类型的属性的值的改变,则通过以下方式改变被观察者给定属性的值会自动触发监听:
- 使用点语法
- 使用
setter
方法 - 使用 KVC 的
-setValue:forKey:
方法 - 使用 KVC 的
-setValue:forKeyPath:
方法
-
如果观察者监听的是被观察者集合类型的属性的元素的改变,则需要通过 KVC 的集合代理方法获取可变集合代理对象,并使用可变集合代理对象进行操作,当可变集合代理对象内部的元素发生改变时,会自动触发 KVO 的监听方法。集合对象包括
NSArray
、NSOrderedSet
、NSSet
② 如何手动触发监听
-
如果要手动触发对非集合类型的属性或者成员变量的监听,则使用以下方法:
-(void)willChangeValueForKey:(NSString *)key; -(void)didChangeValueForKey:(NSString *)key;
-
如果要手动触发对有序集合类型(
NSArray
、NSOrderedSet
)的属性或者成员变量的监听,则使用以下方法:-(void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key; -(void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
-
如果要手动触发对无序集合类型(
NSSet
)的属性或者成员变量的监听,则使用以下方法:-(void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects; -(void)didChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;
-
-
控制监听的触发方式
可以通过重写被观察者中的以下方法,控制 KVO 触发监听的方式(自动触发 or 手动触发):
// 用于控制被观察者中给定 key 所标识的属性的触发模式 // @param.key 需要控制触发模式的属性的关键路径 // @return YES - 自动触发,NO - 手动触发 // @note 此方法的默认实现会在被观察者所属的类中搜索名称为 +automaticallyNotifiesObserversOf<Key> 的方法 // 如果找到,则返回 +automaticallyNotifiesObserversOf<Key> 的调用结果 // 如果找不到,则返回 YES +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
如果开发者只允许外界观察
Person
类的name
属性,不允许外界观察Person
类的age
属性和height
属性
则可以在Person
类中添加如下方法:
这样,外界就只能观察Person
类的name
属性
即使外界注册了对Person
类age
属性和height
属性的监听,那么在age
属性和height
属性发生改变时,也不会触发观察者监听的方法:
输出结果如下所示:// 执行 [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:kPersonNameContext]; 时,输出 +[Person automaticallyNotifiesObserversForKey:], key = name +[Person automaticallyNotifiesObserversOfName] // 执行 [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:kPersonAgeContext]; 时,输出 +[Person automaticallyNotifiesObserversForKey:], key = age +[Person automaticallyNotifiesObserversOfAge] // 执行 [self.person addObserver:self forKeyPath:@"height" options:NSKeyValueObservingOptionNew context:kPersonHeightContext]; 时,输出 +[Person automaticallyNotifiesObserversForKey:], key = height +[Person automaticallyNotifiesObserversOfHeight] // 点击屏幕时,输出 处理 self.person.name 发生改变的情况
注意:
- 当被观察者调用
-addObserver:forKeyPath:options:context:
添加观察者时,-addObserver:forKeyPath:options:context:
的内部会调用被观察者的+automaticallyNotifiesObserversForKey:
以确定关键路径(keyPath
)所标识的属性的触发方式 - 当被观察者调用
-addObserver:forKeyPath:options:context:
添加观察者时,如果观察选项(options
)包含NSKeyValueObservingOptionInitial
,则由NSKeyValueObservingOptionInitial
触发的 KVO 通知是无法被+automaticallyNotifiesObserversForKey:
所阻止的
- 当被观察者调用
-
代码示例:手动触发 KVO 的监听
因为 KVO 的底层原理是通过 RunTime 动态生成被观察者的子类,然后修改被观察者所属的类为动态生成的子类,并在动态生成的子类中重写被观察属性的 setter 方法,来达到当被观察的属性改变时可以通知所有观察者的目的
所以只有通过调用被观察属性的 setter 方法,或者通过 KVC 去修改被观察的属性时,才会触发观察者监听的方法。直接修改被观察属性对应的成员变量是不会触发观察者监听的方法的
当需要通过 KVO 监听被观察属性对应的成员变量的值的改变的时候,可以通过在为被观察属性对应的成员变量赋值的前后分别调用
-willChangeValueForKey:
和-didChangeValueForKey:
这对方法,来手动触发观察者监听的方法
点击屏幕,输出结果如下所示:// 处理 self.person->_weight 发生改变的情况 // keyPath = weight // object = <Person: 0x6000033ca1a0> // change = { // kind = 1; // new = 50; // } // context = 0x10f8f4b10
注意:
当被观察者调用
-addObserver:forKeyPath:options:context:
添加观察者时,如果观察选项(options
)为NSKeyValueObservingOptionPrior
(在属性改变之前和属性改变之后,各发一次通知),则属性改变之前发送的通知是在被观察者调用-willChangeValueForKey:
时进行的,属性改变之后发送的通知是在被观察者调用-didChangeValueForKey:
时进行的。被观察者可以通过只调用-willChangeValueForKey:
来单独触发属性改变之前的那次通知,用于在属性值即将更改前做一些操作。但是被观察者无法通过只调用-didChangeValueForKey:
来单独触发属性改变之后的那次通知。属性改变之后的那次通知,需要-willChangeValueForKey:
+-didChangeValueForKey:
配合使用,才能触发 -
代码示例:给定属性的新值和旧值相等时,不触发 KVO 的监听
有时候可能会遇到这样的需求:被观察者中给定属性的值修改前后相等时,不触发观察者监听的方法
例如:ViewController
对Person
对象的name
属性注册了 KVO 监听,我们希望在对name
属性赋值时做一个判断,如果新值和旧值相等,则不触发KVO。可以采用如下的思路来实现:- 在
ViewController
中通过 KVO 三步曲为Person
对象的name
属性添加 KVO 监听(此时Person
对象的name
属性触发 KVO 监听的方式为:自动触发监听) - 在
Person
类内部将name
属性触发 KVO 监听的方式由自动触发监听改为手动触发监听 - 在
Person
类内部重写name
属性的setter
方法,判断新旧值是否相等,如果新旧值不相等,则手动触发 KVO 监听
第一次点击屏幕,输出结果如下所示:// 处理 self.person.name 发生改变的情况 // keyPath = name // object = <Person: 0x600001cef3e0> // change = { // kind = 1; // new = hcg; // } // context = 0x103460aa8
第二次点击屏幕,没有任何输出
- 在
KVO 与集合类型
-
代码示例:通过 KVO 监听集合对象元素的改变
KVO 监听集合对象的元素的改变时,需要通过 KVC 的集合代理方法获取可变集合代理对象,并使用可变集合代理对象进行操作。当可变集合代理对象内部的元素发生改变时,会触发 KVO 的监听方法。集合对象包括
NSArray
、NSOrderedSet
、NSSet
注意:如果直接对集合对象进行操作,是不会触发 KVO 监听的方法的
// 1.直接对集合对象进行操作 // 由输出结果可知直接对集合对象本身进行操作,不会触发 KVO 监听的方法 [self.person.books addObject:@"Physics"]; NSLog(@"self.person.books = %@", self.person.books); // self.person.books = ( // Chinese, // English, // Math, // Technology, // Physics // )
// 2.通过 KVC 的集合代理方法获取可变的集合代理对象,并使用可变的集合代理对象进行操作 // 2.1 向可变数组 books 中添加元素 NSMutableArray* books = [self.person mutableArrayValueForKey:@"books"]; [books addObject:@"Physics"]; NSLog(@"self.person.books = %@", self.person.books); // 处理 self.person.books 发生改变的情况 // keyPath = books // object = <Person: 0x6000015751a0> // change = { // indexes = "<_NSCachedIndexSet: 0x60000151f400>[number of indexes: 1 (in 1 ranges), indexes: (4)]"; // kind = 2; // new = ( // Physics // ); // } // context = 0x10e965b10 // self.person.books = ( // Chinese, // English, // Math, // Technology, // Physics // )
// 2.通过 KVC 的集合代理方法获取可变的集合代理对象,并使用可变的集合代理对象进行操作 // 2.2 替换可变数组 books 中索引 0 处的元素 NSMutableArray* books = [self.person mutableArrayValueForKey:@"books"]; [books replaceObjectAtIndex:0 withObject:@"Physics"]; NSLog(@"self.person.books = %@", self.person.books); // 处理 self.person.books 发生改变的情况 // keyPath = books // object = <Person: 0x6000020d7940> // change = { // indexes = "<_NSCachedIndexSet: 0x6000020e3f20>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; // kind = 4; // new = ( // Physics // ); // old = ( // Chinese // ); // } // context = 0x10639fb10 // self.person.books = ( // Physics, // English, // Math, // Technology // )
// 2.通过 KVC 的集合代理方法获取可变的集合代理对象,并使用可变的集合代理对象进行操作 // 2.3 删除可变数组 books 中索引 0 处的元素 NSMutableArray* books = [self.person mutableArrayValueForKey:@"books"]; [books removeObjectAtIndex:0]; NSLog(@"self.person.books = %@", self.person.books); // 处理 self.person.books 发生改变的情况 // keyPath = books // object = <Person: 0x600003e8bf80> // change = { // indexes = "<_NSCachedIndexSet: 0x600003ecc040>[number of indexes: 1 (in 1 ranges), indexes: (0)]"; // kind = 3; // old = ( // Chinese // ); // } // context = 0x10426cb10 // self.person.books = ( // English, // Math, // Technology // )
关于
change
字典:/* NSKeyValueChangeKey const NSKeyValueChangeIndexesKey = @"indexes"; 用于存储被观察者集合属性被更改的索引 如果被观察者是集合类型的对象,且进行的是(插入、移除、替换)操作,则 change 字典里就会包含这个 key 这个 key 对应的 value 是一个 NSIndexSet 类型的对象,包含集合中被更改的元素的索引 NSKeyValueChangeKey const NSKeyValueChangeKindKey = @"kind"; 用于标识被观察者给定属性的改变类型,change 字典里默认会包含这个 key,其 value 为枚举类型 NSKeyValueChange typedef NS_ENUM(NSUInteger, NSKeyValueChange) { NSKeyValueChangeSetting = 1, // 对属性进行了设值操作(用于非集合类型和集合类型) NSKeyValueChangeInsertion = 2, // 对属性进行了插入操作(仅用于集合类型) NSKeyValueChangeRemoval = 3, // 对属性进行了移除操作(仅用于集合类型) NSKeyValueChangeReplacement = 4, // 对属性进行了替换操作(仅用于集合类型) }; NSKeyValueChangeKey const NSKeyValueChangeNewKey = @"new"; 用于存储被观察者给定属性的新值 如果观察选项 options 中传入了 NSKeyValueObservingOptionNew,则 change 字典里就会包含这个 key NSKeyValueChangeKey const NSKeyValueChangeOldKey = @"old"; 用于存储被观察者给定属性的旧值 如果观察选项 options 中传入了 NSKeyValueObservingOptionOld,则 change 字典里就会包含这个 key 如果是进行插入操作,则 change 字典中会包含 NSKeyValueChangeNewKey 字段,对应的值为插入的元素。前提条件是注册观察者时 options 中传入了 NSKeyValueObservingOptionNew 如果是进行删除操作,则 change 字典中会包含 NSKeyValueChangeOldKey 字段,对应的值为删除的元素,前提条件是注册观察者时 options 中传入了 NSKeyValueObservingOptionOld 如果是进行替换操作,则 change 字典中会包含 NSKeyValueChangeNewKey 和 NSKeyValueChangeOldKey 字段,对应的值为替换后的元素和替换前的元素,前提条件是注册观察者时 options 中传入了 NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld */
-
代码示例:通过 KVO 监听集合对象元素属性的改变
输出结果如下所示:// 处理 self.person.members.age 发生改变的情况 // self.family.members = ( // "name = jack00, age = 1", // "name = jack01, age = 10", // "name = jack02, age = 20" // ) // keyPath = age // object = name = jack00, age = 1 // change = { // kind = 1; // new = 1; // } // context = 0x104756d70
-
代码示例:手动触发 集合对象元素改变 的 KVO 通知
以手动触发数组对象元素改变的 KVO 通知为例,需要根据 KVC 的NSMutableArray
搜索模式:- 至少实现一个插入方法和至少实现一个删除方法,否则无法手动触发数组对象元素改变的 KVO 通知
插入方法:insertObject:in<Key>AtIndex:
或者insert<Key>:atIndexes:
删除方法:removeObjectFrom<Key>AtIndex:
或者remove<Key>AtIndexes:
- 如果不实现替换方法,则执行替换操作时,KVO 会把替换操作当成先删除后插入,即会触发两次 KVO 监听的方法
第一次触发的监听方法中change
字典里的NSKeyValueChangeOldKey
键的值为替换前的元素(前提是在注册观察者时,options
中传入了NSKeyValueObservingOptionOld
)
第二次触发的监听方法中change
字典里的NSKeyValueChangeNewKey
键的值为替换后的元素(前提是在注册观察者时,options
中传入了NSKeyValueObservingOptionNew
) - 如果实现替换方法,则执行替换操作时只会触发一次 KVO 监听的方法,并且
change
字典里会同时包含NSKeyValueChangeOldKey
键和NSKeyValueChangeNewKey
键(前提是在注册观察者时,options
中传入了NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
) - 建议实现替换方法以提高性能
replaceObjectIn<Key>AtIndex:withObject:
或者replace<Key>AtIndexes:with<Key>:
- 至少实现一个插入方法和至少实现一个删除方法,否则无法手动触发数组对象元素改变的 KVO 通知
KVO 与属性的依赖观察
-
被观察的属性依赖于非集合类型的属性
如果被观察的属性的改变依赖于其他的一个或者多个属性的改变(即当其他属性改变时,被观察的属性也需要跟着改变)
则需要使用 KVO 的以下方法建立其他属性与被观察属性的依赖关系// 用于控制被观察者中给定 key 所标识的属性依赖的其他属性 // @param.key 用于标识依赖属性的关键路径 // @return 用于标识被依赖属性的关键路径的集合 // @note 此方法的默认实现会在被观察者所属的类中搜索名称为 +keyPathsForValuesAffecting<Key> 的方法 // 如果找到,则返回 +keyPathsForValuesAffecting<Key> 的调用结果 // 如果找不到,为了向后的二进制兼容性,则返回根据之前已弃用的 +setKeys:triggerChangeNotificationsForDependentKey: 的调用所提供的信息计算出的关键路径的集合 +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;
比如,我们想要对
Download
类中的progress
属性进行 KVO 监听,而该属性的改变依赖于writtenData
属性和totalData
属性的改变
观察者监听了Download
对象的progress
属性,当writtenData
属性和totalData
属性的值发生改变时,观察者也应该被通知
// 注册观察者时,控制台输出结果如下所示: +[Download keyPathsForValuesAffectingValueForKey:], key = progress +[Download keyPathsForValuesAffectingProgress] +[Download keyPathsForValuesAffectingValueForKey:], key = writtenData +[Download keyPathsForValuesAffectingValueForKey:], key = writtenData +[Download keyPathsForValuesAffectingValueForKey:], key = totalData +[Download keyPathsForValuesAffectingValueForKey:], key = totalData // 点击屏幕时,控制台输出结果如下所示: 处理 self.download.progress 发生改变的情况 keyPath = progress object = <Download: 0x600003104f60> change = { kind = 1; new = "0.01"; old = 0; } context = 0x108b15158
-
被观察的属性依赖于集合类型的元素的属性
当被观察的属性的改变依赖于其他的集合类型 元素属性的改变时,上面的那种做法就不管用了。比如:
有一个Employee
类,它有一个salary
属性
有一个Department
类,它有一个NSArray<Employee *>* employees
属性
此时,我们希望为Department
类添加一个totalSalary
属性用来计算所有员工的薪水,也就是在这个关系中Department.totalSalary
依赖于Department.employees
中所有Employee
对象的salary
属性虽然我们可以通过 KVO 将
Department
对象作为所有Employee.salary
属性的观察者
但是我们必须在Employee
对象添加到Department.employees
时,把Department
对象注册为Employee.salary
的观察者
同样地,我们必须在Employee
对象从Department.employees
中移除时,为Employee.salary
注销Department
对象的观察者
然后,在观察者监听的方法中,我们可以针对被依赖项的改变来更新依赖项的值
-
通过属性的依赖观察,监听自定义对象多个属性的改变
当我们通过 KVO 监听自定义对象时,有可能也对自定义对象本身所具有的属性感兴趣。比如:
有一个Dog
类,它有name
属性和height
属性
有一个Person
类,它有一个dog
属性
此时将ViewController
注册为person.dog
的观察者,并希望当person.dog.name
、person.dog.age
的值发生改变时,ViewController
也能收到相应的通知
运行程序时,控制台输出结果如下所示:// +[Person keyPathsForValuesAffectingValueForKey:], key = dog // +[Person keyPathsForValuesAffectingDog] // +[Person keyPathsForValuesAffectingValueForKey:], key = _dog // +[Person keyPathsForValuesAffectingValueForKey:], key = _dog // +[Person keyPathsForValuesAffectingValueForKey:], key = _dog
执行
self.person.dog.name = [NSString stringWithFormat:@"husky%02d", i];
时,控制台输出结果如下所示:// 处理 self.person.dog 发生改变的情况 // keyPath = dog // object = <Person: 0x600000ab5c80> // change = { // kind = 1; // new = "<Dog: 0x600000ab54e0>, name = husky01, height = 30.00"; // old = "<Dog: 0x600000ab54e0>, name = husky01, height = 30.00"; // } // context = 0x104970ee0
执行
self.person.dog.height = i + 30.0f;
时,控制台输出结果如下所示:// 处理 self.person.dog 发生改变的情况 // keyPath = dog // object = <Person: 0x600000ab5c80> // change = { // kind = 1; // new = "<Dog: 0x600000ab54e0>, name = husky01, height = 31.00"; // old = "<Dog: 0x600000ab54e0>, name = husky01, height = 31.00"; // } // context = 0x104970ee0
KVO 的底层实现原理
-
KVO 的底层实现原理简述
苹果使用了 isa 混写技术(isa-swizzling)来实现 KVO
当程序运行过程中,被观察者调用-addObserver:forKeyPath:options:context:
注册观察者时,系统会利用 RunTime API 动态地创建被观察者所属类(HCGPerson
)的子类(NSKVONotifying_HCGPerson
)
然后修改被观察者的isa
指针,让其指向这个动态创建的子类(即修改被观察者所属的类为动态创建的子类)
并在动态生成的子类中重写被观察属性的setter
方法,来达到当被观察的属性改变时可以通知所有观察者的目的注意:
① 动态创建的子类(NSKVONotifying_HCGPerson
)的isa
指针,指向的是它自己的元类,而不是原始类(HCGPerson
)的元类
② 动态创建子类 以及 在动态创建的子类中重写被观察属性的setter
方法,都是在程序运行时进行的,而不是在程序编译时进行的动态创建的子类(
NSKVONotifying_HCGPerson
)被重写的setter
方法的SEL
对应的IMP
为Foundation.framework
中的_NSSetXXXValueAndNotify
函数(XXX 为被观察中给定属性的数据类型)。当被观察者中给定属性的值发生改变时,会调用_NSSetXXXValueAndNotify
函数,_NSSetXXXValueAndNotify
函数的执行过程为:-willChangeValueForKey: // 当注册观察者时,如果 options 选项包含 NSKeyValueObservingOptionPrior,则此方法内部会触发观察者监听的方法(属性值改变之前) -observeValueForKeyPath:ofObject:change:context: [super setter] // 调用父类的 setter 方法 -didChangeValueForKey: // 此方法内部会触发观察者监听的方法(属性值改变之后) -observeValueForKeyPath:ofObject:change:context:
当被观察者注销观察者后,被观察者的
isa
指针会被重新指回原始类(HCGPerson
)
但是利用 RunTime API 动态创建的子类(NSKVONotifying_HCGPerson
)并没有销毁,还会继续保存在内存中KVO 动态生成的子类中包含以下 4 个方法:
- 被观察属性的
setter
方法。用于调用父类的setter
方法,并触发观察者监听的方法 class
方法。动态生成的子类中class
方法返回的是父类的Class
对象,这样做的目的是为了隐藏 KVO 动态生成的子类,不让外界知道 KVO 动态生成的子类的存在_isKVOA
方法。用于标识这是一个KVO 动态生成的子类dealloc
方法。用于释放 KVO 使用过程中申请的资源
- 被观察属性的
-
代码示例:NSKVONotifying_HCGPerson 的伪代码
以监听
HCGPerson
类的name
属性为例
KVO 动态生成的子类的伪代码如下所示:
-
代码示例:验证 KVO 底层实现原理
以监听
Person
类的name
属性为例
创建 2 个Person
类的实例对象person0
和person1
在为person0
添加观察者的前后,分别打印person0
和person1
所属的类、isa
指针的指向、方法列表
输出结果如下所示:
// 为 self.person0 添加观察者之前: // self.person0.class = Person, self.person1.class = Person // self.person0->isa = Person, self.person1->isa = Person // self.person0 的方法列表 = // methodName = name, methodTypes = @16@0:8 // methodName = setName:, methodTypes = v24@0:8@16 // methodName = age, methodTypes = i16@0:8 // methodName = setAge:, methodTypes = v20@0:8i16 // methodName = .cxx_destruct, methodTypes = v16@0:8 // self.person1 的方法列表 = // methodName = name, methodTypes = @16@0:8 // methodName = setName:, methodTypes = v24@0:8@16 // methodName = age, methodTypes = i16@0:8 // methodName = setAge:, methodTypes = v20@0:8i16 // methodName = .cxx_destruct, methodTypes = v16@0:8 // self.person0.setName = 0x100b55c30, self.person1.setName = 0x100b55c30 // // 为 self.person0 添加观察者之后 // self.person0.class = Person, self.person1.class = Person // self.person0->isa = NSKVONotifying_Person, self.person1->isa = Person // self.person0 的方法列表 = // methodName = setName:, methodTypes = v24@0:8@16 // methodName = class, methodTypes = #16@0:8 // methodName = dealloc, methodTypes = v16@0:8 // methodName = _isKVOA, methodTypes = B16@0:8 // self.person1 的方法列表 = // methodName = name, methodTypes = @16@0:8 // methodName = setName:, methodTypes = v24@0:8@16 // methodName = age, methodTypes = i16@0:8 // methodName = setAge:, methodTypes = v20@0:8i16 // methodName = .cxx_destruct, methodTypes = v16@0:8 // self.person0.setName = 0x7fff207bbb57, self.person1.setName = 0x100b55c30 // // (lldb) po (IMP)0x7fff207bbb57 // (Foundation`_NSSetObjectValueAndNotify) // // (lldb) po (IMP)0x100b55c30 // (KeyValueObservingDemo`-[Person setName:] at Person.h:8)
KVO 的其他细节
-
被观察者的 observationInfo 属性
observationInfo
属性是在NSKeyValueObserving.h
文件中通过分类给NSObject
添加的属性,所有继承自NSObject
的对象都含有该属性。observationInfo
属性包含了被观察者所有的观察信息,包括观察者(observer
)、被观察的属性(keyPath
)、观察的选项(options
)、注册观察者时传递的上下文(context
)等代码示例如下所示:
-
通过 KVC 为成员变量赋值从而触发 KVO
输出结果如下所示:keyPath = height object = <Person: 0x60000335aa20> change = { kind = 1; new = 180; } context = 0x102ad48f0
-
使用 KVO 时的注意点
-
如果一个对象被注册为观察者,则该对象必须能响应
-observeValueForKeyPath:ofObject:change:context:
方法,即该对象所属类中必须实现回调方法-observeValueForKeyPath:ofObject:change:context:
以接收被观察者给定属性值改变的通知。当被观察者给定属性的值发生改变时就会调用此回调方法,没有实现会导致程序 Crash -
在观察者所属的类中监听的方法
-observeValueForKeyPath:ofObject:change:context:
里,应该为无法识别的keyPath
、object
、context
调用父类的实现[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
-
因为被观察者在调用
-addObserver:forKeyPath:options:context:
注册观察者之后,并不会对观察者进行强引用
所以需要格外注意观察者的生命周期,被观察者需要在观察者被销毁之前调用-removeObserver:forKeyPath:context:
以移除观察者
否则当被观察者给定的属性再次改变时,KVO 向已释放的观察者再次发送属性值改变的通知,可能会导致程序 Crash -
注册观察者和移除观察者的方法应该是成对调用的。如果重复调用移除观察者的方法,则会抛出
NSRangeException
异常并导致程序 Crash。苹果官方的推荐做法是:
在观察者初始化期间(init
、viewDidLoad
),被观察者调用注册方法将给定的实例对象注册为观察者
在观察者释放过程中(dealloc
),被观察者调用移除方法将给定的实例对象从观察者中移除
这样可以保证注册观察者和移除观察者的方法是成对出现的,是一种比较理想的使用方式 -
在调用注册观察者和移除观察者的方法时,参数
keyPath
传入的是一个字符串,为了避免写错,可以将被观察属性getter
方法的 SEL 转换成字符串,在编译阶段对keyPath
进行检验
NSStringFromSelector(@selector(propertyName))
-
在调用注册观察者的方法时,如果参数
context
传入的是一个 Objective-C 对象,则必须在移除观察者之前持有它的强引用,否则在观察者的监听方法中访问context
就可能导致程序 Crash
可以使用桥接(__bridge_retained
)剥夺传入的 Objective-C 对象的内存管理权,但是必须记得在不需要该 Objective-C 对象时释放它,否则会导致内存泄露 -
KVO 监听集合对象的元素的改变时,需要通过 KVC 的集合代理方法获取可变集合代理对象,并使用可变集合代理对象进行操作。当可变集合代理对象内部的元素发生改变时,会触发 KVO 的监听方法。集合对象包括
NSArray
、NSOrderedSet
、NSSet
。如果直接对集合对象进行操作,是不会触发 KVO 监听的方法的
-
-
防止多次注册或者多次移除相同的观察者
当业务逻辑比较复杂的时,难以避免会多次注册或者多次移除相同的观察者,甚至移除了一个未注册的观察者,从而产生可能导致应用程序 Crash 的风险,这篇文章(黑科技防止多次添加删除KVO出现的问题)中给出了三种解决方案:
- 利用
@try
@catch
- 利用 模型数组 进行存储记录
- 利用
observationInfo
里私有属性
- 利用
-
系统 KVO 的缺点
① 使用步骤比较麻烦,KVO 使用三步曲缺一不可:
- 被观察者添加(注册)观察者
- 观察者实现监听方法以接收被观察者给定属性改变的通知
- 被观察者移除(注销)观察者
② 需要手动移除观察者,并且移除观察者的时机必须合适,还不能重复移除观察者
③ 注册观察者的代码和观察者处理监听的回调代码上下文不同,传递上下文(
context
)是通过void *
指针④ 观察者在处理监听的回调时,需要实现
-observeValueForKeyPath:ofObject:change:context:
,比较麻烦⑤ 处理复杂的业务逻辑时,在观察者处理监听的回调
-observeValueForKeyPath:ofObject:change:context:
中准确地判断被观察者与被观察的属性相对比较麻烦。有多个被观察者和多个被观察的属性时,需要在观察者处理监听的回调-observeValueForKeyPath:ofObject:change:context:
中写大量的if-else
嵌套进行判断
自定义 KVO
-
简述
根据苹果官方文档对 KVO 原理的描述以及作者自己对 KVO 原理的探索,通过给
NSObject
添加分类HCGKVO
,实现自定义 KVO 添加监听和移除监听的方法因为,实现自定义 KVO 的主要目的是为了加深对系统 KVO 实现原理的理解
所以,下面的代码只是抓住了系统 KVO 实现原理的主要流程,并没有对系统 KVO 实现的各方面进行详细的考虑下面代码至少还存在以下问题:
- 没有进行足够的防御性编程(断言?临界条件?)
- 没有考虑多线程环境下的并发操作
- 没有进行性能优化
- 在注册观察者的方法中,只实现了对
key
所标识的属性的监听,没有实现对keyPath
所标识的属性的监听 - 在注册观察者的方法中,只实现了对基本数据类型和 Objective-C 对象类型的属性的监听,没有实现对结构体类型的属性的监听
- 各种数据类型的属性的
setter
方法的实现代码重复度高,未进行提炼与精简
-
源码
HCGObservationInfo.h
和HCGObservationInfo.m
代码如下:#import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN // 当前对象被观察的属性的值发生改变时触发的回调 // @param.observer 观察者 // @param.key 当前对象被观察的属性的关键路径 // @param.oldValue 当前对象被观察的属性发生改变之前的值 // @param.newValue 当前对象被观察的属性发生改变之后的值 // @return 是否移除当前观察者。YES - 移除当前观察者,NO - 保留当前观察者 typedef bool (^HCGKVOCallback)(id observer, NSString* key, id oldValue, id newValue); @interface HCGObservationInfo : NSObject @property (nonatomic, weak) id observer; // 观察者 @property (nonatomic, copy) NSString* key; // 当前对象被观察的属性的关键路径 @property (nonatomic, copy) HCGKVOCallback callback; // 当前对象被观察的属性的值发生改变时触发的回调 -(instancetype)initWithObserver:(id)anObserver key:(NSString *)aKey callback:(HCGKVOCallback)aCallback; +(instancetype)observationInfoWithObserver:(id)anObserver key:(NSString *)aKey callback:(HCGKVOCallback)aCallback; @end NS_ASSUME_NONNULL_END
#import "HCGObservationInfo.h" @implementation HCGObservationInfo -(instancetype)initWithObserver:(id)anObserver key:(NSString *)aKey callback:(HCGKVOCallback)aCallback { if (self = [super init]) { self.observer = anObserver; self.key = aKey; self.callback = aCallback; } return self; } +(instancetype)observationInfoWithObserver:(id)anObserver key:(NSString *)aKey callback:(HCGKVOCallback)aCallback { return [[self alloc] initWithObserver:anObserver key:aKey callback:aCallback]; } @end
NSObject+HCGKVO.h
和NSObject+HCGKVO.m
代码如下:#import <Foundation/Foundation.h> #import "HCGObservationInfo.h" NS_ASSUME_NONNULL_BEGIN @interface NSObject (HCGKVO) // 注册观察者 // @param.anObserver 观察者 // @param.aKey 被观察者给定属性的关键路径 // @param.aCallback 被观察者给定属性的值发生改变时触发的回调 -(void)hcg_addObserver:(id)anObserver forKey:(NSString *)aKey usingCallback:(HCGKVOCallback)aCallback; // 注销观察者 // @param.anObserver 观察者 // @param.aKey 被观察者给定属性的关键路径 -(void)hcg_removeObserver:(id)anObserver forKey:(NSString *)aKey; @end NS_ASSUME_NONNULL_END
#import "NSObject+HCGKVO.h" #import <objc/runtime.h> #import <objc/message.h> static NSString * const kHCGKVOClassPrefix = @"HCGKVONotifying_"; // KVO Class 的前缀 static void const * const kHCGKVOObservationInfoArrayKey = &kHCGKVOObservationInfoArrayKey; // 用于标识当前对象观察信息数组的键 @implementation NSObject (HCGKVO) // 注册观察者 -(void)hcg_addObserver:(id)anObserver forKey:(NSString *)aKey usingCallback:(HCGKVOCallback)aCallback { // 1.检查当前对象所属的类有没有参数 aKey 所标识的属性的 setter 方法 SEL setterSelector = NSSelectorFromString(setterFromGetter(aKey)); Method setterMethod = class_getInstanceMethod([self class], setterSelector); if (!setterMethod) { NSLog(@"在当前对象 %@ 中找不到参数 aKey = %@ 所标识的 setter 方法 %@", self, aKey, setterFromGetter(aKey)); return; } // 2.检查当前对象的 isa 指针所指向的类是不是一个 KVO Class // 如果不是,则新建一个继承自原始类的 KVO Class 并将当前对象的 isa 指针指向这个新创建的 KVO CLass Class kvoClass = nil; Class currentClass = object_getClass(self); NSString* currentClassName = NSStringFromClass(currentClass); if (![currentClassName hasPrefix:kHCGKVOClassPrefix]) { kvoClass = [self hcg_getKVOClassWithOriginalClassName:currentClassName]; object_setClass(self, kvoClass); } else { kvoClass = currentClass; } // 3.为 KVO Class 添加参数 aKey 所标识的属性的 setter 方法 const char * setterTypes = method_getTypeEncoding(setterMethod); IMP setterImplementation = getSetterImplementation(setterTypes); class_addMethod(kvoClass, setterSelector, setterImplementation, setterTypes); // 4.将观察信息添加到当前对象的观察信息数组中,以备属性改变时触发监听 HCGObservationInfo* info = [HCGObservationInfo observationInfoWithObserver:anObserver key:aKey callback:aCallback]; NSMutableArray* infoes = objc_getAssociatedObject(self, kHCGKVOObservationInfoArrayKey); if (!infoes) { infoes = [NSMutableArray array]; objc_setAssociatedObject(self, kHCGKVOObservationInfoArrayKey, infoes, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } [infoes addObject:info]; } // 注销观察者 -(void)hcg_removeObserver:(id)anObserver forKey:(NSString *)aKey { NSMutableArray* infoes = objc_getAssociatedObject(self, kHCGKVOObservationInfoArrayKey); if (!infoes) { return; } for (HCGObservationInfo* info in infoes) { if ([info.key isEqualToString:aKey] && info.observer == anObserver) { [infoes removeObject:info]; break; } } } #pragma mark - 动态创建 KVO 类 // 根据原始类名创建 KVO Class -(Class)hcg_getKVOClassWithOriginalClassName:(NSString *)originalClassName { NSString* kvoClassName = [kHCGKVOClassPrefix stringByAppendingString:originalClassName]; Class kvoClass = NSClassFromString(kvoClassName); // 如果 KVO Class 已经存在,则直接返回 KVO Class if (kvoClass) { return kvoClass; } // 如果 KVO Class 不存在,则创建 KVO Class Class originalClass = object_getClass(self); kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0); objc_registerClassPair(kvoClass); // 为 KVO Class 添加 class 方法的实现:隐藏 KVO Class 的存在 class_addMethod(kvoClass, NSSelectorFromString(@"class"), (IMP)hcg_class, "#@:"); // 为 KVO Class 添加 _isKVOA 方法的实现:标识该类是一个 KVO Class class_addMethod(kvoClass, NSSelectorFromString(@"_isKVOA"), (IMP)hcg_isKVOA, "B@:"); // 返回创建的 KVO Class return kvoClass; } // class 方法的实现 static Class hcg_class(id self, SEL _cmd) { Class currentCls = object_getClass(self); Class superCls = class_getSuperclass(currentCls); return superCls; } // _isKVOA 方法的实现 static bool hcg_isKVOA(id self, SEL _cmd) { return YES; } #pragma mark - 相互转换:setter 方法名与 getter 方法名 /* 这里需要注意:首字母转换成大写这一步,不能直接调用 -[NSString capitalizedString],因为该方法返回的是除了首字母大写之外,其他字母全部小写的字符串 -(void)capitalizedStringDemo { NSString* originalStr = @"myNameIsHuangChaoGen"; NSString* capitalizedStr = originalStr.capitalizedString; NSLog(@"originalStr = %@", originalStr); // originalStr = myNameIsHuangChaoGen NSLog(@"capitalizedStr = %@", capitalizedStr); // capitalizedStr = Mynameishuangchaogen } */ // 根据 getter 方法名返回 setter 方法名 static NSString* setterFromGetter(NSString* getterName) { // 断言 if (!getterName || getterName.length <= 0) { NSLog(@"非法的 getter 方法名 %@", getterName); return nil; } // name -> Name -> setName: NSString* capitalFirstString = [[getterName substringWithRange:NSMakeRange(0, 1)] uppercaseString]; NSString* capitalString = [getterName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:capitalFirstString]; NSString* setterName = [NSString stringWithFormat:@"set%@:", capitalString]; return setterName; } // 根据 setter 方法名返回 getter 方法名 static NSString* getterFromSetter(NSString* setterName) { // 断言 if (!setterName || setterName.length <= 4 || ![setterName hasPrefix:@"set"] || ![setterName hasSuffix:@":"]) { NSLog(@"非法的 setter 方法名 %@", setterName); return nil; } // setName: -> Name -> name NSString* capitalString = [setterName substringWithRange:NSMakeRange(3, setterName.length - 4)]; NSString* smallFirstString = [[capitalString substringWithRange:NSMakeRange(0, 1)] lowercaseString]; NSString* getterName = [capitalString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:smallFirstString]; return getterName; } #pragma mark - 各种数据类型 setter 方法的实现 // 根据 setter 方法类型获取 setter 方法实现 static IMP getSetterImplementation(const char * setterTypes) { IMP setterImplementation = NULL; switch (setterTypes[7]) { // char 类型的属性的 setter 方法实现 case 'c': break; // int 类型的属性的 setter 方法实现 case 'i': setterImplementation = (IMP)hcg_setterForInt; break; // short 类型的属性的 setter 方法实现 case 's': break; // long 类型的属性的 setter 方法实现 case 'l': break; // long long 类型的属性的 setter 方法实现 case 'q': break; // unsigned char 类型的属性的 setter 方法实现 case 'C': break; // unsigned int 类型的属性的 setter 方法实现 case 'I': break; // unsigned short 类型的属性的 setter 方法实现 case 'S': break; // unsigned long 类型的属性的 setter 方法实现 case 'L': break; // unsigned long long 类型的属性的 setter 方法实现 case 'Q': break; // float 类型的属性的 setter 方法实现 case 'f': break; // double 类型的属性的 setter 方法实现 case 'd': break; // bool 类型的属性的 setter 方法实现 case 'B': break; // void 类型的属性的 setter 方法实现 case 'v': break; // char * 类型的属性的 setter 方法实现 case '*': break; // Objective-C 对象类型的属性的 setter 方法实现 case '@': setterImplementation = (IMP)hcg_setterForObject; break; default: NSLog(@"未识别的 setter 方法类型:%s", setterTypes); break; } return setterImplementation; } // Objective-C 对象类型的属性的 setter 方法实现 static void hcg_setterForObject(id self, SEL _cmd, id newValue) { NSString* setterName = NSStringFromSelector(_cmd); NSString* getterName = getterFromSetter(setterName); // 获取旧值 id oldValue = [self valueForKey:getterName]; // 调用原始类的 setter 方法 struct objc_super superCls = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; ((void(*)(void*, SEL, id))objc_msgSendSuper)(&superCls, _cmd, newValue); // 从当前对象中获取观察信息数组,遍历观察信息数组,调用相应观察者的回调 NSMutableArray* infoes = objc_getAssociatedObject(self, kHCGKVOObservationInfoArrayKey); for (HCGObservationInfo* info in infoes) { if ([info.key isEqualToString:getterName]) { // GCD 异步调用 callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ bool isRemoved = info.callback(info.observer, info.key, oldValue, newValue); if (isRemoved) { [self hcg_removeObserver:info.observer forKey:info.key]; } }); } } } // int 类型的属性的 setter 方法实现 static void hcg_setterForInt(id self, SEL _cmd, int newValue) { NSString* setterName = NSStringFromSelector(_cmd); NSString* getterName = getterFromSetter(setterName); // 获取旧值 id oldValue = [self valueForKey:getterName]; // 调用原始类的 setter 方法 struct objc_super superCls = { .receiver = self, .super_class = class_getSuperclass(object_getClass(self)) }; ((void(*)(void*, SEL, int))objc_msgSendSuper)(&superCls, _cmd, newValue); // 从当前对象中获取观察信息数组,遍历观察信息数组,调用相应观察者的回调 NSMutableArray* infoes = objc_getAssociatedObject(self, kHCGKVOObservationInfoArrayKey); for (HCGObservationInfo* info in infoes) { if ([info.key isEqualToString:getterName]) { // GCD 异步调用 callback dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ bool isRemoved = info.callback(info.observer, info.key, oldValue, @(newValue)); if (isRemoved) { [self hcg_removeObserver:info.observer forKey:info.key]; } }); } } } @end
-
使用示例
第一次点击屏幕,控制台输出结果如下所示:// observer = <ViewController: 0x7fb02c507b60>, key = name, oldValue = hcg, newValue = jack00 // observer = <ViewController: 0x7fb02c507b60>, key = age, oldValue = 20, newValue = 0
第二次点击屏幕,控制台输出结果如下所示:
// observer = <ViewController: 0x7fb02c507b60>, key = age, oldValue = 0, newValue = 1