iOS Objective-C KVO 詳解

iOS Objective-C KVO 詳解

1. KVO

KVOKey-Value Observing是蘋(píng)果提供給開(kāi)發(fā)者的一套鍵值觀察的API跛溉,KVO是一種機(jī)制,它允許將其他對(duì)象的指定屬性的更改通知給對(duì)象柄慰。KVO是建立在KVC的基礎(chǔ)上的踢械,對(duì)于KVC的原理及應(yīng)用可以查看我的上一篇文章奸晴。下面我們來(lái)詳細(xì)的介紹KVO

1.1 KVO 可以觀察什么屬性贱枣?

根據(jù)KVO官方文檔的定義监署,我們可以知道可觀察的屬性分為以下三種:

  • attributes: 簡(jiǎn)單屬性,比如基本數(shù)據(jù)類(lèi)型纽哥,字符串布爾值等钠乏,諸如NSNumber和其他一些不可變類(lèi)型,比如NSColor也被認(rèn)為是簡(jiǎn)單屬性昵仅。
  • to-one relationships: 一對(duì)一關(guān)系,一個(gè)屬性的值取決于另一個(gè)值累魔。比如一個(gè)人的全名姓氏名字組成摔笤,其中任一一個(gè)改變都會(huì)影響全名的改變;其實(shí)下載進(jìn)度也跟這個(gè)類(lèi)似垦写,下載量和總量的任一改變都會(huì)改變下載進(jìn)度吕世。
  • to-many relationships: 一對(duì)多關(guān)系,在KVO中不支持多對(duì)多的關(guān)系鍵值路徑梯投。一對(duì)多關(guān)系主要是集合對(duì)象屬性命辖,通常就是NSArray或者NSSet等,但是涉及改變就是他們的可變類(lèi)型分蓖。比如有一個(gè)部門(mén)尔艇,有一個(gè)員工數(shù)組,員工薪資屬性么鹤,部門(mén)有個(gè)總工資屬性终娃,我想監(jiān)聽(tīng)總工資的變化,其實(shí)就是監(jiān)聽(tīng)員工數(shù)組的改變蒸甜,再其改變后可以通過(guò)KVC的數(shù)組操作符計(jì)算總工資的變化棠耕,然后手動(dòng)調(diào)用willchange去觸發(fā)總工資改變的監(jiān)聽(tīng)

對(duì)于一對(duì)一和一對(duì)多的關(guān)系可以查看蘋(píng)果官方文檔,進(jìn)一步了解和示例代碼的查看柠新。

1.2 KVO 的三個(gè)步驟

example.jpg

舉個(gè)例子窍荧,如上圖所示Person對(duì)象有個(gè)Account屬性,而Account對(duì)象又有balanceinterestRate兩個(gè)屬性『拊鳎現(xiàn)在我們想實(shí)現(xiàn)一個(gè)功能:當(dāng)余額和利率變化的時(shí)候需要通知到用戶蕊退,其實(shí)用戶可以通過(guò)輪詢的方式定期去查詢Account對(duì)象中的balanceinterestRate,但是這種方式不僅不及時(shí)而且效率低,消耗大咕痛,更好的方式是使用KVO痢甘,使Person對(duì)象像收到通知一樣能及時(shí)的知道余額和利率的變動(dòng)。

另外要實(shí)現(xiàn)KVO的前提是被觀察對(duì)象時(shí)符合KVO機(jī)制的茉贡,一般來(lái)說(shuō)塞栅,繼承于NSObject根類(lèi)的對(duì)象及其屬性都自動(dòng)符合KVO機(jī)制。當(dāng)然我們也可以自己去實(shí)現(xiàn)腔丧,使其同樣符合KVO機(jī)制放椰,這就是Manual Change Notification(手動(dòng)變更通知),所以KVO包含Automatic Change Notification(自動(dòng)變更通知)和Manual Change Notification(手動(dòng)變更通知)兩種機(jī)制愉粤。

KVO合規(guī)性官方文檔

  • 首先是注冊(cè)觀察者
注冊(cè)觀察者.jpg

將觀察者實(shí)例Person與觀察實(shí)例Account注冊(cè)在一起砾医。Person對(duì)每個(gè)觀察到的鍵路徑向Account發(fā)送一個(gè)addObserver:forKeyPath:options:context:消息,將自己命名為觀察者衣厘。這里observer(監(jiān)聽(tīng)者)如蚜、keyPath(被監(jiān)聽(tīng)者)、options(監(jiān)聽(tīng)策略)影暴、context(上下文)错邦。

  • 被觀察者觸發(fā)回調(diào)
被觀察者觸發(fā)回調(diào).jpg

為了接收Account的變更通知,Person需要實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法型宙。Account將在任何改變的時(shí)候想Person發(fā)送該消息撬呢,Person可以根據(jù)通知做出相應(yīng)的措施。

  • 移除觀察
移除觀察.jpg

最后妆兑,當(dāng)不需要監(jiān)聽(tīng)的時(shí)候就可以通過(guò)removeObserver:forKeyPath:方法移除監(jiān)聽(tīng)魂拦,但是移除必須在監(jiān)聽(tīng)者對(duì)象銷(xiāo)毀前執(zhí)行。

1.3 KVO三個(gè)方法解析

1.3.1 注冊(cè)觀察者

- (void)addObserver:(NSObject *)observer
                     forKeyPath:(NSString *)keyPath 
                     options:(NSKeyValueObservingOptions)options 
                     context:(nullable void *)context;
  • observer: 觀察者搁嗓,一般都是self
  • keyPath: 被觀察者的屬性
  • options: NSKeyValueObservingOptions的組合芯勘,它指定觀察通知中會(huì)回調(diào)什么值
  • context: 上下文,這里是一個(gè)nullable void *類(lèi)型的參數(shù)腺逛,我們通常會(huì)傳nil借尿,其實(shí)應(yīng)該傳NULL,官方文檔也說(shuō)應(yīng)該傳NULL屉来。其實(shí)這里我們可以傳一個(gè)void *類(lèi)型的指針路翻,用來(lái)區(qū)分相同path的不同對(duì)象的觀察。傳值示例:static void *PersonNameContext = &PersonNameContext;

NSKeyValueObservingOptions:的四個(gè)枚舉值

  1. NSKeyValueObservingOptionNew: 表明通知中的更改字典應(yīng)該提供新的屬性值茄靠,如果有的話茂契。
  2. NSKeyValueObservingOptionOld: 表明通知中的更改字典應(yīng)該包含舊的屬性值,如果有的話慨绳。
  3. NSKeyValueObservingOptionInitial: 在屬性發(fā)生變化后立即通知觀察者掉冶,這個(gè)過(guò)程甚至早于觀察者注冊(cè)是時(shí)候真竖。如果在注冊(cè)的時(shí)候配置了 NSKeyValueObservingOptionNew,那么在通知的更改字典中也會(huì)包含 NSKeyValueChangeNewKey厌小,但是不會(huì)包括 NSKeyValueChangeOldKey恢共。(在初始通知中,觀察到的屬性值可能是舊的璧亚,但是對(duì)于觀察者來(lái)說(shuō)是新的)其實(shí)簡(jiǎn)單來(lái)說(shuō)就是這個(gè)枚舉值會(huì)在屬性變化前先觸發(fā)一次 observeValueForKeyPath 回調(diào)讨韭。
  4. NSKeyValueObservingOptionPrior: 這個(gè)會(huì)先后連續(xù)出發(fā)兩次 observeValueForKeyPath 回調(diào)。同時(shí)在回調(diào)中的可變字典中會(huì)有一個(gè)布爾值的 key - notificationIsPrior 來(lái)標(biāo)識(shí)屬性值是變化前還是變化后的癣蟋。如果是變化后的回調(diào)透硝,那么可變字典中就只有 new 的值了,如果同時(shí)制定了 NSKeyValueObservingOptionNew 的話疯搅。如果你需要啟動(dòng)手動(dòng) KVO 的話濒生,你可以指定這個(gè)枚舉值然后通過(guò) willChange 實(shí)例方法來(lái)觀察屬性值。在出發(fā) observeValueForKeyPath 回調(diào)后再去調(diào)用 willChange 可能就太晚了幔欧。

下面我們來(lái)驗(yàn)證一下NSKeyValueObservingOptions幾個(gè)key會(huì)有什么樣的結(jié)果罪治。

初始實(shí)現(xiàn)代碼:

static void *PersonNameContext = &PersonNameContext;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person  = [LGPerson new];
    self.person.name = @"nameA";
    
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
  
    if (context == PersonNameContext) {
        NSLog(@"person name change %@ - %@",self, change);
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name  = @"nameB";
}

NSKeyValueObservingOptionNew:

new.jpg

NSKeyValueObservingOptionOld:

old.jpg

NSKeyValueObservingOptionInitial:

Initial.jpg

NSKeyValueObservingOptionInitial會(huì)觸發(fā)兩次回調(diào),第一次是在屬性改變前礁蔗,第二次是在屬性改變后觉义。但是并沒(méi)有返回任何的舊值和新值。其實(shí)第一次是在我們調(diào)用addObserver:forKeyPath:后就打印了的瘦麸。這與name是否賦初始值沒(méi)有關(guān)系谁撼,有沒(méi)有初值都會(huì)打印歧胁。

Initial | New | Old:

Initial | New | Old.jpg

此時(shí)還是觸發(fā)了兩次回調(diào)滋饲,只不過(guò)第一次返回的新值其實(shí)就是舊值,就是我們初始化時(shí)的值喊巍,第二次返回即包含了新值屠缭,也包含了舊值。其實(shí)我們包含new在第二次就會(huì)返回新值崭参,包含old就會(huì)返回舊值呵曹,如果不包含就不會(huì)返回。如果不包含new第一次就不會(huì)返回新值何暮。

NSKeyValueObservingOptionPrior:

Prior.jpg

這是也是觸發(fā)了兩次回調(diào)奄喂,不過(guò)這兩次回調(diào)是在值改變后觸發(fā)的,并且第一次多返回了一個(gè)notificationIsPrior值海洼。

Prior | New | Old:

Prior | New | Old.jpg

此時(shí)還是觸發(fā)了兩次回調(diào)跨新,同樣在第一次回調(diào)中包含notificationIsPrior值。并且第一次回調(diào)中多了舊值坏逢,第二次回調(diào)中即包含舊值也包含新值域帐。同樣我們包含new在第二次就會(huì)返回新值赘被,包含old就會(huì)返回舊值,如果不包含就不會(huì)返回肖揣。如果不包含old第一次就不會(huì)返回舊值民假。

1.3.2 觀察者接收通知

- (void)observeValueForKeyPath:(nullable NSString *)keyPath 
                    ofObject:(nullable id)object 
                    change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change 
                    context:(nullable void *)context;

除了change其他參數(shù)跟上面注冊(cè)觀察時(shí)的相同。

change包含五個(gè)key龙优,如下:

key value 描述
NSKeyValueChangeKindKey NSNumber類(lèi)型 1:Setting羊异,2:Insertion,3:Removal,4:Replacement
NSKeyValueChangeNewKey id 變化后的新值
NSKeyValueChangeOldKey id 變化后的舊值
NSKeyValueChangeIndexesKey NSIndexSet 插入、刪除或替換的對(duì)象的索引
NSKeyValueChangeNotificationIsPriorKey NSNumber boolValue Option為Prior時(shí)標(biāo)識(shí)屬性值是變化前和還是變化后的

NSKeyValueChangeKindKey對(duì)應(yīng)的枚舉:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

1.3.3 移除觀察

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

移除和注冊(cè)時(shí)一一對(duì)應(yīng)的陋率,有兩個(gè)方法一個(gè)是帶context參數(shù)的球化,另一個(gè)是不帶的。在觀察者生命周期結(jié)束前瓦糟,一定要移除觀察筒愚,如果沒(méi)有移除,KVO機(jī)制會(huì)給一個(gè)不存在的對(duì)象發(fā)送變化回調(diào)消息導(dǎo)致野指針錯(cuò)誤菩浙。另外也不能重復(fù)移除注冊(cè)巢掺,重復(fù)移除會(huì)導(dǎo)致crash,當(dāng)然為了避免crash我們可以把移除放在@try里面去執(zhí)行劲蜻。

1.4 自動(dòng)觀察與手動(dòng)觀察

默認(rèn)情況下陆淀,我們只需要按照上面的步驟就可以實(shí)現(xiàn)屬性的觀察,其實(shí)這是由系統(tǒng)完全控制的先嬉,屬于自動(dòng)觀察轧苫。其實(shí)KVO還給我們提供了手動(dòng)觀察的選項(xiàng)。

如果我們想要開(kāi)啟手動(dòng)觀察就要通過(guò)重寫(xiě)類(lèi)方法+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key疫蔓,如果返回YES就是自動(dòng)觀察含懊,返回NO就是手動(dòng)觀察,根據(jù)方法的我們還可以判斷key值對(duì)不同的key分別實(shí)現(xiàn)自動(dòng)觀察手動(dòng)觀察衅胀。

// 自動(dòng)開(kāi)關(guān)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"balance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

對(duì)于需要手動(dòng)觀察key在改變前需要調(diào)用willChangeValueForKey方法岔乔,在改變后需要調(diào)用didChangeValueForKey方法,如果不調(diào)用滚躯,就不會(huì)觸發(fā)KVO的監(jiān)聽(tīng)雏门。

示例代碼:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    _balance = theBalance;
    [self didChangeValueForKey:@"balance"];
}
  • 我們可以通過(guò)提前檢查是否已更改來(lái)最大程度的減少發(fā)送不必要的通知:

官方示例:

- (void)setBalance:(double)theBalance {
    if (theBalance != _balance) {
        [self willChangeValueForKey:@"balance"];
        _balance = theBalance;
        [self didChangeValueForKey:@"balance"];
    }
}
  • 如果單個(gè)操作導(dǎo)致更改多個(gè)鍵,則必須嵌套更改通知

官方示例:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}
  • 對(duì)于有序的一對(duì)多關(guān)系掸掏,不僅必須指定已更改的鍵茁影,還必須指定更改的類(lèi)型和所涉及對(duì)象的索引。
    • 改變的類(lèi)型的鍵值是一個(gè) NSKeyValueChange 類(lèi)型的枚舉值丧凤,有三個(gè)分別是:NSKeyValueChangeInsertion募闲、NSKeyValueChangeRemovalNSKeyValueChangeReplacement
    • 受影響對(duì)象的索引作為NSIndexSet對(duì)象傳遞息裸。

示例代碼:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

1.5 Registering Dependent Keys(注冊(cè)從屬關(guān)系的鍵值)

在許多情況下蝇更,一個(gè)屬性的值取決于另一對(duì)象中一個(gè)或多個(gè)其他屬性的值沪编。如果一個(gè)屬性的值發(fā)生更改,則派生屬性的值也應(yīng)標(biāo)記為更改年扩。如何確保為這些從屬屬性發(fā)布鍵值觀察通知取決于關(guān)系的基數(shù)蚁廓。 這個(gè)在上面已經(jīng)有所提到,這里在通過(guò)舉例進(jìn)行詳細(xì)的說(shuō)明厨幻。

1.5.1 一對(duì)一關(guān)系

要自動(dòng)觸發(fā)一對(duì)一關(guān)系的通知相嵌,您應(yīng)該重寫(xiě) keyPathsForValuesAffectingValueForKey:或?qū)崿F(xiàn)遵循其定義的用于注冊(cè)從屬鍵的模式的合適方法。

例如况脆,一個(gè)人的全名取決于名字和姓氏饭宾。返回全名的方法可以編寫(xiě)如下:

- (NSString *)fullName {
    return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}

fullName當(dāng)firstName或lastName屬性更改時(shí),必須通知觀察該屬性的應(yīng)用程序格了,因?yàn)樗鼈儠?huì)影響屬性的值看铆。

第一種方法是我們通過(guò)重寫(xiě)keyPathsForValuesAffectingValueForKey:指定fullName的屬性取決于lastNamefirstName屬性。通常我們應(yīng)該調(diào)用super并返回一個(gè)集合盛末,該集合包括這樣做所導(dǎo)致的集合中的其他任何成員免受干擾弹惦。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

第二種方法是通過(guò)實(shí)現(xiàn)遵循命名約定的類(lèi)方法keyPathsForValuesAffecting<Key>來(lái)實(shí)現(xiàn)相同的結(jié)果,其中<Key>是依賴值的屬性名稱(首字母大寫(xiě))悄但。實(shí)現(xiàn)代碼如下:

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}

對(duì)于分類(lèi)我們只能以第二種方法進(jìn)行實(shí)現(xiàn)因?yàn)槲覀儾荒茉俜诸?lèi)中覆蓋keyPathsForValuesAffectingValueForKey:的實(shí)現(xiàn)棠隐。

1.5.2 一對(duì)多關(guān)系

keyPathsForValuesAffectingValueForKey:方法不支持包含多對(duì)多關(guān)系的鍵路徑。那么對(duì)于這種關(guān)系的鍵值路徑我們?cè)撊绾翁幚砟兀?/p>

例如我們有個(gè)Department(部門(mén))檐嚣,他又一個(gè)employees(員工數(shù)組)對(duì)象助泽,部門(mén)跟員工有很多關(guān)系,但是Employee(員工)具有salary(薪資)屬性嚎京,這時(shí)我們希望部門(mén)有個(gè)totalSalary(總工資)屬性嗡贺,那么這個(gè)屬性取決于員工數(shù)組中所有員工的薪資,我們也不能使用keyPathsForValuesAffectingTotalSalaryemployees.salary作為鍵返回挖藏。

此時(shí)我們可以使用鍵值觀察將父項(xiàng)(在此示例中為Department)注冊(cè)為所有子項(xiàng)(在此示例中為employees)的相關(guān)屬性的觀察者暑刃。您必須作為觀察者添加和刪除父對(duì)象厢漩,因?yàn)橐陉P(guān)系中添加或刪除子對(duì)象膜眠。在該observeValueForKeyPath:ofObject:change:context:方法中,您將響應(yīng)更改來(lái)更新從屬值溜嗜,如以下代碼片段所示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

另外如果您使用的是Core Data宵膨,則可以將父項(xiàng)注冊(cè)到應(yīng)用程序的通知中心,作為其托管對(duì)象上下文的觀察者炸宵。父母應(yīng)以類(lèi)似于觀察鍵值的方式響應(yīng)孩子發(fā)布的相關(guān)變更通知辟躏。

2. KVO 底層原理探索

由于KVO的實(shí)現(xiàn)并沒(méi)有開(kāi)源,我們首先看看官方文檔是怎么說(shuō)的:

Automatic key-value observing is implemented using a technique called isa-swizzling. 【譯:】自動(dòng)鍵值觀察使用的是一種叫做isa-swizzling的技術(shù)土全。

The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.【譯:】isa指針捎琐,顧名思義会涎,指向的是對(duì)象所屬的類(lèi),這個(gè)類(lèi)維護(hù)了一個(gè)哈希表瑞凑,這個(gè)哈希表實(shí)質(zhì)上包含指向該類(lèi)實(shí)現(xiàn)的方法的指針以及其他數(shù)據(jù)末秃。

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.【譯:】在位對(duì)象的屬性注冊(cè)觀察者時(shí),將修改觀察對(duì)象的isa指針籽御,指向中間類(lèi)而不是真實(shí)的類(lèi)练慕,因?yàn)?code>isa的值不一定反映的是實(shí)例的實(shí)際的類(lèi)。

You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.【譯:】所以我們永遠(yuǎn)不要依靠isa指針來(lái)確定類(lèi)成員技掏,所以我們應(yīng)該使用class方法確定對(duì)象實(shí)例的類(lèi)铃将。

2.1 中間類(lèi)(派生類(lèi))

根據(jù)官方文檔的內(nèi)容我們可以知道,在KVO的底層實(shí)現(xiàn)中會(huì)生成一個(gè)中間類(lèi)哑梳,此時(shí)我們實(shí)例對(duì)象的isa就指向了這個(gè)中間類(lèi)劲阎,那么我們就來(lái)驗(yàn)證一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    NSLog(@"注冊(cè)KVO前%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"注冊(cè)KVO后%@---%s", NSStringFromClass([self.person class]), object_getClassName(self.person));
}

打印結(jié)果如下:

打印結(jié)果.jpg
打印對(duì)象的isa.jpg

通過(guò)打印結(jié)果可以看出在注冊(cè)KVO觀察后通過(guò)Objective-C方法打印的的類(lèi)名仍然是LGPerson,但是在注冊(cè)后通過(guò)Runtime API打印的確有不同了鸠真,所以說(shuō)Objective-C方法對(duì)class方法進(jìn)行了封裝哪工,讓我們?cè)陂_(kāi)發(fā)過(guò)程中對(duì)中間類(lèi)無(wú)感知,但是底層確實(shí)是實(shí)現(xiàn)了一個(gè)中間類(lèi)就是NSKVONotifying_xxx弧哎。其實(shí)我們也可以通過(guò)打印對(duì)象的isa來(lái)驗(yàn)證雁比,至此我們就驗(yàn)證了官方文檔所說(shuō)的內(nèi)容。

那么這個(gè)中間類(lèi)跟我們的類(lèi)有什么關(guān)系呢撤嫩?我們不妨打印一下類(lèi)和它的子類(lèi)來(lái)看看偎捎。

打印類(lèi)實(shí)現(xiàn)代碼:

NSLog(@"注冊(cè)KVO前");
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
NSLog(@"注冊(cè)KVO后");
[self printClasses:[LGPerson class]];
- (void)printClasses:(Class)cls{
    
    /// 注冊(cè)類(lèi)的總數(shù)
    int count = objc_getClassList(NULL, 0);
    /// 創(chuàng)建一個(gè)數(shù)組, 其中包含給定對(duì)象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    /// 獲取所有已注冊(cè)的類(lèi)
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}
打印結(jié)果.jpg

可以看到在LGPerson的子類(lèi)中有這個(gè)中間類(lèi)序攘,所以說(shuō)這個(gè)中間類(lèi)是類(lèi)的子類(lèi)茴她。

2.2 KVO 觀察

我們知道KVO是觀察屬性的變化,那么屬性的本質(zhì)是成員變量+getter+setter程奠,getter是取值的丈牢,并不會(huì)修改值,值的變化發(fā)生在setter和給成員變量賦值兩種情況瞄沙。那么我們分別測(cè)試一下這兩種情況哪一種會(huì)觸發(fā)KVO的觀察己沛。

聲明代碼:

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end

驗(yàn)證代碼和結(jié)果:

驗(yàn)證代碼和結(jié)果.jpg

通過(guò)上圖我們可以看到直接給實(shí)例變量賦值并不會(huì)觸發(fā)KVO的監(jiān)聽(tīng),但是直接給屬性賦值就觸發(fā)了KVO的監(jiān)聽(tīng)距境,其實(shí)給屬性賦值就是調(diào)用setter方法申尼,所以說(shuō)KVO底層是觀察的setter方法。

2.3 中間類(lèi)都有哪些方法

我們分別打印原始類(lèi)和中間類(lèi)中的方法進(jìn)行查看:

實(shí)現(xiàn)代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.person = [[LGPerson alloc] init];
    
    NSLog(@"原始類(lèi)中的方法");
    [self printClassAllMethod:[LGPerson class]];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"派生類(lèi)中的方法");
    [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
}

printClassAllMethod 代碼:

- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

打印結(jié)果:

查看方法.jpg

我們可以看到中間類(lèi)中有屬性的setter方法垫桂,class方法师幕,dealloc方法以及_isKVOA方法。這里的setter方法是重寫(xiě)了原始類(lèi)的方法诬滩,其余的都是重寫(xiě)的NSObject方法霹粥。

  • 對(duì)于重寫(xiě)setter應(yīng)該是在setter方法中觸發(fā)監(jiān)聽(tīng)回調(diào)灭将,已經(jīng)給原始類(lèi)中屬性賦值
  • 對(duì)于重寫(xiě)class,這里也就驗(yàn)證了我們?cè)谏厦娲蛴?code>class時(shí)為什么都是原始類(lèi)的名稱后控。這樣是為了隱藏中間類(lèi)的存在宗侦,讓開(kāi)發(fā)者在使用過(guò)程中保持一致性。
  • 對(duì)于重寫(xiě)dealloc應(yīng)該是移除監(jiān)聽(tīng)時(shí)需要處理一些邏輯
  • 對(duì)于重寫(xiě)_isKVOA方法應(yīng)該是返回是否是KVO的值

2.3 isa 何時(shí)指回原始類(lèi)

其實(shí)這很容易想到忆蚀,當(dāng)我們移除所有觀察后就意味著我們不需要觀察了矾利,此時(shí)在指向中間類(lèi)也就沒(méi)什么意義了。下面我們進(jìn)行驗(yàn)證馋袜。

驗(yàn)證代碼:

- (void)dealloc{
    NSLog(@"移除觀察前%@",object_getClass(self.person));
    [self.person removeObserver:self forKeyPath:@"nickName"];
    NSLog(@"移除觀察后%@",object_getClass(self.person));
    [self printClasses:[LGPerson class]];
}

打印結(jié)果:

代碼打印.jpg
lldb驗(yàn)證.jpg

我們通過(guò)代碼和lldb進(jìn)行了驗(yàn)證在移除觀察后isa即指回了原始的類(lèi)男旗。另外我們也驗(yàn)證了指回后是否銷(xiāo)毀中間類(lèi),顯然中間類(lèi)并沒(méi)有被銷(xiāo)毀欣鳖。其實(shí)這也很正常察皇,因?yàn)閯?chuàng)建一個(gè)類(lèi)還是非常耗費(fèi)性能的,雖然移除了觀察泽台,但是也不能保證不再重新開(kāi)始觀察什荣,既然創(chuàng)建了就讓它留著吧,如果下次繼續(xù)開(kāi)始監(jiān)聽(tīng)就不用重新創(chuàng)建了怀酷,也就提高了性能稻爬。

3. 自定義KVO

至此我們就基本分析完畢了KVO,那么我們可以自己來(lái)實(shí)現(xiàn)以下蜕依。

擱置了NΤ!样眠!

4.總結(jié)

  1. KVO是蘋(píng)果提供給開(kāi)發(fā)者的一套鍵值觀察的API
  2. KVO由注冊(cè)觀察者友瘤,監(jiān)聽(tīng)通知,移除觀察三個(gè)步驟組成
  3. 有自動(dòng)觀察和手動(dòng)觀察兩種模式
  4. 對(duì)于可變集合需要通過(guò)mutableXXXValueForKey的相關(guān)方法觸發(fā)更改
  5. 我們還可以注冊(cè)從屬關(guān)系的鍵值觀察檐束,KVO支持一對(duì)一和一對(duì)多兩種
  6. KVO本質(zhì)是isa-swizzling技術(shù)辫秧,通過(guò)生成中間類(lèi)(派生類(lèi))來(lái)實(shí)現(xiàn)屬性的觀察
  7. 中間類(lèi)會(huì)重寫(xiě)屬性的setter方法以及重寫(xiě)class方法,dealloc方法和_isKVOA方法
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末被丧,一起剝皮案震驚了整個(gè)濱河市盟戏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晚碾,老刑警劉巖抓半,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喂急,死亡現(xiàn)場(chǎng)離奇詭異格嘁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)廊移,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)糕簿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)探入,“玉大人,你說(shuō)我怎么就攤上這事懂诗》渌裕” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵殃恒,是天一觀的道長(zhǎng)植旧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)离唐,這世上最難降的妖魔是什么病附? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮亥鬓,結(jié)果婚禮上完沪,老公的妹妹穿的比我還像新娘。我一直安慰自己嵌戈,他們只是感情好覆积,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著熟呛,像睡著了一般宽档。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庵朝,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天雌贱,我揣著相機(jī)與錄音,去河邊找鬼偿短。 笑死欣孤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昔逗。 我是一名探鬼主播降传,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼勾怒!你這毒婦竟也來(lái)了婆排?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笔链,失蹤者是張志新(化名)和其女友劉穎段只,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鉴扫,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赞枕,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炕婶。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡姐赡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柠掂,到底是詐尸還是另有隱情项滑,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布涯贞,位于F島的核電站枪狂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宋渔。R本人自食惡果不足惜摘完,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望傻谁。 院中可真熱鬧孝治,春花似錦、人聲如沸审磁。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)态蒂。三九已至杭措,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钾恢,已是汗流浹背手素。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘩蚪,地道東北人泉懦。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疹瘦,于是被迫代替她去往敵國(guó)和親崩哩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355