KVC實現原理剖析

一、KVC簡介

KVC全稱是Key Value Coding,定義在NSKeyValueCoding.h文件中,翻譯成中文是鍵值碼,是由NSKeyValueCoding非正式協議啓用的一種機制,對象採用這種機制來提供對其屬性的間接訪問,這種間接訪問機制補充了實例變量及其關聯的訪問器方法提供的直接訪問。KVC的定義是經過NSObject的拓展類來實現的,Objective-C中有個顯式的NSKeyValueCoding類別名,因此能夠說在平常開發中凡是直接或間接繼承或自NSObject的對象均可以使用KVC機制。html

二、KVC的基礎使用

2.一、KVC處理對象屬性

KVC經過 valueForKey:setValue:forKey:來間接的獲取和設置對象的屬性值。 關於這兩個方法的定義以下:git

  • 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. 【譯】valueForKey:-返回由key參數命名的屬性的值。若是根據訪問者搜索模式中描述的規則找不到由key命名的屬性,則該對象向自身發送一條valueForUndefinedKey:消息。valueForUndefinedKey:默認實現拋出一個NSUndefinedKeyException異常,可是子類能夠覆蓋此行爲,並更優雅地處理該狀況。github

  • 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:-將接收消息的對象指定的key設置爲給定值。setValue:forKey:默認實現會自動把表示標量和結構體的 NSNumberNSValue 對象解包,並賦值給屬性。若是指定 key 所對應的屬性沒有對應的 setter 實現,則該對象會向自身發送 setValue:forUndefinedKey:消息,而該消息的默認實現會拋出一個NSUndefinedKeyException 的異常。可是子類能夠重寫此方法以自定義方式處理請求算法

看下面的例子:編程

@interface Person : NSObject{
     // NSString *name;
    // NSString *_name;
    // NSString *_isName;
    // NSString *isName;
}
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
+ (BOOL)accessInstanceVariablesDirectly{
    return YES;
}
@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc]init];
        [person setValue:@"劉德華" forKey:@"name"];
        NSLog(@"name:%@",[person valueForKey:@"name"]);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 17:00:44.647980+0800 KVCDemo[47069:2104780] name:劉德華api

你必定注意到了這段代碼中Person類中的註釋部分的代碼,通過小編驗證不管是以何種形式,程序輸出的結果都是正確的。這裏涉及到的其實KVC的設置和取值規則,會在下面的章節中講解到KVC的設置和取值原理。數組

2.二、KVC使用KeyPath

在開發過程當中,一個類的成員變量有多是自定義類或其餘的複雜數據類型,你能夠先用KVC獲取該屬性,而後再次用KVC來獲取這個自定義類的屬性, 但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。顧名思義,就是按照路徑尋找key。bash

  • 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:-返回相對於接收者的指定key path的值。key path 路徑序列中不符合特定鍵的鍵值編碼的任何對象(即,默認實現valueForKey:沒法找到訪問器方法)均會接收到valueForUndefinedKey:消息。app

  • 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:-將該消息接收者的指定 key path 的值設置爲給定值。key path 路徑序列中不符合特定鍵的鍵值編碼的任何對象都將收到setValue:forUndefinedKey: 消息。ide

看下面例子:

@interface Student : NSObject
@property (nonatomic, copy) NSString *name;

@end
@implementation Student

@end

@interface Person : NSObject{
    Student *_student;
}

@end
@implementation Person


@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //方式一
        Person *person = [[Person alloc]init];
        Student *student = [[Student alloc]init];
        [person setValue:student forKey:@"student"];
        [student setValue:@"小明" forKey:@"name"];
        NSString *name = [[person valueForKey:@"student"] valueForKey:@"name"];
        NSLog(@"name:%@",name);
        
        //方式二
        [person setValue:@"小小" forKeyPath:@"student.name"];
        NSLog(@"name:%@",[person valueForKeyPath:@"student.name"]);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 17:28:35.709176+0800 KVCDemo[47571:2124832] name:小明 2020-03-05 17:28:35.709646+0800 KVCDemo[47571:2124832] name:小小

從打印結果來看咱們成功的經過keyPath設置了student的值。 KVC對於keyPath搜索機制第一步就是分離key,用小數點.來分割key,而後再像普通key同樣按照先前介紹的順序搜索下去。

2.三、KVC處理數值和結構體類型屬性

在前面小節中的內容都是對對象類型的變量進行的設值取值,那麼若是變量類型是數值類型或者是結構,那麼KVC是否也是能夠進行設置和取值的呢?答案是確定的。若是本來的變量類型是值類型或者結構體,返回值會封裝成NSNumber或者NSValue對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。而後開發者須要手動轉換成原來的類型。儘管valueForKey:會自動將值類型封裝成對象,可是 setValue:forKey:卻不行。你必須手動將值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。由於傳遞進去和取出來的都是id類型,因此須要開發者本身擔保類型的正確性,運行時Objective-C在發送消息的會檢查類型,若是錯誤會直接拋出異常。

看下面例子:

typedef struct {
    float x, y, z;
} ThreeFloats;

@interface Person : NSObject

@property (nonatomic, assign) NSInteger age;

@property (nonatomic) ThreeFloats threeFloats;

@end
@implementation Person

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //處理數值類型
        Person *person = [[Person alloc]init];
        [person setValue:[NSNumber numberWithInteger:20] forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);

        //處理結構體
        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);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 17:49:12.490826+0800 KVCDemo[47898:2139908] age:20 2020-03-05 17:49:12.491413+0800 KVCDemo[47898:2139908] {length = 12, bytes = 0x0000803f0000004000004040} 2020-03-05 17:49:12.491461+0800 KVCDemo[47898:2139908] 1.000000 - 2.000000 - 3.000000

須要注意的是咱們不能直接將一個數值經過KVC賦值的,咱們須要把數據轉爲NSNumber和NSValue類型傳入,那到底哪些類型數據要用NSNumber封裝哪些類型數據要用NSValue封裝呢?看下面這些方法的參數類型就知道了: 可使用NSNumber的數據類型有:

+ (NSNumber*)numberWithChar:(char)value; + (NSNumber*)numberWithUnsignedChar:(unsignedchar)value; + (NSNumber*)numberWithShort:(short)value; + (NSNumber*)numberWithUnsignedShort:(unsignedshort)value; + (NSNumber*)numberWithInt:(int)value; + (NSNumber*)numberWithUnsignedInt:(unsignedint)value; + (NSNumber*)numberWithLong:(long)value; + (NSNumber*)numberWithUnsignedLong:(unsignedlong)value; + (NSNumber*)numberWithLongLong:(longlong)value; + (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value; + (NSNumber*)numberWithFloat:(float)value; + (NSNumber*)numberWithDouble:(double)value; + (NSNumber*)numberWithBool:(BOOL)value; + (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0); + (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);

可使用NSValue的數據類型有:

+ (NSValue*)valueWithCGPoint:(CGPoint)point; + (NSValue*)valueWithCGSize:(CGSize)size; + (NSValue*)valueWithCGRect:(CGRect)rect; + (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform; + (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets; + (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

2.四、KVC處理集合

KVC提供了對集合類型處理的方法。

  • mutableArrayValueForKey: 和 mutableArrayValueForKeyPath: 這些返回行爲像NSMutableArray對象的代理對象。
  • mutableSetValueForKey: 和 mutableSetValueForKeyPath: 這些返回行爲像NSMutableSet對象的代理對象。
  • mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath: 這些返回行爲像NSMutableOrderedSet對象的代理對象。

看下面的例子:

@interface Person : NSObject

@property (nonatomic, copy) NSArray *classArr;
@property (nonatomic, copy) NSString *name;

@end
@implementation Person

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc]init];
        person.classArr = @[@"Chinese",@"Mathematics",@"English"];
        [person setValue:@"小明" forKey:@"name"];
        [person mutableArrayValueForKey:@"classArr"];
        NSLog(@"name:%@,class:%@",[person valueForKey:@"name"],[person mutableArrayValueForKey:@"classArr"]);
       
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 22:01:58.540062+0800 KVCDemo[52012:2424108] name:小明,class:( Chinese, Mathematics,English)

這裏只演示了mutableArrayValueForKey的使用,其餘的方法使用類同,在這裏就很少贅述了。

2.五、KVC處理集合運算符

KVC同時還提供了集合運算符,利用這些集合運算符能夠針對集合作一些高效的統計運算。這些集合運算符主要分爲三大類,以下所示:

聚合操做符

  • @avg: 返回集合中指定對象屬性的平均值
  • @count: 返回集合中指定對象屬性的個數
  • @max: 返回集合中指定對象屬性的最大值
  • @min: 返回集合中指定對象屬性的最小值
  • @sum: 返回集合中指定對象屬性值之和

數組操做符

  • @distinctUnionOfObjects: 返回集合中指定對象屬性的集合,且會進行去重操做
  • @unionOfObjects: 返回集合中指定對象屬性的集合,並不會刪除相同元素。

嵌套操做符

  • @distinctUnionOfArrays: 返回指定的屬性相對應的全部集合的組合的不一樣對象集合,並會刪除相同的元素
  • @unionOfArrays: 返回指定的屬性相對應的全部集合的組合的不一樣對象集合,可是不會刪除相同元素
  • @distinctUnionOfSets: 返回指定的屬性相對應的全部集合的組合中的不一樣對象集合,並刪除相同元素,返回的是 NSSet

看下面的例子:

@interface Book : NSObject
@property (nonatomic, copy)  NSString *name;
@property (nonatomic, assign)  CGFloat price;
@end

@implementation Book
@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Book *book1 = [Book new];
        book1.name = @"編程珠璣";
        book1.price = 50;
        Book *book2 = [Book new];
        book2.name = @"Java編程思想";
        book2.price = 20;
        Book *book3 = [Book new];
        book3.name = @"漫畫算法";
        book3.price = 30;

        Book *book4 = [Book new];
        book4.name = @"算法圖解";
        book4.price = 30;

        NSArray *arrBooks = @[book1, book2, book3, book4];
        //計算總價
        NSNumber *sum = [arrBooks valueForKeyPath:@"@sum.price"];
        NSLog(@"sum:%f", sum.floatValue);
        //計算平均價格
        NSNumber *avg = [arrBooks valueForKeyPath:@"@avg.price"];
        NSLog(@"avg:%f", avg.floatValue);
        //計算書本數量
        NSNumber *count = [arrBooks valueForKeyPath:@"@count"];
        NSLog(@"count:%f", count.floatValue);
        //計算最小的書本價格
        NSNumber *min = [arrBooks valueForKeyPath:@"@min.price"];
        NSLog(@"min:%f", min.floatValue);
        //計算最大的書本價格
        NSNumber *max = [arrBooks valueForKeyPath:@"@max.price"];
        NSLog(@"max:%f", max.floatValue);
        //返回書本的價格集合
        NSArray *distinctPrice = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
        NSLog(@"distinctPrice:%@", distinctPrice);
        //返回書本的價格集合
        NSArray *unionPrice = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
        NSLog(@"unionPrice:%@", unionPrice);
        
        NSArray *arr1 = @[book1,book2];
        NSArray *arr2 = @[book3,book4];
        NSArray *arr = @[arr1,arr2];
        NSArray *collectedDistinctPrice = [arr valueForKeyPath:@"@distinctUnionOfArrays.price"];
        NSLog(@"collectedDistinctPrice:%@", collectedDistinctPrice);
        NSArray *collectedPrice = [arr valueForKeyPath:@"@unionOfArrays.price"];
        NSLog(@"collectedPrice:%@", collectedPrice);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 22:55:37.927846+0800 KVCDemo[52895:2466440] sum:130.000000 2020-03-05 22:55:37.928381+0800 KVCDemo[52895:2466440] avg:32.500000 2020-03-05 22:55:37.928467+0800 KVCDemo[52895:2466440] count:4.000000 2020-03-05 22:55:37.928526+0800 KVCDemo[52895:2466440] min:20.000000 2020-03-05 22:55:37.928568+0800 KVCDemo[52895:2466440] max:50.000000 2020-03-05 22:55:37.928693+0800 KVCDemo[52895:2466440] distinctPrice:( 20,30,50) 2020-03-05 22:55:37.928783+0800 KVCDemo[52895:2466440] unionPrice:( 50,20,30,30) 2020-03-05 22:55:37.928865+0800 KVCDemo[52895:2466440] collectedDistinctPrice:( 20,30, 50) 2020-03-05 22:55:37.928931+0800 KVCDemo[52895:2466440] collectedPrice:( 50, 20,30,30)

2.五、KVC處理字典

當對NSDictionary對象使用KVC時,valueForKey的表現行爲和objectForKey:同樣。因此使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。

  • 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. 【譯】返回相對於接收者的 key 數組的值。該方法會爲數組中的每一個 key 調用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。

看下面的例子:

@interface Address : NSObject
@property (nonatomic, copy) NSString *country;
@property (nonatomic, copy) NSString *province;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *district;
@end

@implementation Address

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        //模型轉字典
        Address *address = [Address new];
        address.country = @"China";
        address.province = @"Guang Dong";
        address.city = @"Shen Zhen";
        address.district = @"Nan Shan";
        NSArray *arr = @[@"country", @"province", @"city", @"district"];
        NSDictionary *dict = [address dictionaryWithValuesForKeys:arr];
        NSLog(@"%@", dict);

        //字典轉模型
        NSDictionary *modifyDict = @{ @"country": @"China", @"province": @"Guang Dong", @"city": @" Shen Zhen", @"district": @"Nan Shan" };
        [address setValuesForKeysWithDictionary:modifyDict];            //用key Value來修改Model的屬性
        NSLog(@"country:%@ province:%@ city:%@ district:%@", address.country, address.province, address.city, address.district);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 23:07:20.645255+0800 KVCDemo[53037:2474708] { city = "Shen Zhen"; country = China; district = "Nan Shan"; province = "Guang Dong"; } 2020-03-05 23:07:20.646032+0800 KVCDemo[53037:2474708] country:China province:Guang Dong city: Shen Zhen district:Nan Shan

2.六、KVC處理異常

在使用KVC開發的過程當中不免會出現一些失誤,諸如寫錯了key或者在設置的時候傳遞了nil的值,KVC中專門提供了處理這些異常的方法。

2.6.一、KVC處理nil異常

一般狀況下,KVC不容許你要在調用setValue:forKey:或者setValue:forKeyPath: 時對非對象傳遞一個nil的值。很簡單,由於值類型是不能爲nil的。若是你不當心傳了,KVC會調用setNilValueForKey:方法。這個方法默認是拋出異常,因此通常而言最好仍是重寫這個方法。 看下面例子:

@interface Person : NSObject
{
    int age;
}

@end

@implementation Person

- (void)setNilValueForKey:(NSString *)key {
    NSLog(@"不能將%@設成nil", key);
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person setValue:nil forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 23:17:24.713116+0800 KVCDemo[53187:2481241] 不能將age設成nil 2020-03-05 23:17:24.713661+0800 KVCDemo[53187:2481241] age:0

2.6.二、處理UndefinedKey異常

一般狀況下,KVC不容許你要在調用setValue:forKey:或者setValue:forKeyPath:時對不存在的key進行操做。 不然會報錯發生崩潰,重寫setValue: forUndefinedKey:valueForUndefinedKey:方法避免崩潰。 看下面的例子:

@interface Person : NSObject

@end

@implementation Person

- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@",key);
    return nil;
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"出現異常,該key不存在%@", key);
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person setValue:nil forKey:@"age"];
        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}
複製代碼

2.七、KVC鍵值驗證(Key-Value Validation)

KVC提供了驗證Key對應的Value是否可用的方法,調用validateValue:forKey:error:(或validateValue:forKeyPath:error:)方法時,協議的默認實現會在接收驗證消息的對象(或keyPath的對象)中根據key搜索是否有方法validate<Key>:error:實現。若是對象沒有這種方法,則默認狀況下驗證成功,而且默認實現返回YES。當存在特定於屬性的驗證方法時,默認實現將返回調用該方法的結果。 因爲特定於屬性的驗證方法經過引用接收值和錯誤參數,所以驗證具備三種可能的結果:

  • 驗證成功,返回 YES,value不作修改。
  • 驗證失敗,返回 NO,value不作修改,若是調用者提供了 NSError 的話,就把錯誤引用設置爲指示錯誤緣由的NSError對象。
  • 驗證失敗,返回 YES,可是建立了一個新的有效的屬性值做爲替代。在返回以前,該方法將值引用修改成指向新值對象。 進行修改時,即便值對象是可變的,該方法也老是建立一個新對象,而不是修改舊對象。

看下面的例子:

@interface Person : NSObject
{
    int age;
}

@end

@implementation Person
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *_Nullable __autoreleasing *)outError {
    NSNumber *age = *ioValue;
    if (age.integerValue == 10) {
        return NO;
    }
    return YES;
}

@end

int main(int argc, const char *argv[])
{
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        NSNumber *age = @10;
        NSError *error;
        NSString *key = @"age";
        BOOL isValid = [person validateValue:&age forKey:key error:&error];
        if (isValid) {
            NSLog(@"鍵值匹配");
            [person setValue:age forKey:key];
        } else {
            NSLog(@"鍵值不匹配");
        }

        NSLog(@"age:%@", [person valueForKey:@"age"]);
    }
    return 0;
}
複製代碼

打印結果: 2020-03-05 23:42:22.811163+0800 KVCDemo[53621:2503057] 鍵值不匹配 2020-03-05 23:42:22.811752+0800 KVCDemo[53621:2503057] age:0

三、KVC設值和取值原理

在前面的章節中的咱們探索了KVC的基本使用,可是仍是不知道KVC的設值和取值規則,只有把這些規則都弄清楚了,才能在實際開發中駕輕就熟。 KVC的設值和取值規則針對於對象類型、可變數組、可變有序集、可變集的規則有所不一樣。

3.一、基礎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:方法並拋出異常,這是默認行爲。可是子類能夠重寫此方法。

3.二、基礎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的子類能夠提出合適的行爲。

3.三、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事件。
  3. 若是沒有可變數組的方法,也沒有找到訪問器,但接受響應的類accessInstanceVariablesDirectly屬性返回YES,則查找一個名爲_<key><key>的實例變量。按照這個順序,若是找到實例變量,則返回一個代理對象。改對象將接收全部NSMutableArray發送過來的消息,一般是NSMutableArray或其子類。
  4. 若是全部狀況都失敗,則返回一個可變的集合代理對象。當它接收NSMutableArray消息時,發送一個setValue:forUndefinedKey:消息給接收mutableArrayValueForKey:消息的原始對象。這個setValue:forUndefinedKey:的默認實現是提出一個NSUndefinedKeyException異常,可是子類能夠重寫這個實現。

3.三、其餘

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

四、自定義KVC

在上一個章節中咱們分析了KVC的設值和取值規則,那麼即可以遵守規則定義本身的KVC。自定義KVC主要是基於取值和設值兩個方面考慮。

4.一、自定義KVC設值

自定義KVC設值仍是在setValue:forKey:方法上面作文章,大體思路以下:

  1. 首先須要判斷傳進來的key是否爲nil,若是爲nil則直接返回,不然執行第2步;
  2. 找到相關方法 set<Key>_set<Key>setIs<Key>是否有實現,若是有實現的話這直接調用這些方法,不然執行第3步;
  3. 判斷accessInstanceVariablesDirectly方法的返回結果,若是返回NO,拋出異常,不然執行第4步;
  4. 按照_<key>_is<Key><key>is<Key>順序查找成員變量,若是找到了則直接賦值,不然執行第5步;
  5. 若是程序執行到這一步則說明按照搜索規則沒有找到相應的key,則直接拋出異常。

主要代碼以下:

- (void)ds_setValue:(nullable id)value forKey:(NSString *)key{
    
    // 1:非空判斷一下
    if (key == nil  || key.length == 0) return;
    
    // 2:找到相關方法 set<Key> _set<Key> setIs<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 ds_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"執行 %@ 方法",setKey);
        return;
    }else if ([self ds_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"執行 %@ 方法",_setKey);
        return;
    }else if ([self ds_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.找相關實例變量進行賦值,按照這個順序查找_<key> _is<Key> <key> is<Key>
    // 4.1 定義一個收集實例變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    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];
}
複製代碼

判斷方法是否實現:

- (BOOL)ds_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;
}
複製代碼

獲取實例變量數組的方法以下:

- (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;
}
複製代碼

4.二、自定義KVC取值

自定義KVC設值仍是在lg_valueForKey:方法上面作文章,大體思路以下:

  1. 首先判斷key是否爲nil,若是爲nil,則直接返回,不然執行第2步;
  2. 按照順序找到相關方法get<Key><key>countOf<Key>objectIn<Key>AtIndex是否實現,若是有其中一個實現則直接調用這個方法,不然執行第3步;
  3. 判斷accessInstanceVariablesDirectly方法的返回結果,若是返回NO,拋出異常,不然執行第4步;
  4. 按照_<key>_is<Key><key>is<Key>順序查找成員變量,若是找到了則直接取值,不然執行第5步;
  5. 若是執行到這一步,則返回空。 主要代碼以下:
- (nullable id)ds_valueForKey:(NSString *)key {
    // 1:刷選key 判斷非空
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2:找到相關方法 get<Key> <key> countOf<Key>  objectIn<Key>AtIndex
    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.找相關實例變量進行賦值 按照順序查找是否實現_<key> _is<Key> <key> is<Key>
    // 4.1 定義一個收集實例變量的可變數組
    NSMutableArray *mArray = [self getIvarListName];
    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 @"";
}
複製代碼

在這裏的自定義是比較簡潔的一種寫法,並不夠完善,若是有興趣的能夠閱讀[DIS_KVC_KVO](https://github.com/renjinkui2719/DIS_KVC_KVO)的源碼,對KVC和KVO都有比較全面詳細的自定義。

五、參考資料

蘋果官方文檔-KVC

iOS KVC和KVO詳解

DIS_KVC_KVO

相關文章
相關標籤/搜索