KVC實現原理剖析

1、KVC簡介

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

2、KVC的基礎使用

2.1、KVC處理對象屬性

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

  • 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異常,但是子類可以覆蓋此行為,并更優雅地處理該情況。

  • 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:劉德華

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

2.2、KVC使用KeyPath

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

  • 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:消息。

  • 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: 消息。

看下面例子:

@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.3、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.4、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.5、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.5、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.6、KVC處理異常

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

2.6.1、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.2、處理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.7、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

3、KVC設值和取值原理

在前面的章節中的我們探索了KVC的基本使用,但是還是不知道KVC的設值和取值規則,只有把這些規則都弄清楚了,才能在實際開發中得心應手。
KVC的設值和取值規則針對于對象類型、可變數組、可變有序集、可變集的規則有所不同。

3.1、基礎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.2、基礎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.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.3、其他

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

4、自定義KVC

在上一個章節中我們分析了KVC的設值和取值規則,那么便可以遵照規則定義自己的KVC。自定義KVC主要是基于取值和設值兩個方面考慮。

4.1、自定義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.2、自定義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的源碼,對KVC和KVO都有比較全面詳細的自定義。

5、參考資料

蘋果官方文檔-KVC

iOS KVC和KVO詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380