Objective-C 的 KVO(一):基本使用 && 底层原理

本文深入讲解了Objective-C中的Key-Value Observing (KVO)原理、使用方法、集合操作、属性依赖及底层实现,还包括自定义KVO的实现示例。通过实例演示,助您轻松理解和应用KVO技术。

KVO 简介

  • 相关文档

    Key-Value Observing Programming Guide

    Objective-C 的 KVO(二):NSKeyValueObserving.h 代码注释

    Objective-C 的 KVC(一):基本使用 && 底层原理

    Objective-C 的 KVC(二):NSKeyValueCoding.h 代码注释

  • KVO 的概念

    KVO(Key-Value Observing),翻译成中文叫:键值观察,是苹果提供的一套事件通知机制,允许观察者监听被观察者给定属性的值的改变。当被观察者给定属性的值发生改变时,会触发观察者的监听方法来通知观察者。KVO 是在 MVC 架构中各层之间进行通信的一种特别有用的技术

    KVO 可以监听被观察者中单个属性的改变,也可以监听被观察中集合元素的改变。KVO 监听集合对象的元素的改变时,需要通过 KVC 的集合代理方法获取可变集合代理对象,并使用可变集合代理对象进行操作。当可变集合代理对象内部的元素发生改变时,会触发 KVO 的监听方法。集合对象包括 NSArrayNSOrderedSetNSSet

    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 使用三步曲
  1. 被观察者添加(注册)观察者,即
    被观察者调用 -addObserver:forKeyPath:options:context: 方法为自己添加观察者

  2. 观察者实现监听方法以接收被观察者给定属性改变的通知,即
    在观察者所属的类中实现 -observeValueForKeyPath:ofObject:change:context: 方法以接收被观察者给定属性改变的通知
    如果一个对象被注册为观察者,则该对象必须能响应此回调方法,即该对象所属类中必须实现此回调方法。当被观察者给定属性发生改变时就会调用此回调方法,没有实现会导致程序 Crash

  3. 被观察者移除(注销)观察者,即
    被观察者调用 -removeObserver:forKeyPath:context: 方法移除观察者。因为被观察者在调用 KVO 注册方法添加观察者之后,并不会对观察者进行强引用,所以需要注意观察者的生命周期,被观察者需要在观察者被销毁之前调用此方法以移除观察者,否则当被观察者给定属性的值再次改变时,KVO 向已释放的观察者再次发送属性值改变的通知,可能会导致程序 Crash

    代码示例:
    Person 类

    // 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:
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值