KVO
(Key-Value Observing
),是苹果提供的一套事件通知机制。允许对象监听特定属性的改变,并在改变时接收到事件。KVO
是建立在KVC
上的,所以对属性才会发生作用,一般继承NSObject
的对象都默认支持KVO
。
一、KVO初探
1.1 context 的作用
//- (void)addObserver:(NSObject *)observer toObjectsAtIndexes:(NSIndexSet *)indexes forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
注意:context:
这里我们不应该用 nil,应该是用 NULL
才行,因为它对应的是 void *
。
@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic, assign) double writtenData;
@property (nonatomic, assign) double totalData;
@property (nonatomic, strong) NSMutableArray *dateArray;
@property (nonatomic, strong) LGStudent *st;
@end
我们可以看到,这里可以有多个对象,却有相同的keyPath
,那么我们在方法 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
中该如何区分呢?
通过
context
这种字符串直接匹配,会更加便利,更加安全,性能会更高。相当于一个Tag
1.2 移除通知观察
当我们不需要继续观察对象了,就需要移除观察者。在观察者被释放之前必须移除观察,否则观察者在释放后会再次接收到KVO,野指针会导致程序崩溃。
1.3 多嵌套路径
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
这里我们以下载进度(
downloadProgress = writtenData/totalData
)为例,downloadProgress
跟totalData、writtenData
相关,只要里面一个更改,对应的downloadProgress
肯定也跟着发生变化。
1.4 自动和手动
KVO默认是自动通知,也就是当我们属性的值变化时,就会自动发送通知。我们可以重写
utomaticallyNotifiesObserversForKey:
方法来控制是否启动自动通知。
- 返回为
YES
时,该对象的所有属性启用自动通知。 - 返回为
NO
时,该对象的所有属性禁用自动通知。
当我们要手动通知时,可以在 set<key>
中,手动调用 willChangeValueForKey:
(更改值之前)和 didChangeValueForKey:
(更改值之后)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
1.5 可变数组
- (void)insertObject:(id)object inDateArrayAtIndex:(NSUInteger)index{
[self.dateArray insertObject:object atIndex:index];
}
-(void)removeObjectFromDateArrayAtIndex:(NSUInteger)index{
[self.dateArray removeObjectAtIndex:index];
}
//------ 验证代码 ------
// 数组变化
[self.person.dateArray addObject:@"1"];
// KVO 建立在 KVC
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
打印信息如下:
2020-03-10 15:40:33.429762+0800 001---KVO初探[12157:813687] CHJViewController - {
indexes = "<_NSCachedIndexSet: 0x6000030d9940>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
2
);
}
当我们直接调用
addObject:
时,是不走insertObject:
方法,而通过mutableArrayValueForKey:
是会走insertObject:
方法的。
那么问题又来了,这个kind
为什么会变成2呢?
/* Possible kinds of set mutation for use with -willChangeValueForKey:withSetMutation:usingObjects: and -didChangeValueForKey:withSetMutation:usingObjects:.
Their semantics correspond exactly to NSMutableSet's -unionSet:, -minusSet:, -intersectSet:, and -setSet: method, respectively.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueSetMutationKind) {
NSKeyValueUnionSetMutation = 1, //正常的
NSKeyValueMinusSetMutation = 2, //集合类型的
NSKeyValueIntersectSetMutation = 3,//set的添加
NSKeyValueSetSetMutation = 4//set其他情况
};
由上可知,是因为
NSKeyValueChangeKey
是一个枚举,mutableArrayValueForKey:
时是集合类型。
二、KVO原理
KVO
的核心是isa-swizzling
。简单来说就是修改了原对象的isa
指针,使其指向一个中间类而不是真正的类,所以isa
指针的值并不能反应实例的实际类,所以应该使用class
方法来确定对象的实际类。
@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
//----- 验证------
self.person = [[CHJPerson alloc] init];
Class cls = object_getClass(self.person);
NSLog(@"cls == %@",NSStringFromClass(cls));
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
Class cls2 = object_getClass(self.person);
NSLog(@"cls2 == %@",NSStringFromClass(cls2));
打印信息如下:
2020-03-10 16:24:02.625080+0800 002---KVO原理探讨[14000:943952] cls == CHJPerson
2020-03-10 16:24:17.990241+0800 002---KVO原理探讨[14062:952265] cls2 == NSKVONotifying_CHJPerson
我们发现两次输出的结果不一样,对象没有添加KVO之前,
isa
指针指向的是CHJPerson
类,添加以后,指向的是NSKVONotifying_CHJPerson
。至此我们可以得出对象在添加KVO之后,在运行时为我们动态生成了一个NSKVONotifying_XXX
的中间类,并且将这个对象的isa
指针指向了这个中间类。
2.1 这个 NSKVONotifying_CHJPerson
和 CHJPerson
是什么关系呢?
Class cls2 = object_getClass(self.person);
NSLog(@"cls2 == %@",NSStringFromClass(cls2));
Class supCls = cls2;
do {
supCls = [supCls superclass];
NSLog(@"supCls == %@",NSStringFromClass(supCls));
} while (supCls);
NSLog(@"supCls == %@",NSStringFromClass(supCls));
打印信息:
2020-03-10 16:47:05.437996+0800 002---KVO原理探讨[14494:1016365] cls2 == NSKVONotifying_ CHJPerson
2020-03-10 16:47:05.438197+0800 002---KVO原理探讨[14494:1016365] supCls == CHJPerson
2020-03-10 16:47:05.438286+0800 002---KVO原理探讨[14494:1016365] supCls == NSObject
2020-03-10 16:47:05.438378+0800 002---KVO原理探讨[14494:1016365] supCls == (null)
2020-03-10 16:47:05.438448+0800 002---KVO原理探讨[14494:1016365] supCls == (null)
看到这,相信大家都晓得了吧,这个
NSKVONotifying_CHJPerson
是直接继承CHJPerson
的。
2.2 那么实例变量和属性的区别又是什么呢?
@interface CHJPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
@implementation CHJPerson
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
@end
//-------- 验证 --------
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"CHJ";
self.person->name = @"CC";
}
打印信息:
2020-03-10 17:00:35.855711+0800 002---KVO原理探讨[14803:1061366] 实际情况:(null)-(null)
2020-03-10 17:00:35.855979+0800 002---KVO原理探讨[14803:1061366] {
kind = 1;
new = CHJ;
}
我们可以看到属性
name
没有被打印出来,而实例变量nickName
却有。从而可以得出KVO
主要观察的应该是setter
。
2.3 CHJPerson
的动态子类里面做了些什么呢?
@interface CHJPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
- (void)sayGod;
@end
@interface CHJStudent : CHJPerson
@end
@implementation CHJPerson
- (void)sayHello{
}
//---------验证---------
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_CHJPerson")];
[self printClassAllMethod:[CHJStudent class]];
打印信息:
2020-03-10 17:39:30.760719+0800 002---KVO原理探讨[15685:1202958] *********************
2020-03-10 17:39:30.760832+0800 002---KVO原理探讨[15685:1202958] setNickName:-0x7fff2564cec6
2020-03-10 17:39:30.760935+0800 002---KVO原理探讨[15685:1202958] class-0x7fff2564b989
2020-03-10 17:39:30.761006+0800 002---KVO原理探讨[15685:1202958] dealloc-0x7fff2564b6ee
2020-03-10 17:39:30.761092+0800 002---KVO原理探讨[15685:1202958] _isKVOA-0x7fff2564b6e6
2020-03-10 17:39:30.761200+0800 002---KVO原理探讨[15685:1202958] *********************
2020-03-10 17:39:30.761268+0800 002---KVO原理探讨[15685:1202958] sayHello-0x10d48b540
根据打印,我们可以得出,动态子类重写了
setNickName (setter)
、class
、dealloc
、_isKVOA
。
这里我多打印一个CHJStudent
是为了更加直观验证只有被重写了的,才会被打印出来。
2.4 移除观察,isa
是否指回来?
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"nickName"];
}
通过lldb,打印信息:
(lldb) po object_getClass(self.person)
CHJPerson
(lldb)
所以在移除观察后,
isa
是指回CHJPerson
的。
2.5 动态子类是否会销毁?
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self printClasses:[LGPerson class]];
}
#pragma mark - 遍历类以及子类
- (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(@"CHJViewController:classes = %@", mArray);
}
打印信息:
2020-03-10 17:56:09.977303+0800 002---KVO原理探讨[15919:1242029] LGViewController:classes = (
CHJPerson,
CHJStudent,
"NSKVONotifying_CHJPerson"
)
由此,我们可以知道,动态的子类是不会被销毁的
小结:
- 动态生成子类 :
NSKVONotifying_xxx
- 观察的是
setter
- 动态子类重写了很多方法
setter
、class
、dealloc
、_isKVOA
- 移除观察的时候
isa
指向回来 - 动态子类不会销毁
未完待续。。。
网友评论