iOS基礎黑科技:KVO(鍵值觀察)以及KVC(鍵值編碼)

一、前言:

許多大神的博客都有關于KVO以及KVC的分析,在這里我整理一下自己關于KVO以及KVC的理解。求大神們輕噴~~


二、基本概念:

1、鍵值編碼(KVC):顧名思義,鍵值編碼是一種訪問對象屬性的機制。通常情況下我們會應用點語法(xxx.name)來訪問一個對象中的屬性。通過KVC,我們可以只使用屬性名字的字符串(也就是鍵)來間接訪問和操作對象的屬性(但其實在KVC的底層實現中,還是會調用相應屬性的存取方法,如果對應的存取方法存在的話),同時,利用KVC可以訪問對象中的私有變量,而不僅僅是公開的屬性。在NSKeyValueCoding.h的頭文件中可以看到相應的方法聲明。

2、鍵值觀察(KVO):鍵值觀察是一種通知機制能夠使得被觀察對象的屬性在發生變化的時候通知觀察者,類似于一種target-action的機制,是Cocoa中觀察者模式的一種實現。通常鍵值觀察會協同鍵值編碼來一起使用。在NSKeyValueObserving.h頭文件中可以看到跟KVO相關的方法。


三、基本使用:


如上圖所示,利用KVC可以修改對象中的私有變量

其他的一些功能,例如:當找不到鍵名稱對應的屬性或者變量的時候,可以使用- (id)valueForUndefinedKey:(NSString *)key 來處理;

在設置值之前檢驗鍵名字是否有效: (BOOL)validateValue:(inout id? _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError;

以及利用鍵值編碼的集合操作符來對集合元素屬性進行操作:集合鍵路徑.@操作符.屬性鍵路徑(例如有一個items的NSArray的屬性和一個price的float型屬性,我們可以通過集合操作符 items.@count 以及 @sum.price 來分別計算元素個數以及總和)


四、KVC的設置變量值的搜索模式:

(1)首先會搜索對應屬性名稱的存取方法,也就是setter以及getter方法,如果有就直接調用存取方法。

(2)如果要訪問的變量沒有存取方法(例如私有的實例變量或者沒有自動生成存取方法的屬性值),那么會首先檢查類方法檢查類方法:+ (BOOL)accessInstanceVariablesDirectly,如果該類方法返回Yes,就會按照名稱順序:_key、_isKey、key、isKey來搜索實例變量;如果該類方法返回NO,就會調用setValue:forUndefinedKey: 或者 valueForUndefinedKey:的方法。

下面我們來實驗一下KVC的搜索模式:


首先我們設置四個名稱按照上述規則命名的私有變量,然后調用KVC賦值方法,打印出四個私有變量的值。


從結果可以看到,KVC搜索機制會最先搜索以_key為名稱命名的變量

接著我們注釋掉一些變量,看看是否符合我們預期的結論:


如上圖所示,搜索機制完全符合上述的結論。


(3)KVC 與點語法比較:用點語法編譯器會做預編譯檢查,訪問不存在的屬性編譯器會報錯,但是用 KVC 方式編譯器無法做檢查, 如果有錯誤只能運行的時候才能發現。相比點語法用 KVC 方式 KVC 的效率會稍低一點,但是較為靈活,可以在程序運行時決定訪問哪些屬性,并且用 KVC 可以訪問對象的私有成員變量,訪問速度方面,通過點語法訪問變量比使用KVC訪問變量要快,因為點語法是直接訪問存取方法,沒有響應復雜的搜索機制。


五、KVO的實現原理:

KVO的實現原理,其實就是運行時使用了指針混淆,也就是所謂的 isa-swizzling 黑科技。(isa是一個很重要的指針,每一個實例和類都有一個isa指針,實例變量的isa指針指向對應的類,而類的isa指針指向其元類,在方法調用的運行時實現,其實就是根據isa指針找到所在的類或者元類,接著在類或者元類的方法列表中尋找。)

具體的指針混淆機制就是:當某個實例(例如Person類的實例per,繼承自NSObject)作為被觀察者,調用KVO的 addObserver方法時,在運行時會動態生成Person的一個特殊的子類,之所以說它特殊,是因為這個子類的名稱是按照規則命名的,對于Person類,就會創建一個名為NSKVONotifying_Person的子類(類名前綴就是規定的NSKVONotifying_),在這個子類中,運行時會重寫要監聽屬性的setter方法(注意對于沒有監聽的屬性,是不會重寫setter方法的),class方法,dealloc方法,還有一個名為_isKVOA的方法,然后重點來了,運行時會把實例per的 isa 指針指向修改,原本是指向Person類,修改為指向NSKVONotifying_Person類,也就是新創建的 “特殊” 子類,利用KVO調用的被監聽屬性的存取方法,就是這個子類重寫的存取方法。下面我們來驗證一下:


首先寫一個函數,利用runtime獲取一個類里面的方法。


? 因為不能直接訪問類里面的isa指針,我們這里反過來證明,假設存在個名為?NSKVONotifying_Person?的類,我們嘗試去獲取它,因為如果不存在的話,會直接返回nil,這樣我們就可以判斷是否存在這個類。

? 如上圖所示,我們成功獲取了NSKVONotifying_Person這個類,并且打印出了里面的方法,確實發現重寫了name屬性的setter方法,并且重寫了class,所以我們調用 [per class] 時,是返回 Person,而不是NSKVONotifying_Person。可以看見,NSKVONotifying_Person?的父類就是Person,因此,上述結果驗證了開始的說法。


下一篇文章我們具體探討一下運行時是如何實現指針混淆的~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容