KVO底層原理

概念

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)聽前后person1person2的類對(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í),修改父類的值得過程
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容