概念
KVO的全稱是Key-Value- Observing,俗稱“鍵值監(jiān)聽”,可以用于監(jiān)聽某個(gè)對(duì)象屬性值的改變。
本質(zhì)
- 利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類,并且讓改instance對(duì)象的isa指向這個(gè)全新的子類
- 當(dāng)修改對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù)
1.先調(diào)用willChangeValueForKey:
2.調(diào)用父類的setter方法
3.調(diào)用didChangeValueForKey: - 內(nèi)部觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)
底層原理探索
先看以下一段簡單的KVO的代碼實(shí)現(xiàn),觀察RMPerson的屬性age改變,簡單的代碼實(shí)現(xiàn)
#import "ViewController.h"
#import "RMPerson.h"
@interface ViewController ()
@property (nonatomic, strong) RMPerson *person1;
@property (nonatomic, strong) RMPerson *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.person1 = [[RMPerson alloc] init];
self.person1.age = 10;
self.person2 = [[RMPerson alloc] init];
self.person2.age = 20;
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.person1 setAge:11];
[self.person2 setAge:21];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變, options:%@ context:%@", object, keyPath, change, context);
}
- (void)dealloc
{
[self.person1 removeObserver:self forKeyPath:@"age"];
}
@end
--------------------------------------------------------------------------------------------------------------------------
//RMPerson.h
#import <Foundation/Foundation.h>
@interface RMPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end
//RMPerson.M
#import "RMPerson.h"
@implementation RMPerson
- (void)setAge:(int)age
{
_age = age;
}
@end
在以上代碼中,對(duì)象person1的屬性age添加監(jiān)聽和person2的屬性age沒有監(jiān)聽作對(duì)比,可以猜測(cè),兩者調(diào)用的都是對(duì)象的setAge:方法,為何添加了KVO監(jiān)聽的peron1會(huì)回調(diào)方法observeValueForKeyPath:ofObject:change:context:,而person2卻沒有。
猜測(cè):
- 1.蘋果調(diào)用
setAge:上做了手腳 - 2.會(huì)不會(huì)是生成了一個(gè)新的中間對(duì)象或者利用OC獨(dú)有的運(yùn)行時(shí)狀態(tài),生成了一個(gè)RMPerson的子類,重寫了age的setter方法?
帶著上面兩點(diǎn)疑問,打印下監(jiān)聽前后person1和person2的類對(duì)象是否有變化,看以下代碼
1、監(jiān)聽前后類對(duì)象的變化
NSLog(@"監(jiān)聽前 %@:%p %@:%p",
object_getClass(self.person1),object_getClass(self.person1),
object_getClass(self.person2),object_getClass(self.person2));
// 給person對(duì)象添加KVO監(jiān)聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"監(jiān)聽后 %@:%p %@:%p",
object_getClass(self.person1),object_getClass(self.person1),
object_getClass(self.person2),object_getClass(self.person2));
//打印結(jié)果
2018-08-24 17:40:19.689016+0800 KVO[60640:4402657] 監(jiān)聽前 RMPerson:0x10e2600b0 RMPerson:0x10e2600b0
2018-08-24 17:40:20.621251+0800 KVO[60640:4402657] 監(jiān)聽后 NSKVONotifying_RMPerson:0x608000112750 RMPerson:0x10e2600b0
從上面代碼看出,
1.監(jiān)聽前兩者都是RMPerson類,其類對(duì)象地址是一致的
2.給對(duì)象person1的屬性age添加監(jiān)聽后,對(duì)象person1的打印的類是NSKVONotifying_RMPerson,
所以,我們可以得出給對(duì)象屬性添加監(jiān)聽后,蘋果會(huì)動(dòng)態(tài)的生成一個(gè)NSKVONotifying_XXX的中間類,其繼承于RMPerson,如何得知呢?打印一下NSKVONotifying_RMPerson的父類 [object_getClass(self.person1) superClass]即可知道其父類是RMPerson。
2、監(jiān)聽前后其"setAge:方法地址"
打印下添加監(jiān)聽后其setAge:方法地址
NSLog(@"person1添加KVO監(jiān)聽之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
2018-08-27 11:02:25.651418+0800 KVO[72155:5132928] person1添加KVO監(jiān)聽之后 - 0x100669f8e 0x1002c4520
(lldb) p (IMP)0x100669f8e
(IMP) $0 = 0x0000000100669f8e (Foundation`_NSSetIntValueAndNotify)
(lldb) p (IMP)0x1002c4520
(IMP) $1 = 0x00000001002c4520 (KVO`-[RMPerson setAge:] at RMPerson.m:13)
(lldb)
從上面,打印后,使用lldb打印其地址的內(nèi)容,可以得知,監(jiān)聽對(duì)象person1的屬性age后,其setAge:方法底層實(shí)際是調(diào)用了_NSSetIntValueAndNotify方法,此方法的偽代碼如下
- (void)setAge:(int)age
{
_NSSetIntValueAndNotify();
}
// 偽代碼
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
- (void)didChangeValueForKey:(NSString *)key
{
// 通知監(jiān)聽器,某某屬性值發(fā)生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
要了解其_NSSetIntValueAndNotify其方法的實(shí)現(xiàn),要反編譯其Foundation框架的底層實(shí)現(xiàn),有興趣的自己再了解。從上面我們可看出,setAge:方法實(shí)際實(shí)現(xiàn)了此三個(gè)方法
① willChangeValueForKey:
② [super setAge:age]
③ didChangeValueForKey
最后再通知監(jiān)聽器,告訴其某某屬性值發(fā)生改變了,回調(diào)了[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];此方法。
總結(jié)
使用KVO監(jiān)聽了某個(gè)對(duì)象的屬性,實(shí)際就是
- 利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類,并且讓改instance對(duì)象的isa指向這個(gè)全新的子類
- 當(dāng)修改對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù)
1.先調(diào)用willChangeValueForKey:
2.調(diào)用父類的setter方法
3.調(diào)用didChangeValueForKey: - 內(nèi)部觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法(observeValueForKeyPath:ofObject:change:context:)
為何稱KVO為黑魔法,實(shí)際上掩飾了利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類,屬性值發(fā)生了改變時(shí),修改父類的值得過程