写在前面
- 基础使用
- 基本原理
- 最佳实践 FBKVOController
基础使用
监听一个对象的属性变化,比如监听TCKVOObject
的name
属性
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation TCKVOObject
@end
// 监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionNew
context:nil];
回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
NSLog(@"\n%@\n%@", keyPath, change);
}
移除监听
[obj removeObserver:self forKeyPath:@"name"];
触发
obj.name = @"aa";
obj.name = @"bb";
打印
//obj.name = @"aa";
name
{
kind = 1;
new = aa;
}
//obj.name = @"bb";
name
{
kind = 1;
new = bb;
}
关于 NSKeyValueObservingOptions
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions)
{
///改变之后的值
NSKeyValueObservingOptionNew = 0x01,
///改变之前的值
NSKeyValueObservingOptionOld = 0x02,
///初始值,addObserver时就会调用
NSKeyValueObservingOptionInitial = 0x04,
///修改前后会调用
NSKeyValueObservingOptionPrior = 0x08
};
监听属性的属性
比如:监听TCKVOObject
中的subObj.count
的变化
@interface TCKVOSubObject : NSObject
@property (nonatomic, assign) NSInteger count;
@end
@implementation TCKVOSubObject
@end
@interface TCKVOObject : NSObject
@property (nonatomic, strong) TCKVOSubObject *subObj;
@end
监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
obj.subObj = [[TCKVOSubObject alloc] init];
[obj addObserver:self
forKeyPath:@"subObj.count"
options:NSKeyValueObservingOptionNew
context:nil];
监听集合属性
比如:监听array的变化
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSMutableArray *array;
@end
@implementation TCKVOObject
@end
监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"array"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:nil];
触发核心方法
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
触发
obj.array = [NSMutableArray array];
NSMutableArray *array = [obj mutableArrayValueForKey:@"array"];
[array addObject:@(1)];
[array addObject:@(2)];
[array removeObjectAtIndex:0];
array[0] = @(3);
obj.array = nil;
打印
//obj.array = [NSMutableArray array];
array
{
kind = 1;
new = (
);
old = "<null>";
}
//[array addObject:@(1)];
array
{
indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
1
);
}
//[array addObject:@(2)];
array
{
indexes = "<_NSCachedIndexSet: 0x6000023686a0>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
2
);
}
//[array removeObjectAtIndex:0];
array
{
indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 3;
old = (
1
);
}
//array[0] = @(3);
array
{
indexes = "<_NSCachedIndexSet: 0x600002368680>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
3
);
old = (
2
);
}
//obj.array = nil;
array
{
kind = 1;
new = "<null>";
old = (
3
);
}
注意:addObject:
只有new
,removeObjectAtIndex:
只有old
,不要以集合整体来看就可以了,单看具体操作的元素就可以了,比如添加一个元素时,初始时没有的,自然是没有old
值。
关于kind
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
///设置一个新值
NSKeyValueChangeSetting = 1,
///表示一个对象被插入到一对多关系的属性
NSKeyValueChangeInsertion = 2,
///表示一个对象被从一对多关系的属性中移除
NSKeyValueChangeRemoval = 3,
///表示一个对象在一对多的关系的属性中被替换
NSKeyValueChangeReplacement = 4,
};
监听依赖属性
当一个属性有多个属性组成时,其中一个属性变化时,组合属性也需要变化
比如:我们需要监听string
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSString *subString1;
@property (nonatomic, strong) NSString *subString2;
@property (nonatomic, strong) NSString *string;
@end
@implementation TCKVOObject
- (NSString *)string
{
return [NSString stringWithFormat:@"%@-%@", self.subString1, self.subString2];
}
@end
我们需要在TCKVOObject
类中添加下面方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
修改后
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSString *subString1;
@property (nonatomic, strong) NSString *subString2;
@property (nonatomic, strong) NSString *string;
@end
@implementation TCKVOObject
- (NSString *)string
{
return [NSString stringWithFormat:@"%@-%@", self.subString1, self.subString2];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
if ([key isEqualToString:@"string"])
{
return [NSSet setWithObjects:@"subString1", @"subString2", nil];
}
return [super keyPathsForValuesAffectingValueForKey:key];
}
@end
监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"string"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:nil];
触发
obj.subString1 = @"subString1";
obj.subString2 = @"subString2";
打印
//obj.subString1 = @"subString1";
string
{
kind = 1;
new = "subString1-(null)";
old = "(null)-(null)";
}
//obj.subString2 = @"subString2";
string
{
kind = 1;
new = "subString1-subString2";
old = "subString1-(null)";
}
手动触发
有时候不需要属性一改变就回调,需要在属性变化为某种条件才触发
比如:手动监听string
属性
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSString *string;
@end
@implementation TCKVOObject
@end
我们需要TCKVOObject
中添加下面核心方法
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
修改后
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSString *string;
@end
@implementation TCKVOObject
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"string"])
{
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
@end
监听
TCKVOObject *obj = [[TCKVOObject alloc] init];
[obj addObserver:self forKeyPath:@"string"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:nil];
触发
[obj willChangeValueForKey:@"string"];
obj.string = @"aa";
[obj didChangeValueForKey:@"string"];
手动触发集合属性
监听数组array
的变化
@interface TCKVOObject : NSObject
@property (nonatomic, strong) NSMutableArray *array;
@end
@implementation TCKVOObject
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"array"])
{
return NO;
}
return YES;
}
@end
触发
NSMutableArray *tmp = [obj mutableArrayValueForKey:@"array"];
[obj willChangeValueForKey:@"array"];
[tmp addObject:@"1"];
[tmp addObject:@"2"];
[obj didChangeValueForKey:@"array"];
[obj willChangeValueForKey:@"array"];
tmp[0] = @"3";
[obj didChangeValueForKey:@"array"];
打印
/*
[obj willChangeValueForKey:@"array"];
[tmp addObject:@"1"];
[tmp addObject:@"2"];
[obj didChangeValueForKey:@"array"];
*/
array
{
kind = 1;
new = (
1,
2
);
old = (
);
}
/*
[obj willChangeValueForKey:@"array"];
tmp[0] = @"3";
[obj didChangeValueForKey:@"array"];
*/
array
{
kind = 1;
new = (
3,
2
);
old = (
1,
2
);
}
小结
- 移除观察者之前没有添加,会闪退
- 只添加了观察者没有释放,当观察者释放了,触发回调会闪退(监听和移除最好成双成对)
- 重复添加,会多次回调
实现原理
首次为对象添加观察者时addObserver:forKeyPath:options:context
,通过运行时为动态生成一个子类,把当前对象的isa指针指向新的子类,在新的子类中重写了需要观察的属性的set方法、class方法,在set方法中通过调用willChangeValueForKey:
和didChangeValueForKey:
方法,其中在didChangeValueForKey:
中调用了observeValueForKeyPath:ofObject:change:context
方法进行回调,重写的class方法仍然返回的是父类,这个外部使用就没有感知了。
验证如下:
@interface TCKVOObject : NSObject
@property (nonatomic, assign) NSInteger count;
@end
@implementation TCKVOObject
- (void)willChangeValueForKey:(NSString *)key
{
NSLog(@"begin->willChangeValueForKey:%@", key);
[super willChangeValueForKey:key];
NSLog(@"end->willChangeValueForKey:%@", key);
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"begin->didChangeValueForKey:%@", key);
[super didChangeValueForKey:key];
NSLog(@"end->didChangeValueForKey:%@", key);
}
@end
//测试
{
_obj = [[TCKVOObject alloc] init];
[_obj addObserver:self
forKeyPath:@"count"
options:NSKeyValueObservingOptionNew
context:nil];
_obj.count ++;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
NSLog(@"\nkeyPath:%@ change:%@", keyPath, change);
}
/*
打印如下:
begin->willChangeValueForKey:count
end->willChangeValueForKey:count
begin->didChangeValueForKey:count
keyPath:count change:{
kind = 1;
new = 1;
}
end->didChangeValueForKey:count
*/
这里我们可以验证 didChangeValueForKey:
方法里调用了 observeValueForKeyPath:ofObject:change:context
。
添加 addObserver:forKeyPath:options:context
之前
添加 addObserver:forKeyPath:options:context
之后
_obj
对象的 isa
指针指向的对象为 NSKVONotifying_TCKVOObject
我们再打印出 NSKVONotifying_TCKVOObject
的全部新加方法
- (void)allMethodWithObj:(id)obj
{
unsigned int count = 0;
Method *methodList = class_copyMethodList(object_getClass(obj), &count);
for(int i = 0; i < count; i++)
{
SEL sel = method_getName(methodList[i]);
NSString *methodName = NSStringFromSelector(sel);
NSLog(@"%@", methodName);
}
}
/*
打印如下:
setCount:
class
dealloc
_isKVOA
*/
最佳实践 FBKVOController
先看效果
{
_obj = [[TCKVOObject alloc] init];
[self.KVOController observe:_obj keyPath:@"count" options:NSKeyValueObservingOptionNew
block:^(id _Nullable observer,
id _Nonnull object,
NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
NSLog(@"%@", change);
}];
_obj.count ++;
}
支持Block,也不需要关注移除时机,用法非常方便。
FBKVOController 底层原理
要实现这样的效果,我们只需要把观察者回调逻辑转移至 FBKVOController
,当 observeValueForKeyPath:ofObject:change:context
方法回调时,再抛出来即可。
FBKVOController
可以监听多个对象,每个对象又可以监听多个属性。可以通过 NSMapTable
存储,其中 key 为被监听的对象,而 value 用来存储被监听对象的属性,由于可以监听多个属性,可以使用列表,考虑到重复的属性没有意义,用 Set 来去重再合适不过了。
这里有个细节需要注意,假设在 A
对象中需要监听 A
对象的属性 p1
,那么在 A
对象中需要持有 FBKVOController
对象,而在向 FBKVOController
对象添加监听时,需要把 A
对象传入 FBKVOController
存入 NSMapTable
的 Key 中,这就造成了循环引用。为了解决这个问题,可以通过设置 NSMapTable
的 Key 为 NSPointerFunctionsWeakMemory
通过弱引用存储。
KVO 还是有很多多线程应用场景,所以在操作 NSMapTable
时对其加锁处理就可,所以 FBKVOController
是线程安全的。
关于释放问题,当持有 FBKVOController
对象释放时,FBKVOController
对象也自动释放,在 FBKVOController
中 dealloc
方法移除相关数据即可。
最后,思考一下为什么要用单例 _FBKVOSharedController
来监听回调,而不在 FBKVOController
中直接实现?
原因是:在 KVO 中,如果观察者对象释放了,这时候触发回调就会闪退,所以采用单例 _FBKVOSharedController
来监听回调就很容易避免这个问题了。
网友评论