<簡書 — 劉小壯> https://www.jianshu.com/p/1d39bc610a5bhtml
在工做中常常會使用到
KVC
,可是不少人對於KVC
的實現原理並不太清楚。好比說KVC
在進行存取時,是怎麼進行查找並賦值的。ios網上有不少講
KVC
的文章,可是有不少質量並不高。這兩天抽空把我所理解的KVC
寫出來,當作學習交流,正好也讓各位大神幫我指正一下,十分感謝!macos
KVC
全稱是Key Value Coding
,定義在NSKeyValueCoding.h
文件中,是一個非正式協議。KVC
提供了一種間接訪問其屬性方法或成員變量的機制,能夠經過字符串來訪問對應的屬性方法或成員變量。數組
在NSKeyValueCoding
中提供了KVC
通用的訪問方法,分別是getter
方法valueForKey:
和setter
方法setValue:forKey:
,以及其衍生的keyPath
方法,這兩個方法各個類通用的。而且由KVC
提供默認的實現,咱們也能夠本身重寫對應的方法來改變實現。安全
KVC
主要對三種類型進行操做,基礎數據類型及常量、對象類型、集合類型。服務器
@interface BankAccount : NSObject @property (nonatomic, strong) NSNumber *currentBalance; @property (nonatomic, strong) Person *owner; @property (nonatomic, strong) NSArray<Transaction *> *transactions; @end
在使用KVC
時,直接將屬性名當作key
,並設置value
,便可對屬性進行賦值。app
[myAccount setValue:@(100.0) forKey:@"currentBalance"];
**除了對當前對象的屬性進行賦值外,還能夠對其更「深層」的對象進行賦值。**例如對當前對象的address
屬性的street
屬性進行賦值。KVC
進行多級訪問時,直接相似於屬性調用同樣用點語法進行訪問便可。佈局
[myAccount setValue:@"中關村大街" forKeyPath:@"address.street"];
經過keyPath
對數組進行取值時,而且數組中存儲的對象類型都相同,能夠經過valueForKeyPath:
方法指定取出數組中全部對象的某個字段。例以下面例子中,經過valueForKeyPath:
將數組中全部對象的name
屬性值取出,並放入一個數組中返回。性能
NSArray *names = [array valueForKeyPath:@"name"];
須要注意的是,雖然看到dictionary
的字樣,下面兩個方法並非字典的方法。學習
KVC
還有更強大的功能,能夠根據給定的一組key
,獲取到一組value
,而且以字典的形式返回,獲取到字典後能夠經過key
從字典中獲取到value
。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
一樣,也能夠經過KVC
進行批量賦值。在對象調用setValuesForKeysWithDictionary:
方法時,能夠傳入一個包含key
、value
的字典進去,KVC
能夠將全部數據按照屬性名和字典的key
進行匹配,並將value
給User
對象的屬性賦值。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
在項目中常常會遇到字典轉模型的狀況,若是在自定義的init
方法裏逐個賦值,這樣每次數據發生改變還須要改賦值語句。然而經過KVC
爲咱們提供的賦值API,能夠對數據進行批量賦值。假設有如下JSON
數據並定義User
類,在外界經過setValuesForKeysWithDictionary:
方法對User
進行賦值。
JSON數據: { "username": "lxz", "age": 25, "id": 100 } @interface User : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSString age; @property (nonatomic, assign) NSInteger userId; @end @implementation User - (void)setValue:(id)value forUndefinedKey:(NSString *)key { if ([key isEqualToString:@"id"]) { self.userId = [value integerValue]; } } @end
賦值時會遇到一些問題,例如服務器會返回一個id
字段,可是對於客戶端來講id
是系統保留字段,能夠重寫setValue:forUndefinedKey:
方法並在內部處理id
參數的賦值。
轉換時須要服務器數據和類定義匹配,字段數量和字段名都應該匹配。若是User
比服務器數據多,則服務器沒傳的字段爲空。若是服務端傳遞的數據User
中沒有定義,則會致使崩潰。
在KVC
進行屬性賦值時,內部會對基礎數據類型作處理,不須要手動作NSNumber
的轉換。須要注意的是,NSArray
和NSDictionary
等集合對象,value
都不能是nil
,不然會致使Crash
。
當根據KVC
搜索規則,沒有搜索到對應的key
或者keyPath
,則會調用對應的異常方法。異常方法的默認實現,在異常發生時會拋出一個NSUndefinedKeyException
的異常,而且應用程序Crash
。
咱們能夠重寫下面兩個方法,根據業務需求合理的處理KVC
致使的異常。
- (nullable id)valueForUndefinedKey:(NSString *)key; - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
當經過KVC
給某個非對象的屬性賦值爲nil
時,此時KVC
會調用屬性所屬對象的setNilValueForKey:
方法,並拋出NSInvalidArgumentException
的異常,並使應用程序Crash
。
咱們能夠經過重寫下面方法,在發生這種異常時進行處理。例如給name
賦值爲nil
的時候,就能夠重寫setNilValueForKey:
方法並表示name
是空的。
- (void)setNilValueForKey:(NSString *)key { if ([key isEqualToString:@"name"]) { [self setValue:@"" forKey:@」age」]; } else { [super setNilValueForKey:key]; } }
根據KVO
的實現原理,是在運行時生成新的子類並重寫其setter
方法,在其內容發生改變時發送消息。但這只是對屬性直接進行賦值會觸發,若是屬性是容器對象,對容器對象進行add
或remove
操做,則不會調用KVO
的方法。能夠經過KVC
對應的API
來配合使用,使容器對象內部發生改變時也能觸發KVO
。
在進行容器對象操做時,先調用下面方法經過key
或者keyPath
獲取集合對象,而後再對容器對象進行add
或remove
等操做時,就會觸發KVO
的消息通知了。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; - (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); - (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
keyPath方法:
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath; - (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); - (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
KVC
提供的valueForKeyPath:
方法很是強大,能夠經過該方法對集合對象進行「深刻」操做,在其keyPath
中嵌套集合運算符,例如求一個數組中對象某個屬性的count
。(集合對象主要指NSArray
和NSSet
,但不包括NSDictionary
)
上面表達式主要分爲三部分,left
部分是要操做的集合對象,若是調用KVC
的對象原本就是集合對象,則left
能夠爲空。中間部分是表達式,表達式通常以@符號開頭。後面是進行運算的屬性。
集合運算符主要分爲三類:
NSNumber
爲主。下面是爲了方便模擬KVC
操做,而建立的測試代碼。定義Transaction
類爲模型類,類中包含三種類型的屬性。並定義BankAccount
類,其中包含一個數組,下面的代碼示例就都是操做這個數組的,而且數組包含全部Transaction
對象。
@interface Transaction : NSObject @property (nonatomic, strong) NSString *payee; @property (nonatomic, strong) NSNumber *amount; @property (nonatomic, strong) NSDate *date; @end
@interface BankAccount : NSObject @property (nonatomic, strong) NSArray *transactions; @end
集合操做符處理NSArray
和NSSet
及其子類這樣的集合對象,並根據不一樣的操做符返回不一樣類型的對象,返回值通常都是NSNumber
。
@avg
用來計算集合中right keyPath
指定的屬性的平均值。NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
@count
用來計算集合的總數。NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
備註:@count
操做符比較特殊,它不須要寫right keyPath
,即便寫了也會被忽略。
@sum
用來計算集合中right keyPath
指定的屬性的總和。NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
@max
用來查找集合中right keyPath
指定的屬性的最大值。NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
@min
用來查找集合中right keyPath
指定的屬性的最小值。NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
備註:@max
和@min
在進行判斷時,都是經過調用compare:
方法進行判斷,因此能夠經過重寫該方法對判斷過程進行控制。
@unionOfObjects
將集合對象中,全部payee
對象放在一個數組中並返回。NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
@distinctUnionOfObjects
將集合對象中,全部payee
對象放在一個數組中,並將數組進行去重後返回。NSArray *distinctPayees = [self.transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];
注意:以上兩個方法中,若是操做的屬性爲nil
,在添加到數組中時會致使Crash
。
因爲嵌套操做符是須要對嵌套的集合對象進行操做,因此新建一個arrayOfArrays
對象,其中包含兩個數組,數組中存儲的都是Transaction
類型對象。
NSArray *moreTransactions = ....; NSArray *arrayOfArrays = @[self.transactions, moreTransactions];
@unionOfArrays
是用來操做集合內部的集合對象,將全部right keyPath
對應的對象放在一個數組中返回。NSArray *collectedPayees = [arrayOfArrays valueForKeyPath:@"@unionOfArrays.payee"];
@distinctUnionOfArrays
是用來操做集合內部的集合對象,將全部right keyPath
對應的對象放在一個數組中,並進行排重。NSArray *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfArrays.payee"];
@distinctUnionOfSets
是用來操做集合內部的集合對象,將全部right keyPath
對應的對象放在一個set
中,並進行排重。NSSet *collectedPayees = [arrayOfArrays valueForKeyPath:@"@distinctUnionOfSets.payee"];
若是在集合對象中操做的屬性,原本就是NSNumber
類型,則能夠像下面這樣,直接用self
表明值自身。
NSArray *array = @[@(productA.price), @(productB.price), @(productC.price), @(productD.price)]; NSNumber *avg = [array valueForKeyPath:@"@avg.self"];
KVC
是支持基礎數據類型和結構體的,能夠在setter
和getter
的時候,經過NSValue
和NSNumber
來轉換爲OC
對象。Swift
中不存在這樣的需求,由於Swift
中全部變量都是對象。
如下是結構體轉換的示例代碼,能夠調用initWithBool:
方法對基礎數據類型進行包裝,除了調用方法外還能夠經過字面量實現,例如@(YES)
的調用。經過NSNumber
的boolValue
屬性轉換爲基礎數據類型。
@property (nonatomic, assign, readonly) BOOL boolValue; - (NSNumber *)initWithBool:(BOOL)value NS_DESIGNATED_INITIALIZER;
結構體轉換的代碼定義在UIGeometry.h
中,以NSValue
的Category
形式存在。NSValue
對CGPoint
、CGRect
等結構體都提供了轉換方法,例以下面是對CGPoint
進行轉換的示例代碼。
@property(nonatomic, assign, readonly) CGPoint CGPointValue; + (NSValue *)valueWithCGPoint:(CGPoint)point;
須要注意的是,不管何時都不該該給setter
中傳入nil
,會致使Crash
並引發NSInvalidArgumentException
異常。
在調用KVC
時能夠先進行驗證,驗證經過下面兩個方法進行,支持key
和keyPath
兩種方式。驗證方法默認實現返回YES
,能夠經過重寫對應的方法修改驗證邏輯。
驗證方法須要咱們手動調用,並不會在進行KVC
的過程當中自動調用。
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; - (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
下面是使用驗證方法的例子。在validateValue
方法的內部實現中,若是傳入的value
或key
有問題,能夠經過返回NO
來表示錯誤,並設置NSError
對象。
Person *person = [[Person alloc] init]; NSError *error; NSString *name = @"John"; if (![person validateValue:&name forKey:@"name" error:&error]) { NSLog(@"%@", error); }
KVC
還支持對單獨屬性作驗證,能夠經過定義validate<Key>:error:
格式的方法,並在方法內部實現驗證代碼。在編寫KVC
驗證代碼的時候,應該先查找屬性有沒有自定義validate
方法,而後再查找validateValue:
方法,若是有則調用本身實現的方法,若是兩個方法都沒有實現則默認返回YES
。
- (BOOL)validateName:(id *)ioValue error:(NSError * __autoreleasing *)outError{ if ((*ioValue == nil) || ([(NSString *)*ioValue length] < 2)) { if (outError != NULL) { *outError = [NSError errorWithDomain:PersonErrorDomain code:PersonInvalidNameCode userInfo:@{ NSLocalizedDescriptionKey : @"Name too short" }]; } return NO; } return YES; }
我以爲KVC
應該支持validateValue
自動驗證,在調用setValue
或getValue
時自動進行驗證,若是不符合驗證規則,就調用失敗。若是外界使用的地方都先調用一次validateValue
的話,這是很麻煩的。固然也有解決方法,能夠經過Method Swizzling
方法hook
住setValue
和getValue
方法。
**KVC
在經過key
或者keyPath
進行操做的時候,能夠查找屬性方法、成員變量等,查找的時候能夠兼容多種命名。**具體的查找規則要以官方文檔爲主,因此我把官方文檔翻譯了一下寫在下面。
在KVC
的實現中,依賴setter
和getter
的方法實現,因此方法命名應該符合蘋果要求的規範,不然會致使KVC
失敗。
在學習KVC
的搜索規則前,要先弄明白一個屬性的做用,這個屬性在搜索過程當中起到很重要的做用。這個屬性表示是否容許讀取實例變量的值,若是爲YES
則在KVC
查找的過程當中,從內存中讀取屬性實例變量的值。
@property (class, readonly) BOOL accessInstanceVariablesDirectly;
這是valueForKey:
的默認實現,給定一個key
當作輸入參數,開始下面的步驟,在這個接收valueForKey:
方法調用的類內部進行操做。
經過getter
方法搜索實例,例如get<Key>
, <key>
, is<Key>
, _<key>
的拼接方案。按照這個順序,若是發現符合的方法,就調用對應的方法並拿着結果跳轉到第五步。不然,就繼續到下一步。
若是沒有找到簡單的getter
方法,則搜索其匹配模式的方法countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
。
若是找到其中的第一個和其餘兩個中的一個,則建立一個集合代理對象,該對象響應全部NSArray
的方法並返回該對象。不然,繼續到第三步。
代理對象隨後將NSArray
接收到的countOf<Key>
、objectIn<Key>AtIndex:
、<key>AtIndexes:
的消息給符合KVC
規則的調用方。
當代理對象和KVC
調用方經過上面方法一塊兒工做時,就會容許其行爲相似於NSArray
同樣。
若是沒有找到NSArray
簡單存取方法,或者NSArray
存取方法組。則查找有沒有countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
命名的方法。
若是找到三個方法,則建立一個集合代理對象,該對象響應全部NSSet
方法並返回。不然,繼續執行第四步。
此代理對象隨後轉換countOf<Key>
、enumeratorOf<Key>
、memberOf<Key>:
方法調用到建立它的對象上。實際上,這個代理對象和NSSet
一塊兒工做,使得其表象上看起來是NSSet
。
若是沒有發現簡單getter
方法,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly
是返回YES
的。搜索一個名爲_<key>
、_is<Key>
、<key>
、is<Key>
的實例,根據他們的順序。
若是發現對應的實例,則馬上得到實例可用的值並跳轉到第五步,不然,跳轉到第六步。
若是取回的是一個對象指針,則直接返回這個結果。 若是取回的是一個基礎數據類型,可是這個基礎數據類型是被NSNumber
支持的,則存儲爲NSNumber
並返回。 若是取回的是一個不支持NSNumber
的基礎數據類型,則經過NSValue
進行存儲並返回。
若是全部狀況都失敗,則調用valueForUndefinedKey:
方法並拋出異常,這是默認行爲。可是子類能夠重寫此方法。
這是setValue:forKey:
的默認實現,給定輸入參數value
和key
。試圖在接收調用對象的內部,設置屬性名爲key
的value
,經過下面的步驟:
查找set<Key>:
或_set<Key>
命名的setter
,按照這個順序,若是找到的話,調用這個方法並將值傳進去(根據須要進行對象轉換)。
若是沒有發現一個簡單的setter
,可是accessInstanceVariablesDirectly
類屬性返回YES
,則查找一個命名規則爲_<key>
、_is<Key>
、<key>
、is<Key>
的實例變量。根據這個順序,若是發現則將value
賦值給實例變量。
若是沒有發現setter
或實例變量,則調用setValue:forUndefinedKey:
方法,並默認提出一個異常,可是一個NSObject
的子類能夠提出合適的行爲。
這是mutableArrayValueForKey:
的默認實現,給一個key
當作輸入參數。在接收訪問器調用的對象中,返回一個名爲key
的可變代理數組,這個代理數組就是用來響應外界KVO
的對象,經過下面的步驟進行查找:
查找一對方法insertObject:in<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
(至關於NSMutableArray
的原始方法insertObject:atIndex:
和removeObjectAtIndex:
)或者方法名是insert<Key>:atIndexes:
和remove<Key>AtIndexes:
(至關於NSMutableArray
的原始方法insertObjects:atIndexes:
和removeObjectsAtIndexes:
)。
若是找到最少一個insert
方法和最少一個remove
方法,則返回一個代理對象,來響應發送給NSMutableArray
的組合消息insertObject:in<Key>AtIndex:
、removeObjectFrom<Key>AtIndex:
、insert<Key>:atIndexes:
,和remove<Key>AtIndexes:
消息。
當對象接收一個mutableArrayValueForKey:
消息並實現可選替換方法,例如replaceObjectIn<Key>AtIndex:withObject:
或replace<Key>AtIndexes:with<Key>:
方法,代理對象會在適當的狀況下使用它們,以得到最佳性能。
若是對象沒有可變數組方法,查找一個替代方法,命名格式爲set<Key>:
。在這種狀況下,向mutableArrayValueForKey:
的原始響應者發送一個set<Key>:
消息,來返回一個代理對象來響應NSMutableArray
事件。
提示: 這一步描述的機制遠不如上一步有效,由於它可能重複建立新的集合對象,而不是修改現有的對象。所以,在本身設計的KVC
時應該儘可能避免它。
若是沒有可變數組的方法,也沒有找到訪問器,但接受響應的類accessInstanceVariablesDirectly
屬性返回YES
,則查找一個名爲_<key>
或<key>
的實例變量。
按照這個順序,若是找到實例變量,則返回一個代理對象。改對象將接收全部NSMutableArray
發送過來的消息,一般是NSMutableArray
或其子類。
若是全部狀況都失敗,則返回一個可變的集合代理對象。當它接收NSMutableArray
消息時,發送一個setValue:forUndefinedKey:
消息給接收mutableArrayValueForKey:
消息的原始對象。
這個setValue:forUndefinedKey:
的默認實現是提出一個NSUndefinedKeyException
異常,可是子類能夠重寫這個實現。
還有NSMutableSet
和NSMutableOrderedSet
兩種搜索模式,這兩種搜索模式和NSMutableArray
步驟相同,只是搜索和調用的方法不一樣。詳細的搜索方法均可以在KVC官方文檔中找到,再套用上面的流程便可理解。
根據上面KVC
查找規則的描述,咱們定義一個TestObject
類,並指定其餘setter
和getter
,以及合成爲其餘的成員變量,看KVC
是否可以找到屬性的對象並賦值。
@interface TestObject : NSObject { NSObject *_newObject; } @property (nonatomic, strong, setter=newSetObject:, getter=newObject) NSObject *object; @property (nonatomic, strong) NSObject *twoObject; @end @implementation TestObject @synthesize object = _newObject; @end
這裏對兩個屬性進行賦值,twoObject
屬性賦值沒有任何問題,而第二個屬性賦值則會致使Crash
。崩潰信息如上面所述拋出一個NSUnknownKeyException
異常,並提示沒有找到object
獲取方法和實例對象。
TestObject *object = [[TestObject alloc] init]; [object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(twoObject))]; [object setValue:[NSObject new] forKey:NSStringFromSelector(@selector(object))];
若是將object
改成newObject
則能夠解決這個問題,以此驗證上面的KVC
查找規則。
**根據上面KVC
的實現原理,咱們能夠看出KVC
的性能並不如直接訪問屬性快,雖然這個性能消耗是微乎其微的。**因此在使用KVC
的時候,建議最好不要手動設置屬性的setter
、getter
,這樣會致使搜索步驟變長。
並且儘可能不要用KVC
進行集合操做,例如NSArray
、NSSet
之類的,集合操做的性能消耗更大,並且還會建立沒必要要的對象。
根據上面的實現原理咱們知道,KVC
本質上是操做方法列表以及在內存中查找實例變量。咱們能夠利用這個特性訪問類的私有變量,例以下面在.m
中定義的私有成員變量和屬性,均可以經過KVC
的方式訪問。
這個操做對readonly
的屬性,@protected
的成員變量,均可以正常訪問。若是不想讓外界訪問類的成員變量,則能夠將accessInstanceVariablesDirectly
屬性賦值爲NO
。
TestObject.m文件 @interface TestObject () { NSObject *_objectOne; } @property (nonatomic, strong) NSObject *objectTwo; @end
KVC
在實踐中也有不少用處,例如UITabbar
或UIPageControl
這樣的控件,系統已經爲咱們封裝好了,可是對於一些樣式的改變並無提供足夠的API
,這種狀況就須要咱們用KVC
進行操做了。
能夠自定義一個UITabbar
對象,而後在內部建立本身想要的視圖,並經過layoutSubviews
方法在內部進行從新佈局。而後經過KVC
的方式,將UITabbarController
的tabbar
屬性替換爲自定義的類便可。
KVC
存在一個問題在於,由於傳入的key
或keyPath
是一個字符串,這樣很容易寫錯或者屬性自身修改後字符串忘記修改,這樣會致使Crash
。
能夠利用iOS
的反射機制來規避這個問題,經過@selector()
獲取到方法的SEL
,而後經過NSStringFromSelector()
將SEL
反射爲字符串。這樣在@selector()
中傳入方法名的過程當中,編譯器會有合法性檢查,若是方法不存在或未實現會報黃色警告。
[self valueForKey:NSStringFromSelector(@selector(object))];