iOS底層探索 -- KVC 底層原理分析
前言
在日常的開發中,在對數據進行處理中,常常使用三方框架將其轉換爲模型 (model)
,以方便使用點語法
進行調用。這些框架底層都是運用的KVC(Key-Value Coding)
,今天來探索一下KVC
底層的原理。
1. KVC(Key-Value Coding)初探
KVC
即Key-Value Coding
,翻譯過來就是鍵值編碼
,關於這個概念,具體可以查看Apple
的官方文檔
Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties
.【譯】鍵值編碼是由
NSKeyValueCoding
非正式協議啓用的一種機制,對象
採用這種機制來提供對其屬性的間接訪問。
當對象符合鍵值編碼時,可以通過簡潔,統一的消息傳遞接口通過字符串參數來訪問其屬性。這種間接訪問機制補充了實例變量及其關聯的訪問器方法提供的直接訪問。
當對象繼承自NSObject
時,可以通過簡潔的方法(setValue:forKey
和valueForKey
)實現以下操作:
- 訪問對象屬性,例如通過
getter valueForKey:
和setter setValue:forKey:
,用於通過名稱或鍵(參數化爲字符串)來訪問對象屬性。 - 操作集合屬性,比如:
NSArray
- 在集合對象上調用集合運算符
- 訪問非對象屬性,比如:
結構體
- 通過鍵路徑訪問屬性
2. KVC 深入
通常在我們給對象聲明屬性時,會有一下幾類:
-
屬性。這些是簡單的值,例如標量,字符串或布爾值。值對象(例如)
NSNumber
和其他不可變類型(例如)NSColor
也被視爲屬性。 -
一對一的關係。這些是具有自己屬性的
可變對象
。對象
的屬性可以更改,而無需更改對象本身。例如,銀行帳戶對象可能具有所有者屬性,該屬性是Person對象的實例,該對象本身具有地址屬性。所 有者的地址可以更改,而無需更改銀行帳戶持有的所有者
-
一對多關係。這些是集合對象。比如:
NSArray
和NSSet
2.1 訪問對象屬性
- 通過
valueForKey:
和setValue:ForKey:
來間接的獲取和設置屬性值
[person setValue:@"KC" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"酷C" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);
setValue:forKey
:- Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics
.
If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.
【譯】將相對於接收消息的對象的指定鍵的值設置爲給定值。
setValue:forKey:
自動實現代表標量和結構的自動包裝NSNumber
和NSValue
對象的默認實現,並將其分配給屬性。有關包裝和展開語義的詳細信息,請參見表示非對象值。
如果指定的鍵對應於接收setter調用的對象所不具有的屬性,則該對象向自身發送setValue:forUndefinedKey:
消息。的默認實現setValue:forUndefinedKey:
會引發NSUndefinedKeyException。
但是,子類可以重寫此方法以自定義方式處理請求。
valueForKey
:- Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.
【譯】返回由
key
參數命名的屬性的值。如果根據訪問者搜索模式中描述的規則找不到由鍵命名的屬性,則該對象向自身發送一條valueForUndefinedKey:
消息。的默認實現valueForUndefinedKey:
會引發一個NSUndefinedKeyException
,但是子類可以覆蓋此行爲,並更優雅地處理該情況。
valueForKeyPath:
和setValue:ForKeyPath:
在Storyboard
或xib
中使用KVC
在Storyboard
或 xib
中使用KeyPath
來修改控件的某個屬性,如下圖:
對View
的
layer.cornerRadius
進行修改,實現圓角,其實本質就是調用valueForKeyPath:
和 setValue:ForKeyPath:
開發中不建議使用這種方式在 Storyboard 或者 xib 中修改,會造成後人很難找到很難維護
valueForKeyPath
:Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.
【譯】
valueForKeyPath
: 返回相對於接收者的指定密鑰路徑的值。密鑰路徑序列中不符合特定鍵的鍵值編碼的任何對象(即,默認實現valueForKey:
無法找到訪問器方法)均會接收到valueForUndefinedKey:
消息。
setValue:forKeyPath
:Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.
setValue:forKeyPath
:在相對於接收器的指定鍵路徑處設置給定值。密鑰路徑序列中不符合特定鍵的鍵值編碼的任何對象都會收到一條setValue:forUndefinedKey:
消息。
通過valueForKeyPath
和setValue:forKeyPath
,可以對具有自己數據的可變對象設值,如下示例:
LGPerson *person = [[LGPerson alloc] init];
LGStudent *student = [[LGStudent alloc] init];
student.subject = @"iOSer";
person.student = student;
[person setValue:@"我最帥" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
查看官方文檔
dictionaryWithValuesForKeys
:Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.
【譯】返回相對於接收者的鍵數組的值。該方法調用
valueForKey:
數組中的每個鍵。返回的NSDictionary
值包含數組中所有鍵的值。
setValuesForKeysWithDictionary
:Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.
【譯】使用字典鍵識別屬性,以指定字典中的值設置接收器的屬性。默認實現
setValue:forKey:
爲每個鍵值對調用,設置時將nil
替換爲NSNull
。
NSDictionary* dict = @{
@"name":@"CC",
@"nick":@"KC",
@"subject":@"iOS",
@"age":@18,
@"length":@180
};
LGStudent *p = [[LGStudent alloc] init];
// 字典轉模型
[p setValuesForKeysWithDictionary:dict];
NSLog(@"%@",p);
// 鍵數組轉模型到字典
NSArray *array = @[@"name",@"age"];
NSDictionary *dic = [p dictionaryWithValuesForKeys:array];
NSLog(@"%@",dic);
需要注意的是:集合對象,如
NSArray
,NSSet
和NSDictionary
,不能包含nil
的值。而是nil
使用NSNull
對象表示值。NSNull
提供一個代表nil
對象屬性值的實例。
實現dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
會自動在NSNull
(在dictionary
參數中)和nil
(在存儲屬性中)之間相互轉換。
2.2 訪問集合屬性
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);
2.3 集合運算符
在使用valueForKeyPath:
發送消息時,可以在鍵路徑中嵌入集合運算符
路徑格式:
1.left key path:指向的要進行運算的集合,如果是直接給集合發送的 valueForKeyPath: 消息,left
key path 可以省略
2.right key path:指定了運算符應在集合中進行操作的屬性。除之外,所有集合運算符都@count需要正確的密鑰路徑
集合運算符可以分爲三種:
-
聚合運算符
@avg
將指定的屬性將其轉換爲double(用0代替nil值)
,並計算這些值的算術平均值。然後,以NSNumber
形式返回
NSNumber *transactionAverage = [self.transactions valueForKeyPath:@"@avg.amount"];
@count
返回操作對象指定屬性的個數
NSNumber *numberOfTransactions = [self.transactions valueForKeyPath:@"@count"];
@max
返回指定屬性的最大值
NSDate *latestDate = [self.transactions valueForKeyPath:@"@max.date"];
@min
返回指定屬性的最小值
NSDate *earliestDate = [self.transactions valueForKeyPath:@"@min.date"];
@sum
返回指定屬性的之和
NSNumber *amountSum = [self.transactions valueForKeyPath:@"@sum.amount"];
-
數組運算符
@distinctUnionOfObjects
返回操作對象指定屬性的集合–去重
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
@unionOfObjects
返回操作對象指定的屬性的集合
NSArray *payees = [self.transactions valueForKeyPath:@"@unionOfObjects.payee"];
-
嵌套運算符
@distinctUnionOfArrays
返回一個數組
,該數組包含操作對象(嵌套集合)指定屬性–去重@unionOfArrays
返回一個數組
,該數組包含操作對象指定的屬性,不去重。@distinctUnionOfSets
交集 返回一個NSSet
,該NSSet
包含操作對象(嵌套集合)指定屬性–去重
2.4 訪問非對象屬性
非對像屬性包含兩種形式:基本數據類型,結構體(struct
)
- 訪問基本數據類型
如圖:常用的基本數據類型需要在設置屬性的時候包裝成 NSNumber
類型,然後在讀取值的時候使用各自對應的讀取方法,
如: double 類型的標量讀取的時候使用 doubleValue
- 結構體
如圖所示:NSPoint
、NSRange
、NSRect
和NSSize
需要轉換成 NSValue
類型,對於自定義的結構體,也需要進行 NSValue
的轉換操作。如下示例:
typedef struct {
float x, y, z;
} ThreeFloats;
@interface MyClass
@property (nonatomic) ThreeFloats threeFloats;
@end
NSValue* result = [myClass valueForKey:@"threeFloats"];
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"%f - %f - %f",th.x,th.y,th.z);
ThreeFloats floats = {1., 2., 3.};
NSValue* value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[myClass setValue:value forKey:@"threeFloats"];
2.5 屬性驗證
調用validateValue:forKey:error:
方法或者alidateValue:forKeyPath:error:
方法進行屬性驗證。
會在接收驗證消息的對象
中查找與之匹配的validate<Key>:error:
方法,如果沒有這個方法,則驗證成功,返回YES
。當存在特定於屬性的驗證方法時,默認實現將返回調用該方法的結果。
簡單的說,就是防止對錯誤的key
賦值,也可以判斷特定的key
,進行重定向爲另一個key
。
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError{
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"裏面修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的屬性",inKey,self] code:10088 userInfo:nil];
return NO;
}
2.6 KVC 取值和賦值原理
-
賦值原理
- 依次判斷是否有
set<Key>
或者_set<Key>
或者setIs<key>
的方法。如果找到,直接調用,沒有,則進入第 2 步。 - 判斷
accessInstanceVariablesDirectly
方法,是否開啓實例變量賦值,若返回YES
,則進入步驟3,返回NO
,則進去步驟4。 - 依次尋找類似於
_<key>
、_is<Key>
、<key>
或者is<Key>
的實例變量。如果找到,直接設置變量。找不到,則進去步驟4。 - 找不到訪問器或實例變量後,調用
setValue:forUndefinedKey:
,報錯,引發異常。
- 依次判斷是否有
-
取值原理
-
依次判斷是否有訪問方法
get<Key>
、<key>
、is<Key>
或者_<key>
,如果找到,則執行步驟 6進行細節處理。否則,請繼續下一步。 -
判斷是否是
NSArray
, -
判斷是否是
NSSet
-
判斷
accessInstanceVariablesDirectly
方法,是否開啓實例變量賦值,若返回YES
,則進入步驟5,返回NO
,則進去步驟7 -
依次查找名爲
_<key>
、_is<Key>
、<key>
或者is<Key>
的實例變量,如果找到,直接獲取值,找不到則直接執行步驟 6。 -
如果檢索到的屬性值是對象指針,則只需返回結果,
如果該值是所支持的標量類型
NSNumber
,則將其存儲在NSNumber
實例中並返回如果結果是
NSNumber
不支持的標量類型,請轉換爲NSValue
對象並返回該對象 -
調用
valueForUndefinedKey:
,報錯,引發異常
-