KVC/KVO原理詳解及編程指南
轉載請註明出處
若是以爲文章對你有所幫助,請經過留言或關注微信公衆賬號wangzzstrive來支持我,謝謝!
前言:
一、本文基本不講KVC/KVO的用法,只結合網上的資料說說對這種技術的理解。
二、因爲KVO內容較少,並且是以KVC爲基礎實現的,本文將着重介紹KVC部分。
1、簡介
KVC/KVO是觀察者模式的一種實現,在Cocoa中是以被萬物之源NSObject類實現的NSKeyValueCoding/NSKeyValueObserving非正式協議的形式被定義爲基礎框架的一部分。從協議的角度來講,KVC/KVO本質上是定義了一套讓咱們去遵照和實現的方法。
固然,KVC/KVO實現的根本是Objective-C的動態性和runtime,這在後文的原理部分會有詳述。
另外,KVC/KVO機制離不開訪問器方法的實現,這在後文中也有解釋。
一、KVC簡介
全稱是Key-value coding,翻譯成鍵值編碼。顧名思義,在某種程度上跟map的關係匪淺。它提供了一種使用字符串而不是訪問器方法去訪問一個對象實例變量的機制。
二、KVO簡介
全稱是Key-value observing,翻譯成鍵值觀察。提供了一種當其它對象屬性被修改的時候能通知當前對象的機制。再MVC大行其道的Cocoa中,KVO機制很適合實現model和controller類之間的通信。
2、KVC相關技術
一、Key和Key Path
KVC定義了一種按名稱訪問對象屬性的機制,支持這種訪問的主要方法是:
- - (id)valueForKey:(NSString *)key;
- - (void)setValue:(id)value forKey:(NSString *)key;
- - (id)valueForKeyPath:(NSString *)keyPath;
- - (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
前邊兩個方法用到的Key較容易理解,就是要訪問的屬性名稱對應的字符串。
後面兩個方法用到的KeyPath是一個被點操做符隔開的用於訪問對象的指定屬性的字符串序列。好比KeyPath address.street將會訪問消息接收對象所包含的address屬性中包含的一個street屬性。其實KeyPath說白了就是咱們平時使用點操做訪問某個對象的屬性時所寫的那個字符串。
二、點語法和KVC
在實現了訪問器方法的類中,使用點語法和KVC訪問對象其實差異不大,兩者能夠任意混用。可是沒有訪問起方法的類中,點語法沒法使用,這時KVC就有優點了。(緣由見第三部分的第一節:KVC如何訪問屬性值。)
三、一對多關係(To-Many)中的集合訪問器方法
咱們平時大部分使用的屬性都是一對一關係(To-One),好比Person類中的name屬性,每一個人只有一個名字。但也有一對多的關係,好比Person中有一個friendsName屬性,這是個集合(在Objective-C中能夠是NSArray,NSSet等),保存的是一我的的全部朋友的名字。
當操做一對多的屬性中的內容時,咱們有兩種選擇:
①間接操做
先經過KVC方法取到集合屬性,而後經過集合屬性操做集合中的元素。
②直接操做
蘋果爲咱們提供了一些方法模板,咱們能夠以規定的格式實現這些方法來達到訪問集合屬性中元素的目的。
有序集合對應方法以下:
- -countOf<Key>
- -objectIn<Key>AtIndex:
- -<key>AtIndexes:
- -get<Key>:range:
-
- -insertObject:in<Key>AtIndex:
- -insert<Key>:atIndexes:
- -removeObjectFrom<Key>AtIndex:
- -remove<Key>AtIndexes:
- -replaceObjectIn<Key>AtIndex:withObject:
- -replace<Key>AtIndexes:with<Key>:
無序集合對應方法以下:
- -countOf<Key>
- -objectIn<Key>AtIndex:
- -<key>AtIndexes:
- -get<Key>:range:
-
- -insertObject:in<Key>AtIndex:
- -insert<Key>:atIndexes:
- -removeObjectFrom<Key>AtIndex:
- -remove<Key>AtIndexes:
- -replaceObjectIn<Key>AtIndex:withObject:
- -replace<Key>AtIndexes:with<Key>:
不過這些方法除非是頗有需求,不然我的認爲沒有實現的必要,間接法也不是很麻煩,基本能知足需求了。值得指出的是,蘋果甚至都沒有讓這些方法以哪怕是非正式協議的形式出現,而只是在編程指南中提了一下。
四、鍵值驗證(Key-Value Validation)
KVC提供了驗證Key對應的Value是否可用的方法:
- - (BOOL)validateValue:(inout id *)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
該方法默認的實現是調用一個以下格式的方法:
- - (BOOL)validate<Key>:error:
好比屬性name對應的方法爲:
- -(BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError {
-
- return ...;
- }
這樣就給了咱們一次糾錯的機會。
須要指出的是,KVC是不會自動調用鍵值驗證方法的,就是說咱們須要手動驗證。可是有些技術,好比CoreData會自動調用。
五、KVC對數值和結構體型屬性的支持
一套機制若是不支持數值和結構體型的數據,那麼它的實用性就會大大折扣。幸運的是KVC中蘋果對這方面的支持作的很好。KVC能夠自動的將數值或結構體型的數據打包或解包成NSNumber或NSValue對象,以達到適配的目的。
舉個例子,Person類有個個NSInteger類型的age屬性
①修改值
咱們經過KVC技術使用以下方式設置age屬性的值:
- [person setValue:[NSNumber numberWithInteger:5] forKey:@"age"];
咱們賦給age的是一個NSNumber對象,KVC會自動的將NSNumber對象轉換成NSInteger對象,而後再調用相應的訪問器方法設置age的值。
②獲取值
一樣,以以下方式獲取age屬性值:
- [person valueForKey:@"age"];
這時,會以NSNumber的形式返回age的值。
須要說明的是,何時返回的是NSNumber,何時返回的是NSValue?
③使用NSNumber封裝
可使用NSNumber的數據類型有:
- + (NSNumber *)numberWithChar:(char)value;
- + (NSNumber *)numberWithUnsignedChar:(unsigned char)value;
- + (NSNumber *)numberWithShort:(short)value;
- + (NSNumber *)numberWithUnsignedShort:(unsigned short)value;
- + (NSNumber *)numberWithInt:(int)value;
- + (NSNumber *)numberWithUnsignedInt:(unsigned int)value;
- + (NSNumber *)numberWithLong:(long)value;
- + (NSNumber *)numberWithUnsignedLong:(unsigned long)value;
- + (NSNumber *)numberWithLongLong:(long long)value;
- + (NSNumber *)numberWithUnsignedLongLong:(unsigned long long)value;
- + (NSNumber *)numberWithFloat:(float)value;
- + (NSNumber *)numberWithDouble:(double)value;
- + (NSNumber *)numberWithBool:(BOOL)value;
- + (NSNumber *)numberWithInteger:(NSInteger)value NS_AVAILABLE(10_5, 2_0);
- + (NSNumber *)numberWithUnsignedInteger:(NSUInteger)value NS_AVAILABLE(10_5, 2_0);
總之就是一些常見的數值型數據。
④使用NSValue封裝
NSValue主要用於處理結構體型的數據,它自己提供了以下集中結構的支持:
- + (NSValue *)valueWithCGPoint:(CGPoint)point;
- + (NSValue *)valueWithCGSize:(CGSize)size;
- + (NSValue *)valueWithCGRect:(CGRect)rect;
- + (NSValue *)valueWithCGAffineTransform:(CGAffineTransform)transform;
- + (NSValue *)valueWithUIEdgeInsets:(UIEdgeInsets)insets;
- + (NSValue *)valueWithUIOffset:(UIOffset)insets NS_AVAILABLE_IOS(5_0);
只有有限的6種而已!那對於其它自定義的結構體怎麼辦?別擔憂,任何結構體都是能夠轉化成NSValue對象的,具體實現方法參見我以前的一篇文章:
六、集合運算符(Collection Operators)
集合運算符是一個特殊的Key Path,能夠做爲參數傳遞給valueForKeyPath:方法,注意只能是這個方法,若是傳給了valueForKey:方法保證你程序崩潰。
運算符是一個以@開頭的特殊字符串,格式以下圖所示:
①簡單集合運算符
簡單集合運算符共有@avg,@count,@max,@min,@sum5種,都表示啥不用我說了吧,目前還不支持自定義。
有一個集合類的對象:transactions,它存儲了一個個的Transaction類的實例,該類有三個屬性:payee,amount,date。下面以此爲例說明如何使用這些運算符:
要獲取amount的平均值能夠這樣:
- NSNumber *transactionAverage = [transactions valueForKeyPath:@"@avg.amount"];
要獲取transactions集合中元素數目能夠這樣:
- NSNumber *numberOfTransactions = [transactions valueForKeyPath:@"@count"];
須要之處的是,@count是這些集合運算符中比較特殊的一個,由於它沒有右路經,緣由很容易理解。
②對象運算符
比集合運算符稍微複雜,能以數組的方式返回指定的內容,一共有兩種:
- @distinctUnionOfObjects
- @unionOfObjects
它們的返回值都是NSArray,區別是前者返回的元素都是惟一的,是去重之後的結果;後者返回的元素是全集。
用法以下:
- NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
- NSArray *payees = [transactions valueForKeyPath:@"@unionOfObjects.payee"];
前者會將收款人的姓名去除重複的之後返回,後者直接返回全部收款人的姓名。
③Array和Set操做符
這種狀況更復雜了,說的是集合中包含集合的狀況,咱們執行了以下的一段代碼:
- self.arrayOfTransactionsArray = [NSMutableArray array];
-
- [arrayOfTransactionsArray addObject:transactions];
-
- [arrayOfTransactionsArrays addObject:moreTransactions];
獲得了一個包含集合的集合:arrayOfTransactionsArray
這時若是咱們想操做arrayOfTransactionsArray中包含的集合中的元素時,可使用以下三個運算符:
- @distinctUnionOfArrays
- @unionOfArrays
- @distinctUnionOfSets
前兩個針對的集合是Arrays,後一個針對的集合是Sets。由於Sets中的元素自己就是惟一的,因此沒有對應的@unionOfSets運算符。
它們的用法舉例以下:
- NSArray *payees = [arrayOfTransactionsArrays valueForKeyPath:@"@unionOfArrays.payee"];
3、實現原理
一、KVC如何訪問屬性值
KVC再某種程度上提供了訪問器的替代方案。不過訪問器方法是一個很好的東西,以致於只要是有可能,KVC也儘可能再訪問器方法的幫助下工做。爲了設置或者返回對象屬性,KVC按順序使用以下技術:
①檢查是否存在-<key>、-is<key>(只針對布爾值有效)或者-get<key>的訪問器方法,若是有可能,就是用這些方法返回值;
檢查是否存在名爲-set<key>:的方法,並使用它作設置值。對於-get<key>和-set<key>:方法,將大寫Key字符串的第一個字母,並與Cocoa的方法命名保持一致;
②若是上述方法不可用,則檢查名爲-_<key>、-_is<key>(只針對布爾值有效)、-_get<key>和-_set<key>:方法;
③若是沒有找到訪問器方法,能夠嘗試直接訪問實例變量。實例變量能夠是名爲:<key>或_<key>;
④若是仍爲找到,則調用valueForUndefinedKey:和setValue:forUndefinedKey:方法。這些方法的默認實現都是拋出異常,咱們能夠根據須要重寫它們。
二、KVC/KVO實現原理
鍵值編碼和鍵值觀察是根據isa-swizzling技術來實現的,主要依據runtime的強大動態能力。下面的這段話是引自網上的一篇文章:
當某個類的對象第一次被觀察時,系統就會在運行期動態地建立該類的一個派生類,在這個派生類中重寫基類中任何被觀察屬性的 setter 方法。
派生類在被重寫的 setter 方法實現真正的通知機制,就如前面手動實現鍵值觀察那樣。這麼作是基於設置屬性會調用 setter 方法,而經過重寫就得到了 KVO 須要的通知機制。固然前提是要經過遵循 KVO 的屬性設置方式來變動屬性值,若是僅是直接修改屬性對應的成員變量,是沒法實現 KVO 的。
同時派生類還重寫了 class 方法以「欺騙」外部調用者它就是起初的那個類。而後系統將這個對象的 isa 指針指向這個新誕生的派生類,所以這個對象就成爲該派生類的對象了,於是在該對象上對 setter 的調用就會調用重寫的 setter,從而激活鍵值通知機制。此外,派生類還重寫了 dealloc 方法來釋放資源。
原文寫的很好,還舉了解釋性的例子,你們能夠去看看。
在我以前的一篇介紹Objective-C類和元類的文章:
中介紹過,isa指針指向的實際上是類的元類,若是以前的類名爲:Person,那麼被runtime更改之後的類名會變成:NSKVONotifying_Person。
新的NSKVONotifying_Person類會重寫如下方法:
增長了監聽的屬性對應的set方法,class,dealloc,_isKVOA。
①class
重寫class方法是爲了咱們調用它的時候返回跟重寫繼承類以前一樣的內容。
打印以下內容:
- NSLog(@"self->isa:%@",self->isa);
- NSLog(@"self class:%@",[self class]);
在創建KVO監聽前,打印結果爲:
- self->isa:Person
- self class:Person
在創建KVO監聽以後,打印結果爲:
- self->isa:NSKVONotifying_Person
- self class:Person
這也是isa指針和class方法的一個區別,你們使用的時候注意。
②重寫set方法
新類會重寫對應的set方法,是爲了在set方法中增長另外兩個方法的調用:
- - (void)willChangeValueForKey:(NSString *)key
- - (void)didChangeValueForKey:(NSString *)key
其中,didChangeValueForKey:方法負責調用:
- - (void)observeValueForKeyPath:(NSString *)keyPath
- ofObject:(id)object
- change:(NSDictionary *)change
- context:(void *)context
方法,這就是KVO實現的原理了!
若是沒有任何的訪問器方法,-setValue:forKey方法會直接調用:
- - (void)willChangeValueForKey:(NSString *)key
- - (void)didChangeValueForKey:(NSString *)key
若是在沒有使用鍵值編碼且沒有使用適當命名的訪問起方法的時候,咱們只須要顯示調用上述兩個方法,一樣可使用KVO!
總結一下,想使用KVO有三種方法:
1)使用了KVC
使用了KVC,若是有訪問器方法,則運行時會在訪問器方法中調用will/didChangeValueForKey:方法;
沒用訪問器方法,運行時會在setValue:forKey方法中調用will/didChangeValueForKey:方法。
2)有訪問器方法
運行時會重寫訪問器方法調用will/didChangeValueForKey:方法。
所以,直接調用訪問器方法改變屬性值時,KVO也能監聽到。
3)顯示調用will/didChangeValueForKey:方法。
總之,想使用KVO,只要有will/didChangeValueForKey:方法就能夠了。
③_isKVOA
這個私有方法估計是用來標示該類是一個 KVO 機制聲稱的類。
4、優勢和缺點
一、優勢
①能夠再很大程度上簡化代碼
例子網上不少,這就不舉了
②能跟腳本語言很好的配合
才疏學淺,沒學過AppleScript等腳本語言,因此沒能深入體會到該優勢。
二、缺點
KVC的缺點不明顯,主要是KVO的,詳情能夠參考這篇文章:
核心思想是說KVO的回調機制,不能傳一個selector或者block做爲回調,而必須重寫-addObserver:forKeyPath:options:context:方法所引起的一系列問題。問了解決這個問題,做者還親自實現了一個MAKVONotificationCenter類,代碼見github:
不過我的認爲這只是蘋果作的KVO不夠完美,不能算是缺陷。
參考文檔: