前言
本章内容主要是围绕KVO进行探索,从KVO的介绍 -》KVO的坑点 -》 KVO的大致流程 -》KVO的自定义实现 -》优秀的KVO封装库介绍。
一、KVO是什么?
KVO的全称为:Key-Value Observing,“键值监听”。
主要作用为:监听某个对象属性值的改变,继而进行对应的业务处理。
简单使用的代码:
self.person = [[MYPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
}
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"likeSome"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
int b = 100 + (arc4random() % 101);
NSString *tt = [NSString stringWithFormat:@"value: %d",b];
self.person.name = tt;
}
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"--keypath = 【%@】 -change = 【%@】", keyPath,change);
}
二、KVO注意项
1. KVO中的Context有什么作用?
根据官方文档 : KVO官方说明 上面介绍到: Context起到的作用就是一个标识符, 主要是为了监听对象更加安全,万一监听的属性路径一致,导致无法区分监听场景而诞生的,下面展示下官方的一段源码:
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self
forKeyPath:@"balance"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:PersonAccountBalanceContext];
[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) {
} else if (context == PersonAccountInterestRateContext) {
} else {
}
}
2. 忘记移除观察者,而造成程序的崩溃
官方文档中,有这么一句话:
An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.
大概的意思就是: 观察者不会在dealloc的时候自动移除,而观察对象后续的操作就算观察者依附的对象已经dealloc也会继续发送消息, 这样就会导致程序崩溃。 所以我们需要手动移除观察者, 后面有更加优化的方案解决这个问题。 请继续读下去。
3. 控制某些属性不能使用KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqual: @"name"]) {
return false;
} else {
return true;
}
}
4. 一对多的观察。
是否有遇到过这种场景: A属性的值, 是取决于另外两个属性B、C之间的计算才能得出, 这个时候对A属性进行观察,怎么办呢?
官方举例: fullName = firstName + lastName 当firstName 或者 lastName ,任意一个属性发生改变,必然会影响到 fullName。 那么就需要用到下面这个方法:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
[self.person addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:nil];
5. 对可变数组的KVO。
通过mutableArrayValueForKey 来实现, 直接上用法。
[self.person addObserver:self forKeyPath:@"dateArray" options:NSKeyValueObservingOptionNew context:NULL];
self.person.dateArray = [NSMutableArray array];
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];
三、KVO的流程和原理
自动键值观察:是通过isa-swizzling的技术实现。
- 在原有类的基础上新增一个派生类 NSKVONotifying_ 开头。
可通过代码来调试出来:
self.person = [[MYPerson alloc] init];
[self printClasses:[self.person class]];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[self printClasses:[self.person class]];
- (void)printClasses:(Class)cls {
int count = objc_getClassList(NULL, 0);
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
Class* classes = (Class *)malloc(sizeof(Class) *count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
2021-09-10 17:31:51.644193+0800 001-内存对齐原则[18934:903108] classes = (
MYPerson
)
2021-09-10 17:31:51.665579+0800 001-内存对齐原则[18934:903108] classes = (
MYPerson,
"NSKVONotifying_MYPerson"
)
- 关于在移除观察者的时候, 会不会吧isa指向会原类。
我们在移除观察者代码前后,打上断点去观察, 就会发现,isa会指向原类 。
-(void)dealloc {
[self.person removeObserver:self forKeyPath:@"name"];
}
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $0 = 0x000000028232f380 "NSKVONotifying_MYPerson"
(lldb) p object_getClassName(self.person)
(const char * _Nonnull) $1 = 0x0000000100907618 "MYPerson"
整体流程为: 当前类设置观察者后, 会派生一个子类(名称开头为NSKVONotifying_), 这个派生类和原来的类一模一样, 并且会在原来的类之上增加对属性的 setter方法监听,也就是 willChange , didChange。 当这些被观察的属性发生改变的时候,就会给所有的观察者,发送指令, 这个属性改变了,你该干啥就要干啥了。 这些设置好后,就把原类的isa指向这个派生的类, 最后在原类移除观察者的时候,就会将isa还原回来。
|