IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发工具 -> 关于KVO的一些总结------持续更新中 -> 正文阅读

[开发工具]关于KVO的一些总结------持续更新中

什么是KVO

KVO的全称是Key-Value Observing ,俗称“键值观察/监听”,是苹果提供的一套事件通知机制,允许一个对象观察/监听另一个对象指定属性值的改变。当被观察对象属性值发生改变时,会触发KVO的监听方法来通知观察者。KVO是在MVC应用程序中的各层之间进行通信的一种特别有用的技术。

KVO和NSNotification 都是 观察者模式的一种实现,

KVO和可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey:等可变代理方法获取得到集合代理对象,并使用对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray和NSSet。

KVO的基本使用

KVO使用三部曲:添加/注册 KVO 监听、实现监听方法以接收属性改变通知、移除 KVO监听。

1.调用方法 addObserver:forKeyPath:options:context:给被观察对象添加观察者;

2.在观察者类中实现 observerValueForKeypath:ofObject:change:context:方法以接收属性改变的通知消息;

3.当观察者不需要再监听时,调用removeObserver:forKeyPath:方法将观察者移除。需要注意的是,至少需要再观察者销毁之前,调用此方法,否则可能会导致Crash。

注册方法

/*
 ** target:  被观察对象
 ** observer:观察者对象
 ** keyPath: 被观察对象的属性的关键路径,不能为nil
 ** options: 观察的配置选项,包括观察的内容(枚举类型):
           NSKeyValueObservingOptionNew:观察新值
           NSKeyValueObservingOptionOld:观察旧值
           NSKeyValueObservingOptionInitial:观察初始值,如果想在注册观察者后,立即接收一次回调,可以加入该枚举值
           NSKeyValueObservingOptionPrior:分别在值改变前后触发方法(即一次修改有两次触发)
 ** context: 可以传入任意数据(任意类型的对象或者C指针),在监听方法中可以接收到这个数据,是KVO中的一种传值方式
             如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就可能导致Crash
 */
 - (void)addObserver:(NSObject *)observer forKeypath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

监听方法

如果对象被注册成为观察者,则该对象必须能响应以下监听方法,即该对象所属类中必须实现监听方法。当被观察者对象属性发生改变时就会调用监听方法。如果没有实现就会导致Crash。

- (void)observeValueForKeyPath:(NSString *)keypath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
/*
 ** keyPath:被观察对象的属性的关键路径
 ** object: 被观察对象
 ** change: 字典 NSDictionary<NSKeyValueChangeKey, id>,属性值更改的详细信息,根据注册方法中options参数传入的枚举来返回
             key为 NSKeyValueChangeKey 枚举类型
             {
                 1.NSKeyValueChangeKindKey:存储本次改变的信息(change字典中默认包含这个key)
                 {
                     对应枚举类型 NSKeyValueChange
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting     = 1,
                         NSKeyValueChangeInsertion   = 2,
                         NSKeyValueChangeRemoval     = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
                     如果是对被观察对象属性(包括集合)进行赋值操作,kind 字段的值为 NSKeyValueChangeSetting
                     如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则会根据集合对象的操作方式来设置 kind 字段的值
                         插入:NSKeyValueChangeInsertion
                         删除:NSKeyValueChangeRemoval
                         替换:NSKeyValueChangeReplacement
                 }    
                 2.NSKeyValueChangeNewKey:存储新值(如果options中传入NSKeyValueObservingOptionNew,change字典中就会包含这个key)
                 3.NSKeyValueChangeOldKey:存储旧值(如果options中传入NSKeyValueObservingOptionOld,change字典中就会包含这个key)
                 4.NSKeyValueChangeIndexesKey:如果被观察的是集合对象,且进行的是(插入、删除、替换)操作,则change字典中就会包含这个key
                     这个key的value是一个NSIndexSet对象,包含更改关系中的索引
                 5.NSKeyValueChangeNotificationIsPriorKey:如果options中传入NSKeyValueObservingOptionPrior,则在改变前通知的change字典中会包含这个key。
                     这个key对应的value是NSNumber包装的YES,我们可以这样来判断是不是在改变前的通知[change[NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES]
             }
 ** context:注册方法中传入的context
 */
}

移除方法

在调用注册方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期。至少需要在观察者销毁之前,调用以下方法移除观察者,否则如果在观察者被释放后,再次触发KVO监听方法就会导致Crash。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;

使用例子

以下使用KVO为person对象添加观察者为当前viewController,监听 person 对象的 name 属性值的改变。当name值改变时,触发KVO的监听方法。

- (void)viewDidLoad {
	[super viewDidLoad];
	
	self.person = [HTPerson new];
	[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
	self.person.name = @"张三";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
	NSLog(@"keyPath:%@",keyPath);
    NSLog(@"object:%@",object);
    NSLog(@"change:%@",change);
    NSLog(@"context:%@",context);
} 

- (void)dealloc {
  [self.person removeObserver:self forKeyPath:@"name"];
}

实际应用

KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。斯坦福大学的ios教程中有一个很经典的案例,通过KVO在Model和Controller之间进行通信。如图:

在这里插入图片描述

KVO触发监听方法的方式

KVO触发分为自动触发和手动触发两种方式。

自动触发

如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发KVO:

使用点语法

使用setter方法

使用KVC的 setValue:forKey:方法

使用KVC的setValue:forKeyPath:方法

如果是监听集合对象的改变,需要通过KVC的mutableArrayValueForKey:等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO,集合对象包含NSAray和NSSet。

手动触发

普通对象属性火上成员变量使用:

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

NSArray 对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;

NSSet 对象使用:

- (void)willChange:(NSKeyValueChange)changeKind valueAtIndexes forKey:(NSString *)key;
- (void)didChange:(NSKetValueChange)changeKind valuesAtIndexes forKey:(NSString *)key;

KVO的进阶使用

observationInfo 属性

observationInfo 属性是NSKeyValueObservering.h 文件中系统通过分类给NSObject 添加的属性,所以所有继承于NSObject 的对象都含有该属性;

可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括observer\keyPath\options\context 等。

@property (nullable) void *observationInfo NS_RETURNS_INNER_POINTER;

context的使用

注册方法addObserver:forKeyPath:options:context:中的context可以传入任意数据,并且可以在监听方法中接收到这个数据。

context作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、多监听;也可以用来传值。

KVO 只有一个监听回调方法observeValueForKeyPath:ofObject:change:context:,我们通常情况下可以在注册方法中指定contextNULL,并在监听方法中通过objectkeyPath来判断触发KVO的来源。但是如果存在继承的情况,比如现在有Person类和它的连个子类Teacher类和Student类,person、teacher和student实例对象都对account对象的balance属性进行观察。问题:

? 当balance发生改变时,应该由谁来处理呢?

? 如果都由person来处理,那么在Person类的监听方法中又该怎么判断是自己的事物还是子类对象的事物呢?

这个时候通过使用context就可以很好地解决这个问题,在注册方法中为context设置一个独一无二的值,然后在监听方法中对context值进行检验即可。

苹果的推荐用法:用context来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为context的值。可以为整个类设置一个context,然后在监听方法中通过object和keypath来确定被观察属性,这样存在继承的情况就可以通过context来判断;也可以为每个被观察对象属性设置不同的context,这样使用context就可以精确的确定被观察对象属性。

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account *)account {
	[account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNEW | NSKeyValueObservingOptionOLd) context:PersonAccountInterestRateContext];
  
  [account addObserver:self forKeyPath:@"interestRate" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:PersonAccountInterestRateContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
		if (context == PersonAccountBalanceContext) {
				// Do something
		}
		else if(context == PersonAccountInterestRateContext) {
				
		}
		else {
					// Any unrecognized context must belong to super
					[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
		}
}

context 优点:嵌套少、性能高、更安全、扩展性强。

Context 注意点:

? 如果传的是一个对象,必须在移除观察之前持有它的强引用,否则在监听方法中访问context就了能导致crash;

? 空传NULL而不应该传nil。

KVO监听集合对象

? KVO可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过KVC的mutableArrayValueForKey: 等方法获取得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发KVO的监听方法。集合对象包含NSArray 和 NSSet。

(注意:如果直接对集合对象进行操作改变,不会触发KVO)

示例及代码输出如下:

观察者viewController 对被观察对象person的myArray 属性进行监听。

- (void)viewDidLoad {
		[super viewDidLoad];
		
		self.person = [HRPerson new];
		self.person.myArray = [NSMutableArray arrayWithCapacity:5];
		[self.person addObserver:self forKeyPath:@"mArray" options:(NSKeyValueObservingOptionNew | NSKeyValueObservevingOptionOld) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
		// [self.person.mArray addObject:@"2"]; // 如果直接对数组进行操作,不会触发KVO
		NSMutableArray *array = [self.person mutableArrayValueForKey:@"mArray"];
		[array addObject:@"1"];
		[array replaceObjectAtIndex:0 withObject:@"2"];
		[array removeObjectAtIndex:0];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
		/*  change 字典的值为:
        {
            indexes:对应的值为数组操作的详细信息,包括索引等
            kind:   对应的值为数组操作的方式:
                     2:代表插入操作
                     3:代表删除操作
                     4:代表替换操作
                     typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
                         NSKeyValueChangeSetting = 1,
                         NSKeyValueChangeInsertion = 2,
                         NSKeyValueChangeRemoval = 3,
                         NSKeyValueChangeReplacement = 4,
                     };
            new/old:如果是插入操作,则字典中只会有new字段,对应的值为插入的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew)
                     如果是删除操作,则字典中只会有old字段,对应的值为删除的元素,前提条件是options中传入了(NSKeyValueObservingOptionOld)
                     如果是替换操作,则字典中new和old字段都可以存在,对应的值为替换后的元素和替换前的元素,前提条件是options中传入了(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld)

            indexes = "<_NSCachedIndexSet: 0x600001d092e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
            kind = 2; 
            new =     (
                1
            );
        }
     */  
    NSLog(@"%@",change);  
}

- (void)dealloc {
  [self.person removeObserver:self forKeyPath:@"mArray"];
}
/*
 { indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 2; new =  (1); }
 { indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 4; new = (2); old = (1); }
 { indexes = "<_NSCachedIndexSet: 0x6000030e5380>number of indexes: 1 (in 1 ranges), indexes: (0)"; kind = 3; old = (2); }
*/

KVO的自动触发控制

可以在被观察对象的类中重写

+ (BOOL)automaticalyNotifiesObserversForKey:(NSString *)key

方法类控制KVO的自动触发。

如果我们只允许外界观察oerson的name属性,可以在Person类如下操作。这样外界接只能观察name属性,即使外界注册了对person对象其他属性的监听,那么在属性发生改变时也不会触发KVO

// 返回值代表允不允许触发KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
		BOOL automatic = NO;
		if ([key isEqualToString:@"name"]) {
			automatic = YES;
		}
		else {
			automic = [super automaticallyNotfiesObserversForKey:key];
		}
		
		return automatic;
}

也可以实现遵循命名规则为+(BOOL)autonaticallyNotifiesObserversOf 的方法来单一控制属性的KVO自动触发,为属性名(首字母大写)。

+ (BOOL)automaticallyNotifiesObserversOfName {
		return NO;
}

注意:

1.第一个方法的优先级高于第二个方法。如果实现了automaticallyNotifiesObserversForKey:方法,并对做了处理,则系统就不会再用该的automaticallyNotifiesObserversof方法。

2.options指定的NSKeyValueObservingOptionInitial 触发的KVO通知,是无法被automaticallyNotifiesObserversForKey:阻止的。

----------持续更新中

  开发工具 最新文章
Postman接口测试之Mock快速入门
ASCII码空格替换查表_最全ASCII码对照表0-2
如何使用 ssh 建立 socks 代理
Typora配合PicGo阿里云图床配置
SoapUI、Jmeter、Postman三种接口测试工具的
github用相对路径显示图片_GitHub 中 readm
Windows编译g2o及其g2o viewer
解决jupyter notebook无法连接/ jupyter连接
Git恢复到之前版本
VScode常用快捷键
上一篇文章      下一篇文章      查看所有文章
加:2021-12-11 15:55:55  更:2021-12-11 15:57:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 12:53:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计