KVC詳解

KVC簡單介紹

KVC(Key-value coding)鍵值編碼,就是指iOS的開發(fā)中,可以允許開發(fā)者通過Key名間接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態(tài)在訪問和修改對象的屬性,而不是在編譯時確定。

KVC在iOS中的定義

無論是Swift還是Objective-C,KVC的定義都是對NSObject的擴展來實現(xiàn)的(Objective-c中有個顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)所以對于所有繼承了NSObject在類型,都能使用KVC(一些純Swift類和結構體是不支持KVC的),下面是KVC最為重要的四個方法

// 直接通過Key來取值
- (nullable id)valueForKey:(NSString *)key;
// 通過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

當然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些

// 默認返回YES,表示如果沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;
// KVC提供屬性值確認的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (nullable id)valueForUndefinedKey:(NSString *)key;
// 和上一個方法一樣,只不過是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// 如果你在SetValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;
// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用于將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

代碼示例

Person類:
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) int age;
@end

Controller類:
Person *p1 = [[Person alloc]init];
[p1 setValue:@"LianXi" forKeyPath:@"name"];
[p1 setValue:@"24" forKeyPath:@"age"];

Person *p2 = [[Person alloc]init];
[p2 setValue:@"XueXi" forKeyPath:@"name"];
[p2 setValue:@"25" forKeyPath:@"age"];

NSLog(@"%@, %@", [p1  valueForKeyPath:@"name"], [p1 valueForKeyPath:@"age"]);

使用KVC間接修改對象屬性時,系統(tǒng)會自動判斷對象屬性的類型,并完成轉換。如該程序中的“24”。
如果需要只生成name的數組,并打印應該怎么辦?
常規(guī)方法:
NSArray *persons = @[p1, p2];
        
NSMutableArray *arrayM = [NSMutableArray array];
for (Person *p in persons) {
      [arrayM addObject:[p valueForKeyPath:@"name"]];
}
NSLog(@"%@", arrayM);
打印效果:(LianXi, XueXi)

KVC方法:
NSArray *persons = @[p1, p2];

NSMutableArray *arrayM = [NSMutableArray array];
[arrayM addObject:[persons valueForKeyPath:@"name"]];
NSLog(@"%@", arrayM);
打印效果:((LianXi, XueXi))

KVC在按照鍵值路徑取值時,會自動層層深入,獲取對應的鍵值。

Book類:
@interface Book : NSObject
@property(nonatomic, copy) NSString *bookname;
@end

Person類:
@class Book;
@interface Person : NSObject
@property(nonatomic, copy) NSString *name;
@property(nonatomic, assign) int age;
@property(nonatomic, strong) Book *book;
@end

UIViewController類:
Person *p1 = [[Person alloc]init];
[p1 setValue:@"LianXi" forKeyPath:@"name"];
[p1 setValue:@"24" forKeyPath:@"age"];

Book *b1 = [[Book alloc] init];
b1.bookname = @"iPhone";
p1.book = b1;
 
Person *p2 = [[Person alloc]init];
[p2 setValue:@"XueXi" forKeyPath:@"name"];
[p2 setValue:@"25" forKeyPath:@"age"];

Book *b2 = [[Book alloc] init];
b2.bookname = @"Mac";
p2.book = b2;

// 建立一個存儲person對象的數組,并打印
NSArray *persons = @[p1, p2];

NSArray *arrayM = [persons valueForKeyPath:@"book.bookname"];
NSLog(@"%@", arrayM);

KVC在內部是按什么樣的順序來尋找key的?

#######當調用setValue:屬性值 forKey:@”name“的代碼時,底層的執(zhí)行機制如下:

  • 程序優(yōu)先調用set<Key>:屬性值方法,代碼通過setter方法完成設置。注意,這里的<key>是指成員變量名,首字母大清寫要符合KVC的全名規(guī)則。
  • 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執(zhí)行setValue:forUNdefinedKey:方法,不過一般開發(fā)者不會這么做。所以KVC機制會搜索該類里面有沒有名為<key>的成員變量,無論該變量是在類接口部分定義,還是在類實現(xiàn)部分定義,也無論用了什么樣的訪問修飾符,只在存在以<key>命名的變量,KVC都可以對該成員變量賦值。
  • 如果該類即沒有set<Key>:方法,也沒有_<key>成員變量,KVC機制會搜索_is<Key>的成員變量。
  • 如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機制再會繼續(xù)搜索<key>和is<Key>的成員變量。再給它們賦值。
  • 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會執(zhí)行該對象的setValue:forUNdefinedKey:方法,默認是拋出異常。
  • 如果開發(fā)者想讓這個類禁用KVC里,那么重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時,會直接用setValue:forUNdefinedKey:方法。

代碼示例

Person類:
@implementation Person
{
    NSString* toSetName;
    NSString* isName;
    //NSString* name;
    NSString* _name;
    NSString* _isName;
}
//- (void)setName:(NSString*)name {
//     toSetName = name;
// }
//- (NSString*)getName {
//    return toSetName;
//}
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}
- (id)valueForUndefinedKey:(NSString *)key {
    NSLog(@"出現(xiàn)異常,該key不存在%@", key);
    return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
     NSLog(@"出現(xiàn)異常,該key不存在%@", key);
}
@end

Controller類:
Person *person = [Person new];
person setValue:@"newName" forKey:@"name"];
NSString *name = [person valueForKey:@"toSetName"];
NSLog(@"%@", name);

重寫accessInstanceVariablesDirectly方法讓其返回NO,再運行代碼打印出:
2017-04-16 23:12:**.302 KVC: 出現(xiàn)異常,該key不存在name
2017-04-16 23:12:**.302 KVC: 出現(xiàn)異常,該key不存在toSetName
2017-04-16 23:12:**.302 KVC: (null)
這說明了重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO后, KVC找不到SetName:方法后,不再去找name系列成員變量,而是直接調用forUndefinedKey方法。

當調用ValueforKey:@”name“的代碼時,KVC對key的搜索方式不同于setValue:屬性值 forKey:@”name“,其搜索方式如下:
  • 首先按get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者int等值類型, 會做NSNumber轉換
  • 如果上面的getter沒有找到,KVC則會查找countOf<Key>,objectIn<Key>AtIndex,<Key>AtIndex格式的方法。如果countOf<Key>和另外兩個方法中的要個被找到,那么就會返回一個可以響應NSArray所的方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發(fā)送NSArray的方法,就會以countOf<Key>,objectIn<Key>AtIndex,<Key>AtIndex這幾個方法組合的形式調用。還有一個可選的get<Ket>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
  • 如果上面的方法沒有找到,那么會查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所的方法的代理集合,以送給這個代理集合消息方法,就會以countOf<Key>,enumeratorOf<Key>,memberOf<Key>組合的形式調用。
  • 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),那么和先前的設值一樣,會按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會直接調用valueForUndefinedKey:
  • 還沒有找到的話,調用valueForUndefinedKey:

代碼示例

TimesArray類:
@interface TimesArray : NSObject
- (void)incrementCount;
- (NSUInteger)countOfNumbers;
- (id)objectInNumbersAtIndex:(NSUInteger)index;
@end

@interface TimesArray()
@property(nonatomic, readwrite, assign) NSUInteger count;
@property(nonatomic, copy) NSString *arrName;
@end
@implementation TimesArray
- (void)incrementCount {
    self.count ++;
}
- (NSUInteger)countOfNumbers {
    return self.count;
}
- (id)objectInNumbersAtIndex:(NSUInteger)index {// 當key使用numbers時,KVC會找到這兩個方法。
    return @(index * 2);
}
- (NSInteger)getNum { //第一個,自己一個一個注釋試
    return 10;
}
- (NSInteger)num { //第二個
    return 11;
}
- (NSInteger)isNum { //第三個
    return 12;
}
@end

Controller類:
TimesArray *arr = [TimesArray new];

NSNumber *num = [arr valueForKey:@"num"];
NSLog(@"%@", num);
    
id ar = [arr valueForKey:@"numbers"];
NSLog(@"%@", NSStringFromClass([ar class]));
NSLog(@"0:%@ 1:%@ 2:%@ 3:%@", ar[0], ar[1], ar[2], ar[3]);
    
[arr incrementCount]; //count加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出1
    
[arr incrementCount]; //count再加1
NSLog(@"%lu", (unsigned long)[ar count]); //打印出2
    
[arr setValue:@"newName" forKey:@"arrName"];
NSString *name = [arr valueForKey:@"arrName"];
NSLog(@"%@", name);

代碼打印出:
2017-04-20 11:03:08.325 KVC[86611:34004819] 10
2017-04-20 11:03:08.326 KVC[86611:34004819] NSKeyValueArray
2017-04-20 11:03:08.326 KVC[86611:34004819] 0:0 1:2 2:4 3:6 //太明顯了,直接調用- (id)objectInNumbersAtIndex:(NSUInteger)index;方法
2017-04-20 11:03:08.326 KVC[86611:34004819] 1
2017-04-20 11:03:08.326 KVC[86611:34004819] 2
2017-04-20 11:03:08.326 KVC[86611:34004819] newName

KVC中使用KeyPath

然而在開發(fā)過程中,一個類的成員變量有可能是其他的自定義類,你可以先用KVC獲取出來再該屬性,然后再次用KVC來獲取這個自定義類的屬性,但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑KeyPath。

// 通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
// 通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;

代碼示例

@interface Address : NSObject
@end

@interface Address()
@property(nonatomic, copy)NSString *country;
@end

@implementation Address
@end

@interface People : NSObject
@end

@interface People()
@property(nonatomic, copy) NSString *name;
@property(nonatomic, strong) Address *address;
@property(nonatomic, assign) NSInteger age;
@end

@implementation People
@end

UIViewController類:
People *people1 = [People new];
Address *add = [Address new];
add.country = @"China";
people1.address = add;
NSString *country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@   country2:%@",country1,country2);
[people1 setValue:@"USA" forKeyPath:@"address.country"];
country1 = people1.address.country;
country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@   country2:%@",country1,country2);

代碼打印出: 
2017-04-16 23:12:**.302 KVC: country1:China   country2:China
2017-04-16 23:12:**.302 KVC: country1:USA   country2:USA

上面的代碼簡單在展示了KeyPath是怎么用的。如果你不小心錯誤的使用了key而非KeyPath的話,KVC會直接查找address.country這個屬性,很明顯,這個屬性并不存在,所以會再調用UndefinedKey相關方法。而KVC對于KeyPath是搜索機制第一步就是分離key,用小數點.來分割key,然后再像普通key一樣按照先前介紹的順序搜索下去。

KVC如何處理異常

KVC中最常見的異常就是不小心使用了錯誤的Key,或者在設值中不小心傳遞了nil的值,KVC中有專門的方法來處理這些異常。
通常在用KVC操作Model時,拋出異常的那兩個方法是需要重寫的。雖然一般很小出現(xiàn)傳遞了錯誤的Key值這種情況,但是如果不小心出現(xiàn)了,直接拋出異常讓APP崩潰顯然是不合理的。
一般在這里直接讓這個Key打印出來即可,或者有些特殊情況需要特殊處理。
通常情況下,KVC不允許你要在調用setValue:屬性值 forKey:@”name“(或者keyPath)時對非對象傳遞一個nil的值。很簡單,因為值類型是不能為nil的。如果你不小心傳了,KVC會調用setNilValueForKey:方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。

[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<People 0x100200080> setNilValueForKey]: could not set nil as the value for the key age.' // 調用setNilValueForKey拋出異常

如果重寫setNilValueForKey:就沒問題了

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

代碼打印出: 
2017-04-16 23:12:**.302 KVC: 不能將age設成nil

KVC處理非對象和自定義對象

不是每一個方法都返回對象,但是valueForKey:總是返回一個id對象,如果原本的變量類型是值類型或者結構體,返回值會封裝成NSNumber或者NSValue對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。然后開以者需要手動轉換成原來的類型。盡管valueForKey:會自動將值類型封裝成對象,但是setValue:forKey:卻不行。你必須手動將值類型轉換成NSNumber或者NSValue類型,才能傳遞過去。
對于自定義對象,KVC也會正確以設值和取值。因為傳遞進去和取出來的都是id類型,所以需要開發(fā)者自己擔保類型的正確性,運行時Objective-C在發(fā)送消息的會檢查類型,如果錯誤會直接拋出異常。

代碼示例

Address *add2 = [Address new];
add2.country = @"England";

[people1 setValue:add2 forKey:@"address"];
NSString *country1 = people1.address.country;
NSString *country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@", country1, country2);
代碼打印出: 
2017-04-16 23:12:**.302 KVC: country1:England country2:England

KVC與容器類

對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要么是有序的(數組),要么是無序的(數組)

  • 不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)一般可以使用valueForKey:來獲取。比如有一個叫items的NSArray屬性,你可以用valurForKey:@"items"來獲取這個屬性。前面valueForKey:的key搜索模式中,我們發(fā)現(xiàn)其實KVC使用了一種更靈活的方式來管理容器類。
  • 而當對象的屬性是可變的容器時,對于有序的容器,可以用下面的方法:
    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
    該方法返回一個可變有序數組,如果調用該方法,KVC的搜索順序如下
  • 搜索insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
  • 如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray2),那么給這個代理集合發(fā)送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes組合的形式調用。還有兩個可選實現(xiàn)的接口:replaceOnjectAtIndex:withObject: , replace<Key>AtIndexes:with<Key>: 。
  • 如果上步的方法沒有找到,則搜索set<Key>: 格式的方法,如果找到,那么發(fā)送給代理集合的NSMutableArray最終都會調用set<Key>:方法。 也就是說,mutableArrayValueForKey:取出的代理集合修改后,用·set<Key>:· 重新賦值回去去。這樣做效率會低很多。所以推薦實現(xiàn)上面的方法。
  • 如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_<key>,<key>,的順序搜索成員變量名,如果找到,那么發(fā)送的NSMutableArray消息方法直接交給這個成員變量處理。
  • 如果還是找不到,調用valueForUndefinedKey:
  • 關于mutableArrayValueForKey:的適用場景,我在網上找了很多,發(fā)現(xiàn)其一般是用在對NSMutableArray添加Observer上。
  • 如果對象屬性是個NSMutableAArray、NSMutableSet、NSMutableDictionary等集合類型時,你給它添加KVO時,你會發(fā)現(xiàn)當你添加或者移除元素時并不能接收到變化。因為KVO的本質是系統(tǒng)監(jiān)測到某個屬性的內存地址或常量改變時,會添加上- (void)willChangeValueForKey:(NSString *)key
    和- (void)didChangeValueForKey:(NSString *)key方法來發(fā)送通知,所以一種解決方法是手動調用者兩個方法,但是并不推薦,你永遠無法像系統(tǒng)一樣真正知道這個元素什么時候被改變。另一種便是利用使用mutableArrayValueForKey:了。

代碼示例

@interface demo : NSObject
@property(nonatomic, strong) NSMutableArray *arr;
@end

@implementation demo
- (id)init {
    if (self == [super init]) {
        _arr = [NSMutableArray new];
        [self addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
    }
    return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    NSLog(@"%@",change);
}
- (void)dealloc {
    [self removeObserver:self forKeyPath:@"arr"]; //一定要在dealloc里面移除觀察
}
- (void)addItem {
    [_arr addObject:@"1"];
}
- (void)addItemObserver {
    [[self mutableArrayValueForKey:@"arr"] addObject:@"1"];
}
- (void)removeItemObserver {
    [[self mutableArrayValueForKey:@"arr"] removeLastObject];
}
@end

然后再:
demo *d = [demo new];
[d addItem];
[d addItemObserver];
[d removeItemObserver];

打印結果:
2017-04-16 23:12:**.302 KVC: [32647:505864] {
    indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        1
    );
}
2017-04-16 23:12:**.302 KVC: [32647:505864] {
    indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 3;
    old =     (
        1
    );
}

從上面的代碼可以看出,當只是普通地調用[_arr addObject:@"1"]時,Observer并不會回調,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"1"];這樣寫時才能正確地觸發(fā)KVO。打印出來的數據中,可以看出這次操作的詳情,kind可能是指操作方法(我還不是很確認),old和new并不是成對出現(xiàn)的,當加添新數據時是new,刪除數據時是old

而對于無序的容器,可以用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
該方法返回一個可變的無序數組如果調用該方法,KVC的搜索順序如下:

  • 搜索addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
  • 如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableSet所有方法代理集合(類名是NSKeyValueFastMutableSet2),那么給這個代理集合發(fā)送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>組合的形式調用。還有兩個可選實現(xiàn)的接口:intersect<Key> , set<Key>: 。
  • 如果reciever是ManagedObject,那么就不會繼續(xù)搜索。
  • 如果上步的方法沒有找到,則搜索set<Key>: 格式的方法,如果找到,那么發(fā)送給代理集合的NSMutableSet最終都會調用set<Key>:方法。 也就是說,mutableSetValueForKey取出的代理集合修改后,用set<Key>: 重新賦值回去去。這樣做效率會低很多。所以推薦實現(xiàn)上面的方法。
  • 如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_<key>,<key>,的順序搜索成員變量名,如果找到,那么發(fā)送的NSMutableSet消息方法直接交給這個成員變量處理。
  • 如果還是找不到,調用valueForUndefinedKey:
  • 可見,除了檢查reciever是ManagedObject以外,其搜索順序和mutableArrayValueForKey基本一至,
同樣,它們也有對應的keyPath版本
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;

iOS5和OSX10.7以后還有個mutableOrdered版本

- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key

這兩種KVC的用法我還不是清楚,目前只能找到用于KVO的例子。

KVC和字典

當對NSDictionary對象使用KVC時,valueForKey:的表現(xiàn)行為和objectForKey:一樣。所以使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。

KVC里面還有兩個關于NSDictionary的方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;

dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對應的屬性,再組成一個字典。
setValuesForKeysWithDictionary是用來修改Model中對應key的屬性。代碼會更直觀一點。

Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key所有的屬性全部取出來
NSLog(@"%@",dict);

NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value來修改Model的屬性
NSLog(@"country:%@  province:%@ city:%@",add.country,add.province,add.city);

//打印結果
2017-04-16 23:12:**.302 KVC: [6607:198900] {
    city = "Shen Zhen";
    country = China;
    district = "Nan Shan";
    province = "Guang Dong";
}
2017-04-16 23:12:**.302 KVC: country:USA  province:california city:Los angle
打印出來的結果完全符合預期。

KVC的內部實現(xiàn)機制

前面我們對析了KVC是怎么搜索key的。所以如果明白了key的搜索順序,是可以自己寫代碼實現(xiàn)KVC的。在考慮到集合和keyPath的情況下,KVC的實現(xiàn)會比較復雜,我們只寫代碼實現(xiàn)最普通的取值和設值即可。

@interface NSObject(MYKVC)
- (void)setMyValue:(id)value forKey:(NSString*)key;
- (id)myValueforKey:(NSString*)key;
@end

@implementation NSObject(MYKVC)
- (void)setMyValue:(id)value forKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return;
    }
    if ([value isKindOfClass:[NSNull class]]) {
        [self setNilValueForKey:key]; //如果需要完全自定義,那么這里需要寫一個setMyNilValueForKey,但是必要性不是很大,就省略了
        return;
    }
    if (![value isKindOfClass:[NSObject class]]) {
        @throw @"must be s NSobject type";
        return;
    }

    NSString *funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
        [self performSelector:NSSelectorFromString(funcName) withObject:value];
        return;
    }
    unsigned int count;
    BOOL flag = false;
    Ivar *vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString *keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];

        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            object_setIvar(self, var, value);
            break;
        }
    }
    if (!flag) {
        [self setValue:value forUndefinedKey:key];//如果需要完全自定義,那么這里需要寫一個self setMyValue:value forUndefinedKey:key,但是必要性不是很大,就省略了
    }
}

- (id)myValueforKey:(NSString *)key {
    if (key == nil || key.length == 0) {
        return [NSNull new]; //其實不能這么寫的
    }
    //這里為了更方便,我就不做相關集合的方法查詢了
    NSString* funcName = [NSString stringWithFormat:@"gett%@:",key.capitalizedString];
    if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
       return [self performSelector:NSSelectorFromString(funcName)];
    }

    unsigned int count;
    BOOL flag = false;
    Ivar* vars = class_copyIvarList([self class], &count);
    for (NSInteger i = 0; i<count; i++) {
        Ivar var = vars[i];
        NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
        if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
        if ([keyName isEqualToString:key]) {
            flag = true;
            return     object_getIvar(self, var);
            break;
        }
    }
    if (!flag) {
        [self valueForUndefinedKey:key];//如果需要完全自定義,那么這里需要寫一個self myValueForUndefinedKey,但是必要性不是很大,就省略了
    }
   return [NSNull new]; //其實不能這么寫的
}
@end

Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";

[add setMyValue:nil forKey:@"area"]; //測試設置 nil value
[add setMyValue:@"UK" forKey:@"country"];
[add setMyValue:@"South" forKey:@"area"];
[add setMyValue:@"300169" forKey:@"postCode"];
NSLog(@"country:%@  province:%@ city:%@ postCode:%@",add.country,add.province,add.city,add._postCode);
NSString* postCode = [add myValueforKey:@"postCode"];
NSString* country = [add myValueforKey:@"country"];
NSLog(@"country:%@ postCode: %@",country,postCode);

//打印結果:
2017-04-16 23:12:**.302 KVC: country:UK  province:South city:Shen Zhen postCode:300169
2017-04-16 23:12:**.302 KVC: country:UK postCode: 300169

上面就是自己寫代碼實現(xiàn)KVC的部分功能。其中我省略了自定義KVC錯誤方法,省略了部分KVC搜索key的步驟,但是邏輯是很清晰明了的,后面的測試也符合預期。當然這只是我自己實現(xiàn)KVC的思路,Apple也許并不是這么做的。

KVC的正確性驗證

KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
這個方法的默認實現(xiàn)是去探索類里面是否有一個這樣的方法:-(BOOL)validate<Key>:error:如果有這個方法,就調用這個方法來返回,沒有的話就直接返回YES

@implementation Address
- (BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{  //在implementation里面加這個方法,它會驗證是否設了非法的value
    NSString* country = *value;
    country = country.capitalizedString;
    if ([country isEqualToString:@"Japan"]) {
        return NO;                                                                             //如果國家是日本,就返回NO,這里省略了錯誤提示,
    }
    return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果沒有重寫-(BOOL)-validate<Key>:error:,默認返回Yes
if (result) {
    NSLog(@"鍵值匹配");
    [add setValue:value forKey:key];
}
else{
    NSLog(@"鍵值不匹配"); //不能設為日本,基他國家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印結果 
2016-04-20 14:55:12.055 KVCDemo[867:58871] 鍵值不匹配
2016-04-20 14:55:12.056 KVCDemo[867:58871] country:China

如上面的代碼,當開發(fā)者需要驗證能不能用KVC設定某個值時,可以調用validateValue: forKey:這個方法來驗證,如果這個類的開發(fā)者實現(xiàn)了-(BOOL)validate<Key>:error:這個方法,那么KVC就會直接調用這個方法來返回,如果沒有,就直接返回YES,注意,KVC在設值時不會主動去做驗證,需要開發(fā)者手動去驗證。所以即使你在類里面寫了驗證方法,但是KVC因為不會去主動驗證,所以還是能夠設值成功。

KVC的使用

KVC在iOS開發(fā)中是絕不可少的利器,這種基于運行時的編程方式極大地提高了靈活性,簡化了代碼,甚至實現(xiàn)很多難以想像的功能,iOS開發(fā)中KVC的使用場景列舉。

①動態(tài)地取值和設值

利用KVC動態(tài)的取值和設值是最基本的用途了。相信每一個iOS開發(fā)者都能熟練掌握。

②用KVC來訪問和修改私有變量

對于類里的私有屬性,Objective-C是無法直接訪問的,但是KVC是可以的,請參考本文前面的Dog類的例子。

③Model和字典轉換

這是KVC強大作用的又一次體現(xiàn),請參考我寫的iOS開發(fā)技巧系列---打造強大的BaseMod系列文章,里面
充分地運用了KVC和Objc的runtime組合的技巧,只用了短短數行代碼就是完成了很多功能。

④修改一些控件的內部屬性

這也是iOS開發(fā)中必不可少的小技巧。眾所周知很多UI控件都由很多內部UI控件組合而成的,但是Apple度沒有提供這訪問這些空間的API,這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數情況可下可以解決這個問題。最常用的就是個性化UITextField中的placeHolderText了。
下面演示如果修改placeHolder的文字樣式。這里的關鍵點是如果獲取你要修改的樣式的屬性名,也就是key或者keyPath名。
修改placeHolder的樣式

⑤一般情況下可以運用runtime來獲取Apple不想開放的屬性名
let count:UnsafeMutablePointer<UInt32> =  UnsafeMutablePointer<UInt32>()
var properties = class_copyIvarList(UITextField.self, count)
while properties.memory.debugDescription !=  "0x0000000000000000"{
    let t = ivar_getName(properties.memory)
    let n = NSString(CString: t, encoding: NSUTF8StringEncoding)
    print(n) //打印出所有屬性,這里我用了Swift語言
    properties = properties.successor()
}

//上面省略了部分屬性
Optional(_disabledBackgroundView)
Optional(_systemBackgroundView)
Optional(_floatingContentView)
Optional(_contentBackdropView)
Optional(_fieldEditorBackgroundView)
Optional(_fieldEditorEffectView)
Optional(_displayLabel)
Optional(_placeholderLabel) //這個正是我想要修改的屬性。
Optional(_dictationLabel)
Optional(_suffixLabel)
Optional(_prefixLabel)
Optional(_iconView)
//省略了部分屬性

操作集合

Apple對KVC的valueForKey:方法作了一些特殊的實現(xiàn),比如說NSArray和NSSet這樣的容器類就實現(xiàn)了這些方法。所以可以用KVC很方便地操作集合
用KVC實現(xiàn)高階消息傳遞

當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是容器本身進行操作。結果會被添加進返回的容器中,這樣,開發(fā)者可以很方便的操作集合來返回另一個集合。

NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str  in arrCapStr) {
    NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length  in arrCapStrLength) {
    NSLog(@"%ld",(long)length.integerValue);
}
打印結果
2017-04-16 23:12:**.302 KVC: English
2017-04-16 23:12:**.302 KVC: Franch
2017-04-16 23:12:**.302 KVC: Chinese
2017-04-16 23:12:**.302 KVC: 7
2017-04-16 23:12:**.302 KVC: 6
2017-04-16 23:12:**.302 KVC: 7

方法capitalizedString被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執(zhí)行capitalizedString并返回一個包含結果的新的NSArray。從打印結果可以看出,所有String都成功以轉成了大寫。
同樣如果要執(zhí)行多個方法也可以用valueForKeyPath:方法。它先會對每一個成員調用 capitalizedString方法,然后再調用length,因為lenth方法返回是一個數字,所以返回結果以NSNumber的形式保存在新數組里。
用KVC中的函數操作集合

KVC同時還提供了很復雜的函數,主要有下面這些

①簡單集合運算符

簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,都表示啥不用我說了吧, 目前還不支持自定義。

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

@implementation Book
@end

Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 22;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 12;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 111;

Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 111;

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

打印結果
2017-04-16 23:12:**.302 KVC: sum:256.000000
2017-04-16 23:12:**.302 KVC: avg:64.000000
2017-04-16 23:12:**.302 KVC: count:4.000000
2017-04-16 23:12:**.302 KVC: min:12.000000
2017-04-16 23:12:**.302 KVC: max:111.000000
②對象運算符

比集合運算符稍微復雜,能以數組的方式返回指定的內容,一共有兩種:
@distinctUnionOfObjects

@unionOfObjects
它們的返回值都是NSArray,區(qū)別是前者返回的元素都是唯一的,是去重以后的結果;后者返回的元素是全集。
用法如下:

NSLog(@"distinctUnionOfObjects");
NSArray *arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrDistinct) {
    NSLog(@"%f", price.floatValue);
}
NSLog(@"unionOfObjects");
NSArray *arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrUnion) {
    NSLog(@"%f", price.floatValue);
}

2017-04-16 23:12:**.302 KVC: distinctUnionOfObjects
2017-04-16 23:12:**.302 KVC: 111.000000
2017-04-16 23:12:**.302 KVC: 12.000000
2017-04-16 23:12:**.302 KVC: 22.000000
2017-04-16 23:12:**.302 KVC: unionOfObjects
2017-04-16 23:12:**.302 KVC: 22.000000
2017-04-16 23:12:**.302 KVC: 12.000000
2017-04-16 23:12:**.302 KVC: 111.000000
2017-04-16 23:12:**.302 KVC: 111.000000

前者會將重復的價格去除后返回所有價格,后者直接返回所有的圖書價格。
③Array和Set操作符

這種情況更復雜了,說的是集合中包含集合的情況,我們執(zhí)行了如下的一段代碼:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:該操作會返回一個數組,這個數組包含不同的對象,不同的對象是在從關鍵路徑到操作器右邊的被指定的屬性里
@unionOfArrays 該操作會返回一個數組,這個數組包含的對象是在從關鍵路徑到操作器右邊的被指定的屬性里和@distinctUnionOfArrays不一樣,重復的對象不會被移除
@distinctUnionOfSets 和@distinctUnionOfArrays類似。因為Set本身就不支持重復。

------來自網絡轉載

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iO...
    Fendouzhe閱讀 732評論 0 6
  • KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iO...
    黑暗中的孤影閱讀 50,248評論 74 441
  • 簡介 KVC(Key-value coding)鍵值編碼,翻譯一下就是指iOS的開發(fā)中,可以允許開發(fā)者通過Key名...
    6ffd6634d577閱讀 1,498評論 1 9
  • KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iO...
    朽木自雕也閱讀 1,693評論 6 1
  • KVC(Key-value coding)鍵值編碼,iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對象的屬性,...
    CALayer_Sai閱讀 2,691評論 0 4

友情鏈接更多精彩內容