iOS底層探索 -- KVC 底層原理分析

前言

  在日常的開發中,在對數據進行處理中,常常使用三方框架將其轉換爲模型 (model),以方便使用點語法進行調用。這些框架底層都是運用的KVC(Key-Value Coding),今天來探索一下KVC底層的原理。

1.   KVC(Key-Value Coding)初探

   KVCKey-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:forKeyvalueForKey)實現以下操作:

  • 訪問對象屬性,例如通過getter valueForKey:setter setValue:forKey:,用於通過名稱或鍵(參數化爲字符串)來訪問對象屬性。
  • 操作集合屬性,比如:NSArray
  • 在集合對象上調用集合運算符
  • 訪問非對象屬性,比如:結構體
  • 通過鍵路徑訪問屬性

2.   KVC 深入

通常在我們給對象聲明屬性時,會有一下幾類:

  • 屬性。這些是簡單的值,例如標量,字符串或布爾值。值對象(例如)NSNumber和其他不可變類型(例如)NSColor也被視爲屬性。

  • 一對一的關係。這些是具有自己屬性的可變對象對象的屬性可以更改,而無需更改對象本身。

      例如,銀行帳戶對象可能具有所有者屬性,該屬性是Person對象的實例,該對象本身具有地址屬性。所
      有者的地址可以更改,而無需更改銀行帳戶持有的所有者
    
  • 一對多關係。這些是集合對象。比如:NSArrayNSSet

2.1  訪問對象屬性

  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:自動實現代表標量和結構的自動包裝NSNumberNSValue對象的默認實現,並將其分配給屬性。有關包裝和展開語義的詳細信息,請參見表示非對象值。
如果指定的鍵對應於接收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,但是子類可以覆蓋此行爲,並更優雅地處理該情況。

  1. valueForKeyPath:setValue:ForKeyPath:Storyboardxib 中使用 KVC

Storyboardxib中使用KeyPath來修改控件的某個屬性,如下圖:

View
layer.cornerRadius進行修改,實現圓角,其實本質就是調用valueForKeyPath:setValue:ForKeyPath:

開發中不建議使用這種方式在 Storyboard 或者 xib 中修改,會造成後人很難找到很難維護
  • valueForKeyPathReturns 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:forKeyPathSets 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:消息。

通過valueForKeyPathsetValue: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"]);
  1. dictionaryWithValuesForKeys:setValuesForKeysWithDictionary:

查看官方文檔

  • dictionaryWithValuesForKeysReturns 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值包含數組中所有鍵的值。

  • setValuesForKeysWithDictionarySets 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);

需要注意的是:集合對象,如NSArrayNSSetNSDictionary,不能包含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
  • 結構體

如圖所示:NSPointNSRangeNSRectNSSize需要轉換成 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 取值和賦值原理

  • 賦值原理

    1. 依次判斷是否有set<Key>或者_set<Key>或者setIs<key>的方法。如果找到,直接調用,沒有,則進入第 2 步。
    2. 判斷accessInstanceVariablesDirectly 方法,是否開啓實例變量賦值,若返回YES,則進入步驟3,返回NO,則進去步驟4
    3. 依次尋找類似於_<key>_is<Key><key>或者is<Key>的實例變量。如果找到,直接設置變量。找不到,則進去步驟4
    4. 找不到訪問器或實例變量後,調用setValue:forUndefinedKey:,報錯,引發異常。
  • 取值原理

    1. 依次判斷是否有訪問方法get<Key><key>is<Key>或者_<key>,如果找到,則執行步驟 6進行細節處理。否則,請繼續下一步。

    2. 判斷是否是NSArray

    3. 判斷是否是NSSet

    4. 判斷accessInstanceVariablesDirectly 方法,是否開啓實例變量賦值,若返回YES,則進入步驟5,返回NO,則進去步驟7

    5. 依次查找名爲_<key>_is<Key><key>或者is<Key>的實例變量,如果找到,直接獲取值,找不到則直接執行步驟 6

    6. 如果檢索到的屬性值是對象指針,則只需返回結果,

      如果該值是所支持的標量類型NSNumber,則將其存儲在NSNumber實例中並返回

      如果結果是NSNumber不支持的標量類型,請轉換爲NSValue對象並返回該對象

    7. 調用valueForUndefinedKey:,報錯,引發異常

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章