看本章之前建议先打好OC对象的基础:深入理解OC中的对象
大纲
- 什么是KVO
- 场景代码示例 (后面的分析都是基于示例代码展开)
- NSKVONotifying_XXX 类对象
- _NSSetXXXValueAndNotify 方法
- 自定义手动触发KVO
- KVO总结
- 拓展补充
1. 什么是KVO
KVO的全称是Key-Value Observing,俗称“键值监听”。可以用于监听某个对象属性值的改变。
- KVO使用:
- 确定需要监听的对象的属性。
- 确定监听者,并注册监听回调。
- 在监听回调中处理业务逻辑。
2. 场景代码示例
定义一个类,申明一个count属性
@interface KVOTest : NSObject
@property (nonatomic, assign) NSInteger count;
@end
@implementation KVOTest
@end
viewcontroller中创建对象,注册属性监听。点击屏幕的时候改变count值
static void *countContext = &countContext;
@interface ViewController ()
@property (nonatomic, strong) KVOTest *kvo;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.kvo = [[KVOTest alloc] init];
self.kvo.count = 1;
// 注册监听
[self.kvo addObserver:self
forKeyPath:@"count"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:countContext];
}
// 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == countContext)
{
NSInteger newCount = [[change valueForKey:NSKeyValueChangeNewKey] integerValue];
NSInteger oldCount = [[change valueForKey:NSKeyValueChangeOldKey] integerValue];
NSLog(@"new: %ld, old: %ld", newCount, oldCount);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 点击屏幕,改变count的值
self.kvo.count += 1;
}
@end
点击屏幕的时候修改count的值将会触发监听回调,控制台打印如下:
上面就是个最简单的KVO使用的场景示例了。
3. NSKVONotifying
我们回到上面的示例代码中添加如下代码:输出注册监听前后类对象和类对象对应的地址:
我们可以先猜测一下控制台的log信息,如果你只是停留在单纯使用KVO的阶段,应该会和我当初猜想的一致。注册监听前后log的“类对象”和“地址”应该都是一样的。而实际的结果是这样的: 是不是很惊讶,我们创建的KVOTest
实例对象的属性被注册了KVO监听后,苹果底层其实偷偷的做了修改,使用runtime运行时动态的创建了一个新的类NSKVONotifying_KVOTest
.通过对比类对象的地址0x102ac2a98
、0x600001f8cb40
也可以确认的确不是同一个类。大致流程如下:
- 我们先验证属性监听isa的变化:
- 监听后isa的确发生了变化,所以类对象的确发生了改变。
- 验证
NSKVONotifying_KVOTest
的父类是否是KVOTest
struct cw_objc_class {
Class _Nonnull isa;
Class _Nullable super_class;
};
- (void)viewDidLoad {
[super viewDidLoad];
self.kvo = [[KVOTest alloc] init];
self.kvo.count = 1;
// 监听之前的类对象
Class classObj = object_getClass(self.kvo);
[self.kvo addObserver:self
forKeyPath:@"count"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:countContext];
// 监听之后的
Class kvo_notify_class = object_getClass(self.kvo);
// 强转一下类型
struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)kvo_notify_class;
NSLog(@"KVOTest类对象: %@ - %p", classObj, classObj);
NSLog(@"NSKVONotifying_KVOTest类对象: %@ - %p", kvo_notify_class, kvo_notify_class);
NSLog(@"NSKVONotifying_KVOTest类对象的父类: %@ - %p", newClass->super_class, newClass->super_class);
}
控制台log如下:
KVOTest类对象: KVOTest - 0x109ca8ab8
NSKVONotifying_KVOTest类对象: NSKVONotifying_KVOTest - 0x600001edc090
NSKVONotifying_KVOTest类对象的父类: KVOTest - 0x109ca8ab8
NSKVONotifying_KVOTest
类对象的父类地址和KVOTest
类对象地址一致。
补充说明:
struct cw_objc_class { Class _Nonnull isa; Class _Nullable super_class; }; struct cw_objc_class *newClass = (__bridge struct cw_objc_class *)kvo_notify_class
Class类型的superclass指针是隐藏属性在外部不能访问,因为我们知道了Class其实也是结构体,并且前两个指针的类型:一个是isa,一个是superclass,所以这里我们可以自定义一个类似结构体,强转一下类型。这样我们就能拿到superclass了。
4. _NSSetXXXValueAndNotify
当属性发生改变时,我们需要通知到observer触发回调。之前的setter方法是满足不了需求的。所以底层继承原来的类,动态的创建了一个子类NSKVONotifying_XXX
来重写setter方法,做了一些额外的操作。我们可以通过一段代码来验证:
- (void)viewDidLoad {
[super viewDidLoad];
self.kvo = [[KVOTest alloc] init];
self.kvo.count = 1;
NSLog(@"监听之前的setter方法: %p", [self.kvo methodForSelector:@selector(setCount:)]);
[self.kvo addObserver:self
forKeyPath:@"count"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:countContext];
NSLog(@"监听之后的setter方法: %p", [self.kvo methodForSelector:@selector(setCount:)]);
}
控制台log如下:
OCTest[8254:266577] 监听之前的setter方法: 0x105e94f70
OCTest[8254:266577] 监听之后的setter方法: 0x7fff2591f5cd
监听前后的方法地址0x105e94f70
和0x7fff2591f5cd
不同,证实了setter方法的确被修改了。进一步断点调试:
- 注册监听前的setter方法调用的是setCount毫无悬念。
- 注册监听后的setter方法如上图,内部其实调用的是Foundation框架下的
_NSSetLongLongValueAndNotify
函数。
关于_NSSetLongLongValueAndNotify
函数,看字面意思就不难理解:setValue即设置新值,notify即通知触发observer回调。
至于LongLong
这是和属性的类型有关的。因为文中示例的count
是NSInteger
类型,这里我们可以修改成NSString
类型来验证下是否会发生变化.
@interface KVOTest : NSObject
@property (nonatomic, copy) NSString *count;
@end
断点调试:
(lldb) po (IMP)0x7fff2591e98b
(Foundation`_NSSetObjectValueAndNotify)
改成NSString
类型后,函数变成了_NSSetObjectValueAndNotify
。有兴趣的话可以多改改试试,这里不再赘述。如果你懂逆向的话是可以找到Foundation框架下所有的这种方法。
_NSSetValueAndNotify
内部实现在接下来一节手动触发KVO会提到。
5. 手动触发KVO
我们给类KVOTest
定义一个成员变量age
@interface KVOTest : NSObject {
@public NSInteger _age;
}
@end
外部在ViewController
中监听age
, 点击屏幕的时候修改age
的值
- (void)viewDidLoad {
[super viewDidLoad];
self.kvo = [[KVOTest alloc] init];
self.kvo->_age = 1;
[self.kvo addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:countContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == countContext)
{
NSInteger newCount = [[change valueForKey:NSKeyValueChangeNewKey] integerValue];
NSInteger oldCount = [[change valueForKey:NSKeyValueChangeOldKey] integerValue];
NSLog(@"new: %ld, old: %ld", newCount, oldCount);
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.kvo->_age += 1;
}
定义成员变量是不会像@property
关键字那样自动合成setter,getter方法的。这里的结果就是直接修改变量,并不能触发KVO机制。所以点击屏幕,回调是不会被触发的。直接修改变量值,不会触发KVO监听回调
接下来我们手动创建一个setter方法试试:
// .h文件
- (void)setAge:(NSInteger)age;
// .m文件
- (void)setAge:(NSInteger)age {
_age = age;
}
viewController
中调用setter方法
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.kvo setAge:18];
}
点击屏幕,监听回调触发
手动实现setter方法就触发了回调(即使我们将setter方法里面的赋值代码注释掉),进一步验证了上文中提到的子类
NSKVONotifying
重写setter方法的验证。
如果我们不按Apple的命名规范,自己定义一个setter方法会如何呢?
// .h文件
- (void)setNewAge:(NSInteger)age;
// .m文件
- (void)setNewAge:(NSInteger)age {
_age = age;
}
viewController
中改成自定义的方法:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.kvo setNewAge:18];
}
结果发现这样并不能触发KVO监听回调。
接下来我们改造一下自定义setter方法,添加一对方法
willChangeValueForKey
,didChangeValueForKey
- (void)setNewAge:(NSInteger)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
此时运行项目,再次点击屏幕修改值,KVO回调会被触发。而这也就是我们手动触发KVO
的方式。
6. 到这里我们可以总结一下KVO的流程:
- 给
KVOTest
类的instance对象
注册属性监听后 - 系统通过runtime动态的创建了一个
KVOTest
的子类NSKVONotifying_KVOTest
类,并将instance
对象的isa
指向它。 - 当属性发生改变时,调用
NSKVONotifying_KVOTest
的setter方法_NSSetXXXValueAndNotify
。
内部实现:
1. willChangeValueForKey
2. 调用父类的setter赋值
3. didChangeValueForKey
4. 触发监听器observer的回调方法。
7. 拓展补充
我们可以写一个方法来获取NSKVONotifying_XXX
中除了setter还有哪些方法
- (void)printMethodnamesOfClass:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [NSMutableString string];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"%@: %@", cls, methodNames);
}
调用方法
- (void)viewDidLoad {
[super viewDidLoad];
self.kvo = [[KVOTest alloc] init];
[self.kvo addObserver:self
forKeyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:countContext];
[self printMethodnamesOfClass:object_getClass(self.kvo)];
}
控制台log
NSKVONotifying_KVOTest: setAge:, class, dealloc, _isKVOA,
- 重写了class方法(这里返回的是父类
KVOTest
,这也是为什么做了监听操作之后调用[self.kvo class]和调用object_getClass(self.kvo)结果不同的原因), - 重写了delloc方法(额外的做了一些操作,所以也需要一些额外释放销毁操作)
- 新增了一个_isKVOA (标识)
网友评论