帶你玩轉KVC的底層實現


一.探索前需知
html

1.1 什麼是KVCapi

KVC(Key-value coding)鍵值編碼,就是指iOS的開發中,能夠容許開發者經過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不須要調用明確的存取方法。這樣就能夠在運行時動態地訪問和修改對象的屬性。而不是在編譯時肯定,這也是iOS開發中的黑魔法之一。不少高級的iOS開發技巧都是基於KVC實現的.數組

二.KVC的初探(KVC 的常見使用場景)bash

2.1  KVC對象屬性的設值:

在LGPerson.h裏:
app

KVC不管獲取值仍是賦值,只須要傳入屬性名稱的字符串就好了。但KVC也提供了傳入 path的方法。所謂path,就是用點號鏈接的多層級的屬性,好比 student.name,student屬性裏的name屬性。

LGPerson *person = [[LGPerson alloc] init];

    // 1:Key-Value Coding (KVC) : 基本類型

    [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"]);

    // 5:KVC - keypath 層層訪問

    LGStudent *student = [[LGStudent alloc] init];

    student.subject    = @"iOS";

    person.student     = student;

    [person setValue:@"大師班" forKeyPath:@"student.subject"];

    NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
複製代碼
 2.2  KVC - 訪問非對象屬性

若是想要訪問對象屬性(基本數據類型、結構體等)應該用NSValue 進行層包裝,如:ide

// 4:KVC - 訪問非對象屬性
    ThreeFloats floats = {1., 2., 3.};
    NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
    [person setValue:value forKey:@"threeFloats"];
    NSValue *reslut = [person valueForKey:@"threeFloats"];
    NSLog(@"%@",reslut);
    
    ThreeFloats th;
    [reslut getValue:&th] ;
    NSLog(@"%f - %f - %f",th.x,th.y,th.z);
複製代碼

2.3  KVC 修改對象裏的集合類型性能

person.array = @[@"1",@"2",@"3"];
    // 因爲不是可變數組 - 沒法作到
    // person.array[0] = @"100";
    NSArray *array = [person valueForKey:@"array"];
    // 用 array 的值建立一個新的數組
    array = @[@"100",@"2",@"3"];
    [person setValue:array forKey:@"array"];
    NSLog(@"%@",[person valueForKey:@"array"]);
    
    // KVC 的方式
    NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
    ma[0] = @"100";
    NSLog(@"%@",[person valueForKey:@"array"]);
複製代碼

2.4 KVC字典轉模型操做ui

- (void)dictionaryTest{
    
    NSDictionary* dict = @{
                           @"name":@"Cooci",
                           @"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);
}
複製代碼

三. KVC 的進階this

   上面說了那麼多,只是介紹了KVC的概念,已經平時開發中常常用到的使用場景,可是KVC的底層究竟是如何進行的呢.(以前的博客文章,基本用源碼來分析底層實現).可是KVC setValue:forKey: 會調用底層objc_setProperty_nonatomic方法,而這個方法是在LLVM彙編的時候執行的,因此對於一些不瞭解彙編指令的盆友就不太友好, 因此咱們就換個思路經過閱讀蘋果官方文檔來探索KVC的底層實現.編碼

首先打開蘋果開發者官網:(https://developer.apple.com/documentation/)

下面有個 Documentation Archive,點擊進入:


Documents裏輸入KeyValueCoding:

(https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/SearchImplementation.html#//apple_ref/doc/uid/20000955-CJBBBFFA)

Gettering Started裏是關於KVC的介紹

About Key-Value Coding

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.

(鍵值編碼是由NSKeyValueCoding非正式協議啓用的一種機制,對象採用該協議提供對其屬性的間接訪問。當一個對象與鍵值編碼兼容時,它的屬性能夠經過一個簡潔、統一的消息傳遞接口經過字符串參數尋址。這種間接訪問機制補充了實例變量及其相關訪問器方法提供的直接訪問)

而後看到下面Accessor Search Patterns: 

3.1 Search Pattern for the Basic Setter

The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:

  1. Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.

  2. If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.

  3. Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

    文檔給出的意思就是:

基本設置程序的搜索模式 setValue:for key:的默認實現是,給定鍵和值參數做爲輸入,嘗試在接收調用的對象內將名爲key的屬性設置爲value(或者,對於非對象屬性,使用如下過程將值的未包裝版本設置爲value,如表示非對象值中所述):
  1.  按此順序查找第一個名爲set<Key>或_set<Key>的訪問器。若是找到,使用輸入值(或者根據須要使用unwrapped值)調用它並完成。(其實還有個 setIs<Key>) 
  2. 若是找不到簡單的訪問器,而且類方法accessInstanceVariablesDirectly返回YES,則按該順序查找名爲「_<key>」、「_is<Key>」、「<key>」或「is<Key>」的實例變量。若是找到,直接用輸入值(或未包裝值)設置變量並完成。 
  3. 在找不到訪問器或實例變量時,調用setValue:forUndefinedKey:。默認狀況下,這會引起異常,但NSObject的子類可能提供特定於鍵的行爲。

看到這你們應該一目瞭然,那麼下面咱們就來驗證下:

LGPerson *person = [[LGPerson alloc] init];
  // 1: KVC - 設置值的過程
  [person setValue:@"LG_Cooci" forKey:@"name"];
複製代碼

咱們再看下LGPerson類

LGPerson.h:

LGPerson.m:

#import "LGPerson.h"

@implementation LGPerson

#pragma mark - 關閉或開啓實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
@end
複製代碼

運行代碼看輸出:2020-02-13 21:23:34.799806+0800 002-KVC取值&賦值過程[43413:4290933] -[LGPerson setName:] - LG_Cooci.

接着把 - (void)setName:(NSString *)name 方法註釋掉,再運行代碼看輸出:

2020-02-13 21:29:34.280334+0800 002-KVC取值&賦值過程[43433:4294790] -[LGPerson _setName:] - LG_Cooci.

接着把 - (void)setName:(NSString *)name 方法註釋掉,再運行代碼看輸出:

2020-02-13 21:31:26.922422+0800 002-KVC取值&賦值過程[43451:4296659] -[LGPerson setIsName:] - LG_Cooci

說明setValue:for key:的默認實現第一步確實是訪問setKey, 若是setKey 沒實現就去訪問_setKey,若是_setKey 沒實現就去訪問setIsKey.(依次進行的)

若是第一步中訪問器都沒實現會怎麼樣呢?

那就會來到第二步:若是找不到簡單的訪問器,而且類方法accessInstanceVariablesDirectly返回YES,則按該順序查找名爲「_<key>」、「_is<Key>」、「<key>」或「is<Key>」的實例變量。若是找到,直接用輸入值(或未包裝值)設置變量並完成。 

咱們也來驗證一下:

類方法accessInstanceVariablesDirectly要返回YES, 我就返回個NO.看看會出現啥問題,

#import "LGPerson.h"

@implementation LGPerson

#pragma mark - 關閉或開啓實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return NO;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}
複製代碼

發現程序會崩潰,'NSUnknownKeyException', reason: '[<LGPerson 0x6000004c8300> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'

原來accessInstanceVariablesDirectly 是判斷關閉或開啓實例變量賦值,你只有返回YES,它纔可以訪問實例變量.

LGPerson.h:

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}

@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) NSSet   *set;

@property (nonatomic, strong) NSMutableArray        *namesArrM;
@property (nonatomic, strong) NSMutableSet          *namesSetM;
@property (nonatomic, strong) NSMutableOrderedSet   *orderedSetM;
@end
複製代碼

LGPerson.m:

#import "LGPerson.h"

@implementation LGPerson

#pragma mark - 關閉或開啓實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

@end複製代碼

ViewController裏代碼:

LGPerson *person = [[LGPerson alloc] init];    
 // 1: KVC - 設置值的過程
[person setValue:@"LG_Cooci" forKey:@"name"];  
 NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);
// NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
// NSLog(@"%@-%@",person->name,person->isName);
// NSLog(@"%@",person->isName);
複製代碼

運行看結果:2020-02-13 21:50:56.090657+0800 002-KVC取值&賦值過程[43521:4311589] LG_Cooci-(null)-(null)-(null),看來只能訪問_name 實例變量有值,說明setValue 優先找的是_name 實例變量.

接着咱們LGPerson _name實例變量註釋掉

而後輸出NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);其它註釋看啥結果:2020-02-13 21:56:58.163683+0800 002-KVC取值&賦值過程[43558:4316299] LG_Cooci-(null)-(null). 看來_name 實例變量沒有隻能訪問_isName 實例變量有值,說明_name 實例變量沒有 setValue  優先找的是 _isName 實例變量.

接下來一個思路註釋掉 _isName 實例變量 在進行打印輸出......最後的最後得出的結論就是:

若是找不到簡單的訪問器,而且類方法accessInstanceVariablesDirectly返回YES,則按該順序查找名爲「_<key>」、「_is<Key>」、「<key>」或「is<Key>」的實例變量。若是找到,直接用輸入值(或未包裝值)設置變量並完成。和官網文檔說的是一模模同樣樣的.

至於第三步應該不用驗證了吧,相信朋友們都和熟悉了吧就是:在找不到訪問器或實例變量時,調用setValue:forUndefinedKey:。默認狀況下,這會引起異常,但NSObject的子類可能提供特定於鍵的行爲. (咱們剛開始開發時,常常會遇到這個異常錯誤).

3.2 Search Pattern for the Basic Getter

The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.

  1. Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.

  2. If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).

    If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.

    The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.

  3. If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).

    If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.

    This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.

  4. If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.

  5. If the retrieved property value is an object pointer, simply return the result.

    If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.

    If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.

  6. If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.

1. 在實例中搜索找到的第一個訪問器方法,該方法的名稱如get<Key>、<key>、is<Key>、或者_<key>。若是找到,則調用它並繼續執行步驟5並返回結果。不然繼續下一步。

話很少說,開始驗證:

LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 設置值的過程
     [person setValue:@"LG_Cooci" forKey:@"name"];
    // 2: KVC - 取值的過程
    // person->_name = @"_name";
    // person->_isName = @"_isName";
    // person->name = @"name";
    // person->isName = @"isName";

     NSLog(@"取值:%@",[person valueForKey:@"name"]);
複製代碼

LGPerson.m:

//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
複製代碼

運行代碼,看控制檯打印:

2020-02-14 10:36:31.988665+0800 002-KVC取值&賦值過程[45573:4395417] 取值:getName. 說明valueForKey 過程當中首先訪問訪問器方法get<Key>.

接着把方法- (NSString *)getName註釋掉,在運行程序,後臺打印以下:

2020-02-14 10:40:06.743185+0800 002-KVC取值&賦值過程[45614:4414017] 取值:name.說明實例方法get<Key>不存在,會訪問訪問器方法<key>.

而後再把方法- (NSString *)name註釋掉,在運行程序......

最後得出告終論:執行valueForKey會在實例中搜索找到的第一個訪問器方法,該方法的名稱如get<Key>、<key>、is<Key>、或者_<key>.而後跳轉第五步.

5.若是該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中並返回該實例。 若是結果是NSNumber不支持的標量類型,則轉換爲NSValue對象並返回該對象。

接着咱們來看第二步和第三步:

2.若是找不到簡單的訪問器方法,請在實例中搜索名稱與模式countOf<Key>和objectIn<Key>AtIndex:(對應於NSArray類定義的基元方法)和<Key>AtIndex:(對應於NSArray方法objectsAtIndexes:)匹配的方法。 若是找到其中的第一個和其餘兩個方法中的至少一個,則建立一個響應全部NSArray方法的集合代理對象並返回該對象。不然,繼續執行步驟3。 代理對象隨後將其接收到的任何NSArray消息轉換爲countOf<Key>、objectIn<Key>AtIndex:、和<Key>AtIndexes:消息的組合,並將其轉換爲建立該對象的鍵值編碼兼容對象。若是原始對象還實現了一個名爲get<Key>:range:(get<Key>:range:)的可選方法,則代理對象在適當時也會使用該方法。實際上,代理對象與密鑰值編碼兼容對象一塊兒工做,容許底層屬性的行爲如同它是NSArray,即便它不是NSArray。

3. 若是找不到簡單的訪問器方法或數組訪問方法組,請查找名爲countOf<Key>、enumeratorOf<Key>和memberOf<Key>的三個方法(對應於NSSet類定義的基本方法)。 若是找到這三個方法,則建立一個集合代理對象,該對象響應全部NSSet方法並返回該對象。不然,繼續執行步驟4。 此代理對象隨後將接收到的任何NSSet消息轉換爲countOf<Key>、enumeratorOf<Key>和memberOf<Key>:消息的組合,並將其轉換爲建立它的對象。實際上,代理對象與鍵值編碼兼容對象一塊兒工做,容許底層屬性的行爲如同它是NSSet,即便它不是NSSet。

什麼意思呢?解釋一下:

LGPerson *person = [[LGPerson alloc] init];
    // 3: KVC - 集合類型
  person.arr = @[@"pen0", @"pen1", @"pen2", @"pen3"];
  NSArray *array = [person valueForKey:@"pens"];
  NSLog(@"%@",[array objectAtIndex:1]);
  NSLog(@"%d",[array containsObject:@"pen1"]);
複製代碼

[person valueForKey:@"pens"] 首先會去搜索訪問器方法- get<Key>, <key>, is<Key>, or _<key>. 可是在這裏咱們沒有去實現.

它會去搜索- (NSUInteger)countOfPens 和 - (id)objectInPensAtIndex:(NSUInteger)index這兩個方法, 看是否有實現.

//MARK: - 集合類型的走

// 個數
- (NSUInteger)countOfPens{
    NSLog(@"%s",__func__);
    return [self.arr count];
}

// 獲取值
- (id)objectInPensAtIndex:(NSUInteger)index {
    NSLog(@"%s",__func__);
    return [NSString stringWithFormat:@"pens %lu", index];
}
複製代碼

當array調用objectAtIndex 會調用到objectInPensAtIndex這裏來,咱們來看下NSLog(@"%@",[array objectAtIndex:1]).

輸出:2020-02-14 11:03:00.250697+0800 002-KVC取值&賦值過程[45706:4427980] pens 1.

而array調用count 會調用到countOfPens這裏來 NSLog(@"%lu",(unsigned long)[array count]);

輸出:2020-02-14 11:13:38.404264+0800 002-KVC取值&賦值過程[45741:4436685] 4

-get<Key>:range://不是必須實現的,但實現後能夠提升性能,其對應於 NSArray 方法 getObjects:range.

若是在上面若是找不到簡單的訪問器方法或數組訪問方法組,那麼會同時查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。若是這三個方法都找到,那麼就返回一個能夠響應NSSet所的方法的代理集合,和上面同樣,給這個代理集合發NSSet的消息,就會以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調用.

// set 集合
    person.set = [NSSet setWithArray:person.arr];
    NSSet *set = [person valueForKey:@"books"];
    [set enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
        NSLog(@"set遍歷 %@",obj);
    }];
複製代碼

LGPerson.m:

//MARK: - set

// 個數

- (NSUInteger)countOfBooks{

    NSLog(@"%s",__func__);

    return [self.set count];
}

// 是否包含這個成員對象

- (id)memberOfBooks:(id)object {

    NSLog(@"%s",__func__);

    return [self.set containsObject:object] ? object : nil;
}

// 迭代器

- (id)enumeratorOfBooks {

    // objectEnumerator

    NSLog(@"來了 迭代編譯");

    return [self.arr reverseObjectEnumerator];
}
複製代碼

 其實第二步和第三步是對集合、數組類型作了特殊的處理,通常開發中不多會用到.接下來來到第四步:

4.若是找不到簡單的訪問器方法或集合訪問方法組,而且若是接收方的類方法accessInstanceVariablesDirectly返回YES,則按該順序搜索名爲_<key>、_is<Key>、<key>或is<Key>的實例變量。若是找到,直接獲取實例變量的值並繼續執行步驟5。不然,繼續執行步驟6。

// 1: KVC - 設置值的過程
 LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 設置值的過程
     [person setValue:@"LG_Cooci" forKey:@"name"];
    // 2: KVC - 取值的過程
     person->_name = @"_name";
     person->_isName = @"_isName";
     person->name = @"name";
     person->isName = @"isName";
     NSLog(@"取值:%@",[person valueForKey:@"name"]);
複製代碼

LGPersson.h:

@interface LGPerson : NSObject{
    @public
     NSString *_name;
     NSString *_isName;
     NSString *name;
     NSString *isName;
}
複製代碼

LGPerson.m:

#pragma mark - 關閉或開啓實例變量賦值
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}

//MARK: - setKey. 的流程分析
//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//MARK: - valueForKey 流程分析 - get<Key>, <key>, is<Key>, or _<key>,

//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)name{
//    return NSStringFromSelector(_cmd);
//}
//
//- (NSString *)isName{
//    return NSStringFromSelector(_cmd);
//}
//
//- (NSString *)_name{
//    return NSStringFromSelector(_cmd);
//}
複製代碼

運行看輸出:2020-02-14 11:34:01.280728+0800 002-KVC取值&賦值過程[45790:4450022] 取值:_name.

說明找不到簡單的訪問器方法或集合訪問方法組,而且若是接收方的類方法accessInstanceVariablesDirectly返回YES,會搜索名爲_<key>的實例變量,

接着咱們註釋 person->_name = @"_name";  運行看輸出:2020-02-14 11:34:01.280728+0800 002-KVC取值&賦值過程[45790:4450022] 取值:_isName.

接着咱們再註釋person->_isName = @"_isName"......

最後得出結論:若是找不到簡單的訪問器方法或集合訪問方法組,而且若是接收方的類方法accessInstanceVariablesDirectly返回YES,則按該順序搜索名爲_<key>、_is<Key>、<key>或is<Key>的實例變量.

第6步就是 上面全部的訪問器方法或集合訪問方法組、實例變量都找不到,會調用valueForUndefinedKey:。默認狀況下,這會引起異常.

四.自定義KVC

4.1 自定義思路

系統的KVC是用NSObject的類別實現的,咱們要自定義KVC無非也是系統的思路,實現個自定義KVC的類別.

4.11 setValue:

1.判斷是否有找到相關方法 set<Key> _set<Key> setIs<Key>的方法.2.判斷是否可以直接賦值實例變量 (accessInstanceVariablesDirectly方法是否返回YES).3.找相關實例變量進行賦值4.若是都找不到拋出異常setValueForUndefinedKey.

4.12 valueForKey

1. 找到相關方法 get<Key> <key> countOf<Key> objectIn<Key>AtIndex.2.判斷是否可以直接賦值實例變量 (accessInstanceVariablesDirectly方法是否返回YES).3.找相關實例變量進行賦值.4.若是都找不到拋出異常valueForUndefinedKey.

4.2 僞代碼實現

@implementation NSObject (LGKVC)

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判斷一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相關方法 set<Key> _set<Key> setIs<Key>
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lg_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lg_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3:判斷是否可以直接賦值實例變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相關實例變量進行賦值
    // 4.1 定義一個收集實例變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2 獲取相應的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3 對相應的 ivar 設置值
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }

    // 5:若是找不到相關實例
    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}


- (nullable id)lg_valueForKey:(NSString *)key{
    
    // 1:刷選key 判斷非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相關方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    // key 要大寫
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
    NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3:判斷是否可以直接賦值實例變量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4.找相關實例變量進行賦值
    // 4.1 定義一個收集實例變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    // _<key> _is<Key> <key> is<Key>
    // _name -> _isName -> name -> isName
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}

#pragma mark - 相關方法
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

@end複製代碼

五.總結

 KVC在設值過程當中:1.首先順序查找第一個名爲set<Key>、_set<Key>、 setIs<Key>的訪問器. 2.若是找不到簡單的訪問器,而且類方法accessInstanceVariablesDirectly返回YES,則按該順序查找名爲「_<key>」、「_is<Key>」、「<key>」或「is<Key>」的實例變量. 3.在找不到訪問器或實例變量時,調用setValue:forUndefinedKey.

KVC在取值過程當中:1.在實例中搜索找到的訪問器方法,該方法的名稱如get<Key>、<key>、is<Key>、或者_<key>.若是有就跳到第五步. 2.是不是NSArray類型判斷. 3.是不是NSSet判斷. 4.判斷是否可以直接賦值實例變量 ,找相關實例變量進行賦值. 5.細節處理 若是該值是NSNumber支持的標量類型,請將其存儲在NSNumber實例中並返回該實例。 若是結果是NSNumber不支持的標量類型,則轉換爲NSValue對象並返回該對象. 6.若是訪問器或實例變量都找不到調用valueForUndefinedKey.

相關文章
相關標籤/搜索