iOS底層原理 - 探尋KVO本質(zhì)

面試題引發(fā)的思考:

Q: iOS用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?即KVO的本質(zhì)是什么?

  • 利用RuntimeAPI動(dòng)態(tài)生成一個(gè)子類,并且讓instance對(duì)象的isa指向這個(gè)全新的子類。

  • 當(dāng)修改instance對(duì)象的屬性時(shí),會(huì)調(diào)用Foundation_NSSetXXXValueAndNotify函數(shù)
    a>willChangeValueForKey:
    b>父類原來的setter方法對(duì)成員變量進(jìn)行賦值
    c>didChangeValueForKey:
    d>內(nèi)部會(huì)觸發(fā)監(jiān)聽器(observer)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

Q: 如何手動(dòng)觸發(fā)KVO?

  • 手動(dòng)調(diào)用willChangeValueForKey:didChangeValueForKey:。

Q: 直接修改成員變量會(huì)觸發(fā)KVO嗎?

  • 不會(huì)觸發(fā)KVO。

1. KVO介紹

KVO的全稱是Key-Value Observing,即“鍵值監(jiān)聽”,用于監(jiān)聽某個(gè)對(duì)象屬性值的改變

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    person1.age = 10;
    Person *person2 = [[Person alloc] init];
    person2.age = 20;
    
    // 給person對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
    person1.age = 20;   // [person1 setAge:20];
    person2.age = 30;   // [person2 setAge:30];

    [person1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變 - %@", object, keyPath, change);
}

// 打印結(jié)果
Demo[1234:567890] 監(jiān)聽到<Person: 0x600001608200>的age屬性發(fā)生了改變 - {
    kind = 1;
    new = 20;
    old = 10;
}

由打印結(jié)果可知:

輸出的是person1的相關(guān)內(nèi)容;給person1對(duì)象添加KVO監(jiān)聽后,age屬性的值發(fā)生改變時(shí),監(jiān)聽者observeValueForKeyPath的方法會(huì)被調(diào)用執(zhí)行。


2. KVO的本質(zhì)

(1)未使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程

person1person2的屬性age賦值,都會(huì)調(diào)用相同的set方法,而set方法的實(shí)現(xiàn)也是一樣的。

Q: 那為什么只會(huì)打印出person1的相關(guān)內(nèi)容?

可以猜測(cè)是跟類的對(duì)象方法沒有關(guān)系,跟類對(duì)象本身有關(guān)。

使用KVO監(jiān)聽對(duì)象與未使用KVO監(jiān)聽對(duì)象的區(qū)別

我們知道instance對(duì)象的isa指向class對(duì)象,所以:

  • person1的類對(duì)象是NSKVONotifying_Person,person2的類對(duì)象是Person
  • NSKVONotifying_Person則是使用Runtime動(dòng)態(tài)創(chuàng)建的一個(gè)類,是Person的一個(gè)子類。
未使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程

由上圖可知:
person2在調(diào)用setAge:方法的時(shí)候,首先根據(jù)person2isa找到Person的class對(duì)象,然后在class對(duì)象中找到setAge:,然后實(shí)現(xiàn)方法。這是未使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程。

(2) 使用KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程

根據(jù)相關(guān)資料可知:

NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify,其內(nèi)部實(shí)現(xiàn)流程為:

  • 首先調(diào)用willChangeValueForKey:方法
  • 然后調(diào)用父類的setAge:方法對(duì)成員變量進(jìn)行賦值
  • 最后調(diào)用didChangeValueForKey:方法,此方法內(nèi)部會(huì)觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
使用了KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程

由上圖可知:

person1在調(diào)用setAge:方法的時(shí)候,首先根據(jù)person1isa找到NSKVONotifying_Person的class對(duì)象,然后在class對(duì)象中找到setAge:,然后實(shí)現(xiàn)方法。這是使用了KVO監(jiān)聽的對(duì)象實(shí)現(xiàn)流程。

1> 驗(yàn)證1:NSKVONotifying_Person的內(nèi)部結(jié)構(gòu)
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];
    
    // 給person對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
    [self printMethodNamesOfClass:object_getClass(person2)];
    [self printMethodNamesOfClass:object_getClass(person1)];
}

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲(chǔ)方法名
    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);
}

// 打印結(jié)果
Demo[1234:567890] Person - age, setAge:,
Demo[1234:567890] NSKVONotifying_Person - setAge:, class, dealloc, _isKVOA,

由打印結(jié)果可知:

NSKVONotifying_Person中有4個(gè)對(duì)象方法,分別為 setAge:、class、dealloc_isKVOA;證實(shí)了其內(nèi)部結(jié)構(gòu)。

2> 驗(yàn)證2:NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];

    NSLog(@"person1添加KVO監(jiān)聽之前 - %p %p",
          [person1 methodForSelector:@selector(setAge:)],
          [person2 methodForSelector:@selector(setAge:)]);

    // 給person對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"person1添加KVO監(jiān)聽之后 - %p %p",
          [person1 methodForSelector:@selector(setAge:)],
          [person2 methodForSelector:@selector(setAge:)]);

    [person1 removeObserver:self forKeyPath:@"age"];
}
打印結(jié)果

由打印結(jié)果可知:

person1添加KVO監(jiān)聽之前,person1person2setAge:方法的地址相同;person1添加KVO監(jiān)聽之后,person1setAge:方法的地址發(fā)生改變。

打印結(jié)果證實(shí)了:

NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify。

內(nèi)部實(shí)現(xiàn)偽代碼如下:

- (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];
}
3> 驗(yàn)證3:NSKVONotifying_Person會(huì)重寫class方法
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];

    // 給person對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"%@ %@", [person1 class], [person2 class]);
    NSLog(@"%@ %@", object_getClass(person1), object_getClass(person2));

    [person1 removeObserver:self forKeyPath:@"age"];
}

// 打印結(jié)果
Demo[1234:567890] Person Person
Demo[1234:567890] NSKVONotifying_Person Person

由打印結(jié)果可知:

通過class方法獲取到的類為Person,而通過runtime的object_getClass方法獲取到的類為NSKVONotifying_Person,說明NSKVONotifying_Person重寫了class方法

Q: 那為什么要重寫class方法呢?

很明顯,蘋果不想讓NSKVONotifying_Person這個(gè)類暴露出來,不希望開發(fā)者知道其內(nèi)部實(shí)現(xiàn),其class方法內(nèi)部實(shí)現(xiàn)應(yīng)該是以下:

// 屏蔽內(nèi)部實(shí)現(xiàn),隱藏了NSKVONotifying_Person類的存在
- (Class)class {
    // 1.獲取類對(duì)象  2.獲取類對(duì)象父類
    return class_getSuperclass(object_getClass(self));
}
4> 驗(yàn)證4:didChangeValueForKey:內(nèi)部會(huì)觸發(fā)observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

Person類中,重寫willChangeValueForKey:didChangeValueForKey:方法:

// TODO: -----------------  Person類  -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

@implementation Person
- (void)setAge:(int)age {
    _age = age;
    NSLog(@"setAge:");
}

- (void)willChangeValueForKey:(NSString *)key {
    NSLog(@"willChangeValueForKey - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey - end");
}

- (void)didChangeValueForKey:(NSString *)key {
    NSLog(@"didChangeValueForKey - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey - end");
}
@end

// TODO: -----------------  ViewController類  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    person.age = 10;

    // 給person對(duì)象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person addObserver:self forKeyPath:@"age" options:options context:nil];

    person.age = 20;

    [person removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變 - %@", object, keyPath, change);
}
打印結(jié)果 - setAge:實(shí)現(xiàn)順序

由打印結(jié)果可知:

didChangeValueForKey:內(nèi)部會(huì)調(diào)用observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:。

最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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