KVO的實(shí)現(xiàn)原理

1买优、KVO簡(jiǎn)介

KVOKey-Value Observing讹开,翻譯成是中文鍵值觀察丸卷,是一種非正式的協(xié)議枕稀,它定義了對(duì)象之間觀察和通知狀態(tài)改變的機(jī)制,是觀察者模式的一種衍生。KVO通過(guò)對(duì)對(duì)象的某個(gè)屬性添加注冊(cè)觀察萎坷,當(dāng)該屬性的值發(fā)送變化時(shí)凹联,會(huì)觸發(fā)觀察者對(duì)象實(shí)現(xiàn)的KVO接口方法,自動(dòng)通知觀察者哆档。簡(jiǎn)單來(lái)說(shuō)KVO就是通過(guò)監(jiān)聽(tīng)key來(lái)獲取所對(duì)應(yīng)的的value的變化蔽挠,從而達(dá)到對(duì)象狀態(tài)變化的監(jiān)聽(tīng)。和KVC一樣KVO的定義也是對(duì)NSObject的擴(kuò)展來(lái)實(shí)現(xiàn)的瓜浸,Objective-C中有個(gè)顯式的NSKeyValueObserving類別名澳淑,所以對(duì)于所有派生于NSObject的類的對(duì)象,都能使用KVO插佛。

2杠巡、KVO的基礎(chǔ)使用

2.1、注冊(cè)觀察者

根據(jù)KVO的定義朗涩,KVO是對(duì)對(duì)象的屬性狀態(tài)變化的監(jiān)聽(tīng)忽孽,那么首先要對(duì)該對(duì)象(被觀察者)進(jìn)行注冊(cè)觀察者。
注冊(cè)觀察者的方法如下:

- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

這個(gè)方法中的四個(gè)參數(shù)解釋如下:

  • observer:注冊(cè)KVO通知的對(duì)象谢床,也就是觀察者兄一。觀察者必須實(shí)現(xiàn)
    observeValueForKeyPath:ofObject:change:context:
  • keyPath:觀察者的屬性的 keypath识腿,相對(duì)于接受者出革,值不能是 nil
  • options:KVO的一些屬性配置渡讼;有四個(gè)選項(xiàng)骂束。
  • context: 上下文,這個(gè)會(huì)傳遞到訂閱著的函數(shù)中成箫,用來(lái)區(qū)分消息展箱,所以應(yīng)當(dāng)是不同的。

其實(shí)前兩個(gè)參數(shù)比較好理解蹬昌,需要特別說(shuō)明的是后面兩個(gè)參數(shù)混驰。

2.1.1、options參數(shù)

options參數(shù)是NSKeyValueObservingOptions類型皂贩,是一個(gè)枚舉類型栖榨,其定義如下:

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew = 0x01,
    NSKeyValueObservingOptionOld = 0x02,
    NSKeyValueObservingOptionInitial = 0x04,
    NSKeyValueObservingOptionPrior = 0x08
};

這個(gè)四個(gè)枚舉變量的含義如下:

  • NSKeyValueObservingOptionNew: 表明變化的change字典應(yīng)該提供新的屬性值
  • NSKeyValueObservingOptionOld: 表明變化的字典應(yīng)該包含舊的屬性值明刷。
  • NSKeyValueObservingOptionInitial:是否應(yīng)在觀察者注冊(cè)方法返回之前立即將通知發(fā)送給觀察者婴栽。如果NSKeyValueObservingOptionNew也被指定,則通知中的change字典將始終包含一個(gè)NSKeyValueChangeNewKey辈末,但絕不會(huì)包含一個(gè)NSKeyValueChangeOldKey愚争。(在初始通知中映皆,觀察到的屬性的當(dāng)前值可能是舊的,但是對(duì)于觀察者來(lái)說(shuō)是新的准脂。)劫扒。
  • NSKeyValueObservingOptionPrior:是否應(yīng)該在每次更改之前和之后向觀察者發(fā)送單獨(dú)的通知,而不是在更改之后發(fā)送單個(gè)通知狸膏。在更改之前發(fā)送的通知中的change字典中會(huì)包含有一個(gè)notificationIsPrior項(xiàng)沟饥,用以區(qū)分是在更改前發(fā)送的通知,但不會(huì)包含有NSKeyValueChangeNewKey湾戳,即使是NSKeyValueObservingOptionNew被指定贤旷。

看如下例子:

在注冊(cè)觀察者的時(shí)候options的參數(shù)傳入的是NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial,這個(gè)時(shí)候在進(jìn)入到頁(yè)面的時(shí)砾脑,打印的結(jié)果中有一個(gè)new項(xiàng)幼驶,其實(shí)這個(gè)時(shí)候的name的只是舊的屬性值。在點(diǎn)擊屏幕觸發(fā)修改name屬性后打印的結(jié)果中的new的結(jié)果是新的屬性值韧衣。


正如上圖所示盅藻,如果options參數(shù)傳入的是NSKeyValueObservingOptionPrior,則會(huì)在更改前后各發(fā)一次通知畅铭,不管是否有傳入NSKeyValueObservingOptionNew氏淑,在更改前的通知中的change字典中都不會(huì)包含有NSKeyValueChangeNewKey項(xiàng)。

2.1.1硕噩、context參數(shù)

context指針在addObserver:forKeyPath:options:context: message中包含任意的數(shù)據(jù)假残,這些數(shù)據(jù)將在相應(yīng)的變更通知中被傳遞回觀察者。您可以指定NULL并完全依賴于keyPath來(lái)確定更改通知的來(lái)源炉擅,但是這種方法可能會(huì)對(duì)一個(gè)對(duì)象造成問(wèn)題辉懒,因?yàn)樵搶?duì)象的超類由于不同的原因也在觀察相同的keyPath。一種更安全谍失、更可擴(kuò)展的方法是使用context來(lái)確保接收到的通知是針對(duì)觀察者的眶俩,而不是超類。類中唯一命名的靜態(tài)變量的地址是一個(gè)很好的context快鱼。在超類或子類中以類似方式選擇的context不太可能重疊仿便。可以為整個(gè)類選擇一個(gè)context,并依賴于通知消息中的keyPath來(lái)確定更改了什么攒巍。或者荒勇,也可以為每個(gè)觀察到的keyPath創(chuàng)建不同的上下文柒莉,從而完全繞過(guò)字符串比較的需要,從而提高通知解析的效率沽翔。

正如下面所示的那樣是為Person類和Student類的name屬性創(chuàng)建的context兢孝。這樣只需要在接收通知的時(shí)候判斷context邊可以分辨出是哪個(gè)對(duì)象的屬性發(fā)生了改變窿凤。

 static void * PersonNameContext = &PersonNameContext;
 static void * StudentNameContext = &StudentNameContext;

2.2、觀察者接收消息

觀察者接收消息的方法如下:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context

在這個(gè)方法的里面除了change外的其他幾個(gè)參數(shù)都是在添加注冊(cè)觀察者的時(shí)候傳入的參數(shù)原樣帶回跨蟹,在這里可以通過(guò)keyPath來(lái)匹配確認(rèn)改變的屬性雳殊,也可以通過(guò)object和keyPath相結(jié)合的方式來(lái)區(qū)分確認(rèn)是哪個(gè)對(duì)象的那個(gè)屬性發(fā)生了修改,但是這樣不免在代碼的合理性和優(yōu)雅上大打折扣了窗轩,所以最好的方式是通過(guò)context來(lái)區(qū)分夯秃。change這個(gè)字典保存了變更的信息,其內(nèi)容和你在添加注冊(cè)的時(shí)候傳入的options參數(shù)有關(guān)痢艺。

2.3仓洼、手動(dòng)觀察

按照上面章節(jié)所講可以實(shí)現(xiàn)對(duì)對(duì)象屬性的監(jiān)聽(tīng),那是因?yàn)閷傩灾档淖兓上到y(tǒng)控制的堤舒,開(kāi)發(fā)者只需要告訴系統(tǒng)監(jiān)聽(tīng)什么屬性便可以了色建,但是在實(shí)際的開(kāi)發(fā)中我們有可能屬性的值的變化并不需要受系統(tǒng)的支配。實(shí)際上除了系統(tǒng)自動(dòng)監(jiān)聽(tīng)屬性值的變化外舌缤,還有一種方式便是可以由開(kāi)發(fā)者支配屬性的值變化后是否發(fā)送通知箕戳。只需要修改類方法 automaticallyNotifiesObserversForKey:的返回值,如果返回 YES 就是自動(dòng)国撵,返回 NO 就是手動(dòng)陵吸。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

一旦automaticallyNotifiesObserversForKey方法的返回的NO,系統(tǒng)就不在自動(dòng)監(jiān)控屬性的值變化卸留,如果想要還能監(jiān)控到屬性的值變化走越,那么還需要調(diào)用兩個(gè)方法:

- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key

只需要在屬性值修改的前后分別調(diào)用這兩個(gè)方法便可。

- (void)setName:(NSString *)name{
    [self willChangeValueForKey:@"name"];
    _name = name;
    [self didChangeValueForKey:@"name"];
}

需要注意的是對(duì)于一個(gè)容器類的屬性耻瑟,不僅必須指定已更改的key旨指,還必須指定更改的類型和所涉及對(duì)象的索引。 更改的類型是 NSKeyValueChange喳整,它指定 NSKeyValueChangeInsertion谆构,NSKeyValueChangeRemoval 或 NSKeyValueChangeReplacement,受影響的對(duì)象的索引作為 NSIndexSet 對(duì)象傳遞:

- (void)removeObjectFromMArrayAtIndex:(NSUInteger)index {
    [self willChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"mArray"];
    [self.mArray removeObjectAtIndex:index];
    [self didChange:NSKeyValueChangeRemoval valuesAtIndexes:[NSIndexSet indexSetWithIndex:index] forKey:@"mArray"];
}

2.4框都、依賴鍵

有時(shí)候一個(gè)屬性的值依賴于另一對(duì)象中的一個(gè)或多個(gè)屬性搬素,如果這些屬性中任一屬性的值發(fā)生變更,被依賴的屬性值也應(yīng)當(dāng)為其變更進(jìn)行標(biāo)記魏保。因此熬尺,object 引入了依賴鍵。

2.4.1谓罗、一對(duì)一關(guān)系

一對(duì)一的這種依賴關(guān)系實(shí)現(xiàn)自動(dòng)的KVO有兩種方式粱哼,一種是重寫(xiě)keyPathsForValuesAffectingValueForKey 方法,一種是實(shí)現(xiàn)一個(gè)合適的方法檩咱。
比如說(shuō)fullName這個(gè)屬性依賴于firstNamelastName揭措,只要是兩者中任一修改都會(huì)影響到fullName胯舷,那么就可以在每次監(jiān)聽(tīng)到兩者中任一變化后對(duì)fullName進(jìn)行值修改便可以達(dá)到目的。

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

但是這樣的實(shí)現(xiàn)不免有些麻煩绊含,重寫(xiě)keyPathsForValuesAffectingValueForKey方法桑嘶,使得fullNam的監(jiān)聽(tīng)和firstName、lastName相關(guān)聯(lián)躬充,這樣的方式會(huì)更加簡(jiǎn)單逃顶。

+ (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)類方法 keyPathsForValuesAffectingValueForKey 來(lái)返回一個(gè)集合麻裳,這樣就實(shí)現(xiàn)了fullName和firstName口蝠、lastName的聯(lián)動(dòng)。
實(shí)際上還有一個(gè)便利的方法津坑,就是 keyPathsForValuesAffecting<Key>妙蔗,Key 是屬性的名稱(需要首字母大寫(xiě))。這個(gè)方法的效果和 keyPathsForValuesAffectingValueForKey 是一樣的疆瑰,但針對(duì)的某個(gè)具體屬性眉反。

+ (NSSet *)keyPathsForValuesAffectingFullName {
    return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
2.4.2、一對(duì)多關(guān)系

keyPathsForValuesAffectingValueForKey方法不支持包含一對(duì)多關(guān)系的Key Path穆役。例如寸五,假設(shè)你有一個(gè)Department對(duì)象,該對(duì)象與Employee 有一對(duì)多關(guān)系(即 employees 屬性)耿币,而 Employee 具有salary 屬性梳杏。 如果需要在Department 對(duì)象上增加totalSalary 屬性,而該屬性取決于關(guān)系中所有Employees的薪水淹接。例如十性,您不能使用keyPathsForValuesAffectingTotalSalary 和返回employees.salary 作為鍵來(lái)執(zhí)行此操作。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    } else{
       [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (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;
}

2.5塑悼、取消注冊(cè)

在合適的地方取消注冊(cè)是一個(gè)必要的過(guò)程劲适,否則會(huì)造成不可預(yù)估的錯(cuò)誤,建議是取消注冊(cè)和添加注冊(cè)是一對(duì)一關(guān)系厢蒜。取消注冊(cè)的兩個(gè)方法如下:

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

2.6霞势、KVO和線程

一個(gè)需要注意的地方是,KVO 行為是同步的斑鸦,并且與所觀察的值發(fā)生變化的同樣的線程上愕贡。沒(méi)有隊(duì)列或者 Run-loop 的處理。所以巷屿,當(dāng)我們?cè)噲D從其他線程改變屬性值的時(shí)候我們應(yīng)當(dāng)十分小心固以,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來(lái)說(shuō)攒庵,我們不推薦把 KVO 和多線程混起來(lái)嘴纺。如果我們要用多個(gè)隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO浓冒。

3栽渴、KVO的實(shí)現(xiàn)原理

在前面的章節(jié)中介紹了KVO的基本是否,但是對(duì)于KVO的實(shí)現(xiàn)原理還沒(méi)有一個(gè)清晰的概念稳懒,好在KVO的官方文檔有對(duì)于KVO的實(shí)現(xiàn)原理有一個(gè)明確的說(shuō)明闲擦。

Automatic key-value observing is implemented using a technique called isa-swizzling.
【譯】使用isa-swizzling技術(shù)實(shí)現(xiàn)了鍵值的自動(dòng)觀察。
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指針指向?qū)ο蟮念?這個(gè)類維護(hù)了一個(gè)調(diào)度表墅冷。這個(gè)調(diào)度表本質(zhì)上包含指向類實(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.
【譯】當(dāng)一個(gè)觀察者為一個(gè)對(duì)象的屬性注冊(cè)時(shí)或油,被觀察對(duì)象的isa指針被修改寞忿,指向一個(gè)中間類而不是真正的類。因此顶岸,isa指針的值不一定反映實(shí)例的實(shí)際類腔彰。

根據(jù)這段話可以得知一個(gè)信息,那就是KVO實(shí)際上是生成了一個(gè)中間類辖佣,并且將被觀察對(duì)象的isa指向這個(gè)中間類霹抛,但是這個(gè)中間類是什么?這個(gè)中間類和真是的類之間是什么關(guān)系卷谈?KVO是怎樣觀察被觀察對(duì)象的屬性變化的杯拐?這個(gè)中間類會(huì)不會(huì)隨著取消注冊(cè)而消亡?帶著這些疑問(wèn)世蔗,我們進(jìn)入到下一步的探索端逼。

3.1、所謂中間類

在上面有提到被觀察對(duì)象的isa是指向這個(gè)中間類的凸郑,那么我們便可以通過(guò)LLDB指令的方式來(lái)查探這個(gè)中間類裳食。

如上圖所示在為注冊(cè)觀察者之前person對(duì)象的isa指向的Person類,在注冊(cè)觀察者之后person對(duì)象的isa指向的是一個(gè)NSKVONotifying_Person類芙沥,那么我們便可以確定這個(gè)生成的中間類是NSKVONotifying_XX的類诲祸。

在這里需要注意的是如果在外部創(chuàng)建了一個(gè)NSKVONotifying_XX的類,KVO將無(wú)法正常工作而昨。

3.2救氯、NSKVONotifying_Person和Person的關(guān)系

我們已經(jīng)知道了生成的中間類是NSKVONotifying_Person,而且這個(gè)和Person類之間一定存在這某種關(guān)系歌憨,那么我們需要知道NSKVONotifying_Person類的繼承鏈關(guān)系着憨。

如上圖所示,我們嘗試著去獲取NSKVONotifying_Person類的父類务嫡,發(fā)現(xiàn)NSKVONotifying_Person類的父類是Person類甲抖,也就是說(shuō)NSKVONotifying_Person從Person繼承而來(lái)漆改。

3.3、如何觀察屬性變化

KVO觀察的是屬性值的變化准谚,那么對(duì)于一個(gè)屬性來(lái)說(shuō)其值的修改其實(shí)是調(diào)用的setter方法或者通過(guò)KVC的方式挫剑。如果對(duì)于屬性和實(shí)例變量同時(shí)監(jiān)聽(tīng)其變化會(huì)怎么樣呢?

如上圖所示同時(shí)對(duì)實(shí)例變量nickName和屬性name進(jìn)行了注冊(cè)觀察者柱衔,發(fā)現(xiàn)只有屬性name能夠接受到變化的通知樊破,而實(shí)例變量監(jiān)控不到變化。但是如果使用KVC的方式來(lái)訪問(wèn)實(shí)例變量便可以監(jiān)控到其值的變化唆铐。

那么屬性而言屬性值變化的監(jiān)聽(tīng)是通過(guò)其setter方法來(lái)實(shí)現(xiàn)的哲戚。如果用戶注冊(cè)了對(duì)某個(gè)對(duì)象的某一個(gè)屬性的觀察,那么此派生類會(huì)重寫(xiě)這個(gè)方法艾岂,并在其中添加進(jìn)行通知的代碼顺少。Objective-C 在發(fā)送消息的時(shí)候,會(huì)通過(guò) isa 指針找到當(dāng)前對(duì)象所屬的類對(duì)象澳盐。而類對(duì)象中保存著當(dāng)前對(duì)象的實(shí)例方法祈纯,因此在向此對(duì)象發(fā)送消息時(shí)候,實(shí)際上是發(fā)送到了派生類(NSKVONotifying_Person)對(duì)象的方法叼耙。由于編譯器對(duì)派生類的方法進(jìn)行了復(fù)寫(xiě)腕窥,并添加了通知代碼,因此會(huì)向注冊(cè)的對(duì)象發(fā)送通知筛婉。注意派生類只重寫(xiě)注冊(cè)了觀察者的屬性方法簇爆。

NSKVONotifying_Person類除了重寫(xiě)Person類屬性的setter方法,還會(huì)重寫(xiě)class爽撒、dealloc入蛆、_isKVOA等方法。之所以要重寫(xiě)class方法其目的就是為了隱藏NSKVONotifying_Person這個(gè)類硕勿。而重寫(xiě)setter方法是為了在其中調(diào)用- (void)willChangeValueForKey:(NSString *)key;方法和- (void)didChangeValueForKey:(NSString *)key;方法哨毁,然后再didChangeValueForKey中調(diào)用observeValueForKeyPath方法用以通知外界屬性值發(fā)生了變化。

3.3源武、NSKVONotifying_Person類的消亡

NSKVONotifying_Person類是在注冊(cè)觀察者后生成的扼褪,那么會(huì)不會(huì)在取消注冊(cè)后會(huì)消亡呢?在取消注冊(cè)之后代用打印類的方法名稱集的方法粱栖,如果有打印結(jié)果顯示則說(shuō)明NSKVONotifying_Person類并不會(huì)隨著取消注冊(cè)而消亡话浇。

如上圖所示,在取消注冊(cè)觀察者之后打印出了NSKVONotifying_Person類的所有方法闹究,說(shuō)明NSKVONotifying_Person這個(gè)類在取消注冊(cè)觀察者之后依然存在幔崖。在取消注冊(cè)觀察者后person對(duì)象的isa又指向了Person類。

4、 KVO的優(yōu)缺點(diǎn)

4.1赏寇、KVO的優(yōu)點(diǎn)
  1. KVO提供了一種簡(jiǎn)單的方法實(shí)現(xiàn)兩個(gè)對(duì)象間的同步吉嫩。例如:model和view之間同步;
  2. 能夠?qū)Ψ俏覀儎?chuàng)建的對(duì)象嗅定,即內(nèi)部對(duì)象的狀態(tài)改變作出響應(yīng)率挣,而且不需要改變內(nèi)部對(duì)象(SKD對(duì)象)的實(shí)現(xiàn);
  3. 能夠提供觀察的屬性的最新值以及先前值露戒;
  4. 用key paths來(lái)觀察屬性,因此也可以觀察嵌套對(duì)象捶箱;
  5. 完成了對(duì)觀察對(duì)象的抽象智什,因?yàn)椴恍枰~外的代碼來(lái)允許觀察值能夠被觀察
4.2、KVO的缺點(diǎn)
  1. 我們觀察的屬性必須使用strings來(lái)定義丁屎。因此在編譯器不會(huì)出現(xiàn)警告以及檢查荠锭;
  2. 對(duì)屬性重構(gòu)將導(dǎo)致我們的觀察代碼不再可用;
  3. 觀察多個(gè)屬性時(shí)晨川,需要些復(fù)雜的if判斷條件語(yǔ)句证九;
  4. 當(dāng)釋放觀察者時(shí)不需要移除觀察者。

5共虑、自定義KVO

在上邊的章節(jié)中詳細(xì)分析了KVO的使用和實(shí)現(xiàn)原理愧怜,知道了KVO的原理其實(shí)是生成了一個(gè)NSKVONotifying_XX(XX表示被觀察對(duì)象的所屬類)的中間類,這個(gè)中間類繼承自被觀察對(duì)象的所屬類妈拌,并且重寫(xiě)了這個(gè)類的屬性的setter方法和class方法拥坛,同時(shí),觀察者對(duì)象的isa指針的指向不在是指向?qū)ο蟮乃鶎兕惓痉郑侵赶蜻@個(gè)中間類猜惋。KVO觀察屬性的變化其實(shí)是觀察屬性的setter方法的調(diào)用,在中間了重寫(xiě)父類的setter方法中培愁,會(huì)調(diào)用willChangeValueForKey方法和didChangeValueForKey方法著摔,而在didChangeValueForKey方法內(nèi)部會(huì)調(diào)用observeValueForKeyPath方法將屬性的變化通知給觀察者。當(dāng)取消注冊(cè)觀察者的時(shí)候定续,被觀察對(duì)象的isa指針會(huì)重寫(xiě)指向所屬類谍咆。

5.1、基本思路

根據(jù)KVO的實(shí)現(xiàn)原理設(shè)計(jì)自定義KVO的基本思路如下:

  1. 首先檢查被觀察對(duì)象的屬性的setter方法是有實(shí)現(xiàn)香罐;
  2. 動(dòng)態(tài)生一個(gè)中間類繼承自被觀察對(duì)象的所屬類卧波;
  3. 為中間類添加setter方法、class方法庇茫、dealloc方法港粱;
  4. 修改被觀察對(duì)象的isa指向中間類;
  5. 在中間類的setter方法里面進(jìn)行消息發(fā)送給父類,通過(guò)block的方式將屬性的新值和舊值傳回查坪;
  6. 在dealloc方法里面進(jìn)行isa重寫(xiě)指回寸宏。

5.2、注冊(cè)觀察者

按照上面的思路偿曙,首先是要對(duì)被觀察對(duì)象的屬性的setter方法進(jìn)行驗(yàn)證氮凝。驗(yàn)證的關(guān)鍵點(diǎn)就是對(duì)于參入的keyPath進(jìn)行setter的方法的拼接,因?yàn)檫@里的keyPath實(shí)際上就是被觀察對(duì)象的被觀察屬性望忆。

- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath {
    Class superClass = object_getClass(self);
    SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"沒(méi)有當(dāng)前%@的setter罩阵,請(qǐng)檢查keyPath參數(shù)的正確性", keyPath] userInfo:nil];
    }
}

然后需要?jiǎng)討B(tài)的創(chuàng)建一個(gè)中間類,繼承自被觀察對(duì)象的所屬類启摄,并且要重寫(xiě)setter和class方法稿壁。

static NSString *const kDSKVOPrefix = @"DSKVONotifying_";

- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
    NSString *oldClassName = NSStringFromClass([self class]);
    NSString *newClassName = [NSString stringWithFormat:@"%@%@", kDSKVOPrefix, oldClassName];
    Class newClass = NSClassFromString(newClassName);
    // 防止重復(fù)創(chuàng)建生成新類
    if (newClass) return newClass;
    // 申請(qǐng)類
    newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
    //  注冊(cè)類
    objc_registerClassPair(newClass);
    // 添加class : class的指向是父類
    SEL classSEL = NSSelectorFromString(@"class");
    Method classMethod = class_getInstanceMethod([self class], classSEL);
    const char *classTypes = method_getTypeEncoding(classMethod);
    class_addMethod(newClass, classSEL, (IMP)ds_class, classTypes);

    //  添加setter
    SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
    Method setterMethod = class_getInstanceMethod([self class], setterSEL);
    const char *setterTypes = method_getTypeEncoding(setterMethod);
    class_addMethod(newClass, setterSEL, (IMP)ds_setter, setterTypes);

    // 2.3.3 : 添加dealloc
    SEL deallocSEL = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
    const char *deallocTypes = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSEL, (IMP)ds_dealloc, deallocTypes);
    return newClass;
}

Class ds_class(id self, SEL _cmd)
{
    return class_getSuperclass(object_getClass(self));
}

然后需要修改被觀察對(duì)象的isa指向中間類,并且需要將KVO的信息進(jìn)行對(duì)象化保存歉备。完整的注冊(cè)觀察者的方法代碼如下:

static NSString *const kDSKVOAssiociateKey = @"kDSKVO_AssiociateKey";
- (void)ds_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(DSKVOBlock)block {
    // 1: 驗(yàn)證是否存在setter方法 : 不讓實(shí)例進(jìn)來(lái)
    [self judgeSetterMethodFromKeyPath:keyPath];
    // 2: 動(dòng)態(tài)生成子類
    Class newClass = [self createChildClassWithKeyPath:keyPath];
    // 3: 修改isa的指向 
    object_setClass(self, newClass);
    // 4: 保存信息
    DSKVOInfo *info = [[DSKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey));
    if (!mArray) {
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
}

在這里定義了一個(gè)類來(lái)對(duì)象化KVO的信息傅是,并且用關(guān)聯(lián)對(duì)象的形式進(jìn)行信息的保存和取出。

typedef void(^DSKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

@interface DSKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;//觀察者蕾羊,這里用weak修飾喧笔,避免出現(xiàn)循環(huán)引用
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) DSKVOBlock handleBlock;
@end

@implementation DSKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(DSKVOBlock)block {
    if (self = [super init]) {
        _observer = observer;
        _keyPath = keyPath;
        _handleBlock = block;
    }
    return self;
}
@end

5.3、通知觀察者

根據(jù)KVO的原理龟再,觀察者觀察到被觀察者對(duì)象狀態(tài)的變化其實(shí)是觀察的屬性的setter方法书闸,那么我們就可以定義自己的setter方法,只需要發(fā)送出狀態(tài)變化后的值的消息就可以了利凑。

/// 自定義setter方法梗劫,屬于中間類,父類發(fā)送消息
/// @param _cmd 方法編號(hào)截碴,被觀察屬性的setter方法
/// @param newValue 屬性的新的值
static void ds_setter(id self, SEL _cmd, id newValue)
{
    NSLog(@"來(lái)了:%@", newValue);
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    // 消息轉(zhuǎn)發(fā) : 轉(zhuǎn)發(fā)給父類
    // 改變父類的值 --- 可以強(qiáng)制類型轉(zhuǎn)換
    
    struct objc_super superStruct = {
        .receiver    = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue);
    void (*ds_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    ds_msgSendSuper(&superStruct, _cmd, newValue);

    //信息數(shù)據(jù)回調(diào)
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void *_Nonnull)(kDSKVOAssiociateKey));
    for (DSKVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}

在這里之所以要想父類的setter方法發(fā)送消息是為了修改調(diào)用父類的setter方法修改屬性值梳侨。

5.4、自動(dòng)移除觀察者

移除觀察者其實(shí)就是講被觀察者對(duì)象的isa指針指回去日丹。

static void ds_dealloc(id self, SEL _cmd)
{
    NSLog(@"ds_dealloc");
    Class superClass = [self class];
    object_setClass(self, superClass);
}

這里的自定義KVO代碼比較簡(jiǎn)單走哺,其目的只是為了更好的理解KVO的實(shí)現(xiàn)原理。實(shí)際上這里的代碼還存在很多的問(wèn)題哲虾,比如說(shuō)Options參數(shù)處理丙躏,context參數(shù)處理,觀察嵌套對(duì)象的處理束凑,多線程問(wèn)題等等一系列的問(wèn)題晒旅,所以這里的代碼不具備代碼設(shè)計(jì)的完整性。如果想要更加優(yōu)雅的使用KVO汪诉,建議大家去閱讀KVOController的源碼废恋,如果對(duì)于KVO的實(shí)現(xiàn)原理沒(méi)有那么清晰的認(rèn)識(shí)谈秫,可以去GNU下載閱讀KVO的相關(guān)代碼。

6鱼鼓、總結(jié)

  1. KVO調(diào)用addObserver:方法注冊(cè)觀察者拟烫,observer是觀察者對(duì)象,keyPath是被觀察者的屬性名稱迄本,不能為nil硕淑,options參數(shù)的傳入關(guān)系到接收通知的change字典的值,context上下問(wèn)為區(qū)分對(duì)象屬性變化提供有效途徑嘉赎。
  2. 觀察者實(shí)現(xiàn)observeValueForKeyPath方法接收屬性變化的通知置媳。
  3. 調(diào)用removeObserver方法實(shí)現(xiàn)取消注冊(cè)觀察者,這是一個(gè)必要的過(guò)程公条。
  4. KVO的原理在于生成了一個(gè)繼承于被觀察者對(duì)象的類的中間類半开,這個(gè)中間類重寫(xiě)了父類的屬性的setter方法,并且修改了被觀察者對(duì)象的isa指針指向赃份,重寫(xiě)的setter方法里面調(diào)用了willChangeValueForKeydidChangeValueForKey方法,而在didChangeValueForKey方法內(nèi)部會(huì)調(diào)用observeValueForKeyPath方法奢米,從而達(dá)到了屬性修改后通知觀察者的目的抓韩。
  5. 在取消注冊(cè)觀察者后生成的中間類并不會(huì)消亡,并且被觀察者對(duì)象的isa指針會(huì)重新指向原來(lái)的類鬓长。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載谒拴,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末涉波,一起剝皮案震驚了整個(gè)濱河市英上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌啤覆,老刑警劉巖苍日,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異窗声,居然都是意外死亡相恃,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)笨觅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)拦耐,“玉大人,你說(shuō)我怎么就攤上這事见剩∩迸矗” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵苍苞,是天一觀的道長(zhǎng)固翰。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么倦挂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任畸颅,我火速辦了婚禮,結(jié)果婚禮上方援,老公的妹妹穿的比我還像新娘没炒。我一直安慰自己,他們只是感情好犯戏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布送火。 她就那樣靜靜地躺著,像睡著了一般先匪。 火紅的嫁衣襯著肌膚如雪种吸。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,287評(píng)論 1 301
  • 那天呀非,我揣著相機(jī)與錄音坚俗,去河邊找鬼。 笑死岸裙,一個(gè)胖子當(dāng)著我的面吹牛猖败,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播降允,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼恩闻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了剧董?” 一聲冷哼從身側(cè)響起幢尚,我...
    開(kāi)封第一講書(shū)人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翅楼,沒(méi)想到半個(gè)月后尉剩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡毅臊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年边涕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褂微。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡功蜓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宠蚂,到底是詐尸還是另有隱情式撼,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布求厕,位于F島的核電站著隆,受9級(jí)特大地震影響扰楼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜美浦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一弦赖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浦辨,春花似錦蹬竖、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至芽腾,卻和暖如春旦装,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背摊滔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工阴绢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人艰躺。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓呻袭,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親描滔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容