KVC原理剖析

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> https://www.jianshu.com/p/1d39bc610a5bhtml


在工做中常常會使用到KVC,可是不少人對於KVC的實現原理並不太清楚。好比說KVC在進行存取時,是怎麼進行查找並賦值的。ios

網上有不少講KVC的文章,可是有不少質量並不高。這兩天抽空把我所理解的KVC寫出來,當作學習交流,正好也讓各位大神幫我指正一下,十分感謝!macos


博客配圖

協議定義

KVC全稱是Key Value Coding,定義在NSKeyValueCoding.h文件中,是一個非正式協議KVC提供了一種間接訪問其屬性方法或成員變量的機制,能夠經過字符串來訪問對應的屬性方法或成員變量。數組

NSKeyValueCoding Protocol

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"];
複製代碼

keyPath

**除了對當前對象的屬性進行賦值外,還能夠對其更「深層」的對象進行賦值。**例如對當前對象的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:方法時,能夠傳入一個包含keyvalue的字典進去,KVC能夠將全部數據按照屬性名和字典的key進行匹配,並將valueUser對象的屬性賦值。

- (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的轉換。須要注意的是,NSArrayNSDictionary等集合對象,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方法,在其內容發生改變時發送消息。但這只是對屬性直接進行賦值會觸發,若是屬性是容器對象,對容器對象進行addremove操做,則不會調用KVO的方法。能夠經過KVC對應的API來配合使用,使容器對象內部發生改變時也能觸發KVO

在進行容器對象操做時,先調用下面方法經過key或者keyPath獲取集合對象,而後再對容器對象進行addremove等操做時,就會觸發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。(集合對象主要指NSArrayNSSet,但不包括NSDictionary)

集合運算符格式

上面表達式主要分爲三部分,left部分是要操做的集合對象,若是調用KVC的對象原本就是集合對象,則left能夠爲空。中間部分是表達式,表達式通常以@符號開頭。後面是進行運算的屬性。

集合運算符主要分爲三類:

  1. 集合操做符:處理集合包含的對象,並根據操做符的不一樣返回不一樣的類型,返回值以NSNumber爲主。
  2. 數組操做符:根據操做符的條件,將符合條件的對象包含在數組中返回。
  3. 嵌套操做符:處理集合對象中嵌套其餘集合對象的狀況,返回結果也是一個集合對象。

example

下面是爲了方便模擬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
複製代碼

集合操做符

集合操做符處理NSArrayNSSet及其子類這樣的集合對象,並根據不一樣的操做符返回不一樣類型的對象,返回值通常都是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是支持基礎數據類型和結構體的,能夠在settergetter的時候,經過NSValueNSNumber來轉換爲OC對象。Swift中不存在這樣的需求,由於Swift中全部變量都是對象。

如下是結構體轉換的示例代碼,能夠調用initWithBool:方法對基礎數據類型進行包裝,除了調用方法外還能夠經過字面量實現,例如@(YES)的調用。經過NSNumberboolValue屬性轉換爲基礎數據類型。

@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value NS_DESIGNATED_INITIALIZER;
複製代碼

結構體轉換的代碼定義在UIGeometry.h中,以NSValueCategory形式存在。NSValueCGPointCGRect等結構體都提供了轉換方法,例以下面是對CGPoint進行轉換的示例代碼。

@property(nonatomic, assign, readonly) CGPoint CGPointValue;
+ (NSValue *)valueWithCGPoint:(CGPoint)point;
複製代碼

須要注意的是,不管何時都不該該給setter中傳入nil,會致使Crash並引發NSInvalidArgumentException異常。

屬性驗證

在調用KVC時能夠先進行驗證,驗證經過下面兩個方法進行,支持keykeyPath兩種方式。驗證方法默認實現返回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方法的內部實現中,若是傳入的valuekey有問題,能夠經過返回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自動驗證,在調用setValuegetValue時自動進行驗證,若是不符合驗證規則,就調用失敗。若是外界使用的地方都先調用一次validateValue的話,這是很麻煩的。固然也有解決方法,能夠經過Method Swizzling方法hooksetValuegetValue方法。

搜索規則

KVC在經過key或者keyPath進行操做的時候,能夠查找屬性方法、成員變量等,查找的時候能夠兼容多種命名。 具體的查找規則要以官方文檔爲主,因此我把官方文檔翻譯了一下寫在下面。

KVC的實現中,依賴settergetter的方法實現,因此方法命名應該符合蘋果要求的規範,不然會致使KVC失敗。

在學習KVC的搜索規則前,要先弄明白一個屬性的做用,這個屬性在搜索過程當中起到很重要的做用。這個屬性表示是否容許讀取實例變量的值,若是爲YES則在KVC查找的過程當中,從內存中讀取屬性實例變量的值。

@property (class, readonly) BOOL accessInstanceVariablesDirectly;
複製代碼

基礎Getter搜索模式

這是valueForKey:的默認實現,給定一個key當作輸入參數,開始下面的步驟,在這個接收valueForKey:方法調用的類內部進行操做。

  1. 經過getter方法搜索實例,例如get<Key>, <key>, is<Key>, _<key>的拼接方案。按照這個順序,若是發現符合的方法,就調用對應的方法並拿着結果跳轉到第五步。不然,就繼續到下一步。

  2. 若是沒有找到簡單的getter方法,則搜索其匹配模式的方法countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:

    若是找到其中的第一個和其餘兩個中的一個,則建立一個集合代理對象,該對象響應全部NSArray的方法並返回該對象。不然,繼續到第三步。

    代理對象隨後將NSArray接收到的countOf<Key>objectIn<Key>AtIndex:<key>AtIndexes:的消息給符合KVC規則的調用方。

    當代理對象和KVC調用方經過上面方法一塊兒工做時,就會容許其行爲相似於NSArray同樣。

  3. 若是沒有找到NSArray簡單存取方法,或者NSArray存取方法組。則查找有沒有countOf<Key>enumeratorOf<Key>memberOf<Key>:命名的方法。

    若是找到三個方法,則建立一個集合代理對象,該對象響應全部NSSet方法並返回。不然,繼續執行第四步。

    此代理對象隨後轉換countOf<Key>enumeratorOf<Key>memberOf<Key>:方法調用到建立它的對象上。實際上,這個代理對象和NSSet一塊兒工做,使得其表象上看起來是NSSet

  4. 若是沒有發現簡單getter方法,或集合存取方法組,以及接收類方法accessInstanceVariablesDirectly是返回YES的。搜索一個名爲_<key>_is<Key><key>is<Key>的實例,根據他們的順序。

    若是發現對應的實例,則馬上得到實例可用的值並跳轉到第五步,不然,跳轉到第六步。

  5. 若是取回的是一個對象指針,則直接返回這個結果。 若是取回的是一個基礎數據類型,可是這個基礎數據類型是被NSNumber支持的,則存儲爲NSNumber並返回。 若是取回的是一個不支持NSNumber的基礎數據類型,則經過NSValue進行存儲並返回。

  6. 若是全部狀況都失敗,則調用valueForUndefinedKey:方法並拋出異常,這是默認行爲。可是子類能夠重寫此方法。

基礎Setter搜索模式

這是setValue:forKey:的默認實現,給定輸入參數valuekey。試圖在接收調用對象的內部,設置屬性名爲keyvalue,經過下面的步驟:

  1. 查找set<Key>:_set<Key>命名的setter,按照這個順序,若是找到的話,調用這個方法並將值傳進去(根據須要進行對象轉換)。

  2. 若是沒有發現一個簡單的setter,可是accessInstanceVariablesDirectly類屬性返回YES,則查找一個命名規則爲_<key>_is<Key><key>is<Key>的實例變量。根據這個順序,若是發現則將value賦值給實例變量。

  3. 若是沒有發現setter或實例變量,則調用setValue:forUndefinedKey:方法,並默認提出一個異常,可是一個NSObject的子類能夠提出合適的行爲。

NSMutableArray搜索模式

這是mutableArrayValueForKey:的默認實現,給一個key當作輸入參數。在接收訪問器調用的對象中,返回一個名爲key的可變代理數組,這個代理數組就是用來響應外界KVO的對象,經過下面的步驟進行查找:

  1. 查找一對方法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>:方法,代理對象會在適當的狀況下使用它們,以得到最佳性能。

  2. 若是對象沒有可變數組方法,查找一個替代方法,命名格式爲set<Key>:。在這種狀況下,向mutableArrayValueForKey:的原始響應者發送一個set<Key>:消息,來返回一個代理對象來響應NSMutableArray事件。

    提示: 這一步描述的機制遠不如上一步有效,由於它可能重複建立新的集合對象,而不是修改現有的對象。所以,在本身設計的KVC時應該儘可能避免它。

  3. 若是沒有可變數組的方法,也沒有找到訪問器,但接受響應的類accessInstanceVariablesDirectly屬性返回YES,則查找一個名爲_<key><key>的實例變量。

    按照這個順序,若是找到實例變量,則返回一個代理對象。改對象將接收全部NSMutableArray發送過來的消息,一般是NSMutableArray或其子類。

  4. 若是全部狀況都失敗,則返回一個可變的集合代理對象。當它接收NSMutableArray消息時,發送一個setValue:forUndefinedKey:消息給接收mutableArrayValueForKey:消息的原始對象。

    這個setValue:forUndefinedKey:的默認實現是提出一個NSUndefinedKeyException異常,可是子類能夠重寫這個實現。

其餘搜索模式

還有NSMutableSetNSMutableOrderedSet兩種搜索模式,這兩種搜索模式和NSMutableArray步驟相同,只是搜索和調用的方法不一樣。詳細的搜索方法均可以在KVC官方文檔中找到,再套用上面的流程便可理解。

代碼示例

根據上面KVC查找規則的描述,咱們定義一個TestObject類,並指定其餘settergetter,以及合成爲其餘的成員變量,看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的性能並不如直接訪問屬性快,雖然這個性能消耗是微乎其微的。 因此在使用KVC的時候,建議最好不要手動設置屬性的settergetter,這樣會致使搜索步驟變長。

並且儘可能不要用KVC進行集合操做,例如NSArrayNSSet之類的,集合操做的性能消耗更大,並且還會建立沒必要要的對象。

私有訪問

根據上面的實現原理咱們知道,KVC本質上是操做方法列表以及在內存中查找實例變量。咱們能夠利用這個特性訪問類的私有變量,例以下面在.m中定義的私有成員變量和屬性,均可以經過KVC的方式訪問。

這個操做對readonly的屬性,@protected的成員變量,均可以正常訪問。若是不想讓外界訪問類的成員變量,則能夠將accessInstanceVariablesDirectly屬性賦值爲NO

TestObject.m文件

@interface TestObject () {
    NSObject *_objectOne;
}
@property (nonatomic, strong) NSObject *objectTwo;
@end
複製代碼

KVC在實踐中也有不少用處,例如UITabbarUIPageControl這樣的控件,系統已經爲咱們封裝好了,可是對於一些樣式的改變並無提供足夠的API,這種狀況就須要咱們用KVC進行操做了。

新浪微博

能夠自定義一個UITabbar對象,而後在內部建立本身想要的視圖,並經過layoutSubviews方法在內部進行從新佈局。而後經過KVC的方式,將UITabbarControllertabbar屬性替換爲自定義的類便可。

安全性檢查

KVC存在一個問題在於,由於傳入的keykeyPath是一個字符串,這樣很容易寫錯或者屬性自身修改後字符串忘記修改,這樣會致使Crash

能夠利用iOS的反射機制來規避這個問題,經過@selector()獲取到方法的SEL,而後經過NSStringFromSelector()SEL反射爲字符串。這樣在@selector()中傳入方法名的過程當中,編譯器會有合法性檢查,若是方法不存在或未實現會報黃色警告。

[self valueForKey:NSStringFromSelector(@selector(object))];
複製代碼
相關文章
相關標籤/搜索