1. 程式人生 > >ios KVC內部機制探索

ios KVC內部機制探索

KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iOS的開發中,可以允許開發者通過Key名直接訪問物件的屬性,或者給物件的屬性賦值。而不需要呼叫明確的存取方法。這樣就可以在執行時動態在訪問和修改物件的屬性。而不是在編譯時確定,這也是iOS開發中的黑魔法之一。很多高階的iOS開發技巧都是基於KVC實現的。目前網上關於KVC的文章在非常多,有的只是簡單地說了下用法,有的講得深入但是在使用場景和最佳實踐沒有說明,我寫下這遍文章就是給大家詳解一個最完整最詳細的KVC。

KVC在iOS中的定義

無論是Swift還是Objective-C,KVC的定義都是對NSObject的擴充套件來實現的(Objective-c中有個顯式的NSKeyValueCoding

類別名,而Swift沒有,也不需要)。所以對於所有繼承了NSObject在型別,也就是幾乎所有的Objective-C物件都能使用KVC(一些純Swift類和結構體是不支援KVC的),下面是KVC最為重要的四個方法

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

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

+ (BOOL)accessInstanceVariablesDirectly;
//預設返回YES,表示如果沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜尋成員,設定成NO就不這樣搜尋

- (BOOL)validateValue:(inout
id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError; //KVC提供屬性值確認的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設定新值並返回錯誤原因。 - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; //這是集合操作的API,裡面還有一系列這樣的API,如果屬性是一個NSMutableArray,那麼可以用這個方法來返回 - (nullable id)valueForUndefinedKey:(NSString *)key; //如果Key不存在,且沒有KVC無法搜尋到任何和Key有關的欄位或者屬性,則會呼叫這個方法,預設是丟擲異常 - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; //和上一個方法一樣,只不過是設值。 - (void)setNilValueForKey:(NSString *)key; //如果你在SetValue方法時面給Value傳nil,則會呼叫這個方法 - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys; //輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。

上面的這些方法在碰到特殊情況或者有特殊需求還是會用到的,所以也是可以瞭解一下。後面的程式碼示例會有講到其中的一些方法。
同時蘋果對一些容器類比如NSArray或者NSSet等,KVC有著特殊的實現。建議有基礎的或者英文好的開發者直接去看蘋果的官方文件,相信你會對KVC的理解更上一個臺階。

KVC是怎麼尋找Key的

KVC是怎麼使用的,我相信絕大多數的開發者都很清楚,我在這裡就不再寫簡單的使用KVC來設值和取值的程式碼了,首頁我們來探討KVC在內部是按什麼樣的順序來尋找key的。
當呼叫setValue:屬性值 forKey:@”name“的程式碼時,底層的執行機制如下:

  • 程式優先呼叫set<Key>:屬性值方法,程式碼通過setter方法完成設定。注意,這裡的<key>是指成員變數名,首字母大清寫要符合KVC的全名規則,下同
  • 如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,預設該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那麼在這一步KVC會執行setValue:forUNdefinedKey:方法,不過一般開發者不會這麼做。所以KVC機制會搜尋該類裡面有沒有名為_<key>的成員變數,無論該變數是在類介面部分定義,還是在類實現部分定義,也無論用了什麼樣的訪問修飾符,只在存在以_<key>命名的變數,KVC都可以對該成員變數賦值。
  • 如果該類即沒有set<Key>:方法,也沒有_<key>成員變數,KVC機制會搜尋_is<Key>的成員變數,
  • 和上面一樣,如果該類即沒有set<Key>:方法,也沒有_<key>_is<Key>成員變數,KVC機制再會繼續搜尋<key>is<Key>的成員變數。再給它們賦值。
  • 如果上面列出的方法或者成員變數都不存在,系統將會執行該物件的setValue:forUNdefinedKey:方法,預設是丟擲異常。

如果開發者想讓這個類禁用KVC裡,那麼重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時,會直接用setValue:forUndefinedKey:方法。

下面我們來讓程式碼來測試一下上面的KVC機制

@interface Dog : NSObject
@end
@implementation Dog
{
     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(@"出現異常,該key不存在%@",key);
    return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
     NSLog(@"出現異常,該key不存在%@",key);
}
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Dog* dog = [Dog new];
        [dog setValue:@"newName" forKey:@"name"];
        NSString* name = [dog valueForKey:@"toSetName"];
        NSLog(@"%@",name);
    }
    return 0;
}

首先我們先重寫accessInstanceVariablesDirectly方法讓其返回NO,再執行程式碼(注意上面註釋的部分),Xcode直接打印出

2016-04-15 15:52:12.039 DemoKVC[9681:287627] 出現異常,該key不存在name
2016-04-15 15:52:12.040 DemoKVC[9681:287627] 出現異常,該key不存在toSetName
2016-04-15 15:52:12.040 DemoKVC[9681:287627] (null)

這說明了重寫+(BOOL)accessInstanceVariablesDirectly方法讓其返回NO後,KVC找不到SetName:方法後,不再去找name系列成員變數,而是直接呼叫forUndefinedKey方法
所以開發者如果不想讓自己的類實現KVC,就可以這麼做。
下面那兩個setter和gettr的註釋取消掉,再把

NSString* name = [dog valueForKey:@"toSetName"]; 換成 NSString* name = [dog valueForKey:@"name"];

XCode就可以正確地打印出正確的值了

2016-04-15 15:56:22.130 DemoKVC[9726:289258] newName

下面再註釋到accessInstanceVariablesDirectly方法,就能測試其他的key查詢順序了,為了節省篇幅,剩下的的KVC對於key尋找機制就不在這裡展示了,有興趣的讀者可以寫程式碼去驗證。

當呼叫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的子類),呼叫這個代理集合的方法,或者說給這個代理集合傳送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:

下面再上程式碼測試

@interface TwoTimesArray : NSObject
-(void)incrementCount;
-(NSUInteger)countOfNumbers;
-(id)objectInNumbersAtIndex:(NSUInteger)index;
@end
@interface TwoTimesArray()
@property (nonatomic,readwrite,assign) NSUInteger count;
@property (nonatomic,copy) NSString* arrName;
@end
@implementation TwoTimesArray
-(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


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TwoTimesArray* arr = [TwoTimesArray 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);

    }
    return 0;
}
//列印結果 
2016-04-17 15:39:42.214 KVCDemo[1088:74481] 10
2016-04-17 15:39:42.215 KVCDemo[1088:74481] NSKeyValueArray
2016-04-17 15:41:24.713 KVCDemo[1102:75424] 0:0     1:2     2:4     3:6                 //太明顯了,直接呼叫-(id)objectInNumbersAtIndex:(NSUInteger)index;方法
2016-04-17 15:39:42.215 KVCDemo[1088:74481] 1
2016-04-17 15:39:42.215 KVCDemo[1088:74481] 2
2016-04-17 15:39:42.215 KVCDemo[1088:74481] newName

很明顯,上面的程式碼充分說明了說明了KVC在呼叫ValueforKey:@”name“時搜尋key的機制。不過還有些功能沒有全部列出,有興趣的讀者可以寫程式碼去驗證。

在KVC中使用KeyPath

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

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過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
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        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);
    }
    return 0;
}
//列印結果 
2016-04-17 15:55:22.487 KVCDemo[1190:82636] country1:China   country2:China
2016-04-17 15:55:22.489 KVCDemo[1190:82636] country1:USA   country2:USA

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

KVC如何處理異常

KVC中最常見的異常就是不小心使用了錯誤的Key,或者在設值中不小心傳遞了nil的值,KVC中有專門的方法來處理這些異常。
通常在用KVC操作Model時,丟擲異常的那兩個方法是需要重寫的。雖然一般很小出現傳遞了錯誤的Key值這種情況,但是如果不小心出現了,直接丟擲異常讓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
//打印出
2016-04-17 16:19:55.298 KVCDemo[1304:92472] 不能將age設成nil

KVC處理非物件和自定義物件

不是每一個方法都返回物件,但是valueForKey:總是返回一個id物件,如果原本的變數型別是值型別或者結構體,返回值會封裝成NSNumber或者NSValue物件。這兩個類會處理從數字,布林值到指標和結構體任何型別。然後開以者需要手動轉換成原來的型別。儘管valueForKey:會自動將值型別封裝成物件,但是setValue:forKey:卻不行。你必須手動將值型別轉換成NSNumber或者NSValue型別,才能傳遞過去。

對於自定義物件,KVC也會正確以設值和取值。因為傳遞進去和取出來的都是id型別,所以需要開發者自己擔保型別的正確性,執行時Objective-C在傳送訊息的會檢查型別,如果錯誤會直接丟擲異常。

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);
//列印結果
2016-04-17 16:29:36.349 KVCDemo[1346:95910] country1:England   country2:England

KVC與容器類

物件的屬性可以是一對一的,也可以是一對多的。一對多的屬性要麼是有序的(陣列),要麼是無序的(陣列)
不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)一般可以使用valueForKey:來獲取。比如有一個叫items的NSArray屬性,你可以用valurForKey:@"items"來獲取這個屬性。前面valueForKey:的key搜尋模式中,我們發現其實KVC使用了一種更靈活的方式來管理容器類。蘋果的官方文件也推薦我們實現這些這些特殊的訪問器。

而當物件的屬性是可變的容器時,對於有序的容器,可以用下面的方法:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

該方法返回一個可變有序陣列,如果呼叫該方法,KVC的搜尋順序如下

  • 搜尋insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes 格式的方法
    如果至少找到一個insert方法和一個remove方法,那麼同樣返回一個可以響應NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray2),那麼給這個代理集合傳送NSMutableArray的方法,以insertObject:in<Key>AtIndex: , removeObjectFrom<Key>AtIndex: 或者 insert<Key>AdIndexes , remove<Key>AtIndexes組合的形式呼叫。還有兩個可選實現的介面:replaceOnjectAtIndex:withObject: , replace<Key>AtIndexes:with<Key>: 。
  • 如果上步的方法沒有找到,則搜尋set<Key>: 格式的方法,如果找到,那麼傳送給代理集合的NSMutableArray最終都會呼叫set<Key>:方法。 也就是說,mutableArrayValueForKey:取出的代理集合修改後,用·set<Key>:· 重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。
  • 如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(預設行為),會按_<key>,<key>,的順序搜尋成員變數名,如果找到,那麼傳送的NSMutableArray訊息方法直接交給這個成員變數處理。
  • 如果還是找不到,呼叫valueForUndefinedKey:
    關於mutableArrayValueForKey:的適用場景,我在網上找了很多,發現其一般是用在對NSMutableArray新增Observer上。
    如果物件屬性是個NSMutableArray、NSMutableSet、NSMutableDictionary等集合型別時,你給它新增KVO時,你會發現當你新增或者移除元素時並不能接收到變化。因為KVO的本質是系統監測到某個屬性的記憶體地址或常量改變時,會新增上- (void)willChangeValueForKey:(NSString *)key
    - (void)didChangeValueForKey:(NSString *)key方法來發送通知,所以一種解決方法是手動呼叫者兩個方法,但是並不推薦,你永遠無法像系統一樣真正知道這個元素什麼時候被改變。另一種便是利用使用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];

列印結果
2016-04-18 17:48:22.675 KVCDemo[32647:505864] {
    indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
    kind = 2;
    new =     (
        1
    );
}
2016-04-18 17:48:22.677 KVCDemo[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"];這樣寫時才能正確地觸發KVO。打印出來的資料中,可以看出這次操作的詳情,kind可能是指操作方法(我還不是很確認),old和new並不是成對出現的,當加添新資料時是new,刪除資料時是old

而對於無序的容器,可以用下面的方法:

- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;

該方法返回一個可變的無序陣列如果呼叫該方法,KVC的搜尋順序如下

  • 搜尋addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key> 格式的方法
    如果至少找到一個insert方法和一個remove方法,那麼同樣返回一個可以響應NSMutableSet所有方法代理集合(類名是NSKeyValueFastMutableSet2),那麼給這個代理集合傳送NSMutableSet的方法,以addObject<Key>Object: , remove<Key>Object: 或者 add<Key> , remove<Key>組合的形式呼叫。還有兩個可選實現的介面:intersect<Key> , set<Key>:
  • 如果receiver是ManagedObject,那麼就不會繼續搜尋。
  • 如果上步的方法沒有找到,則搜尋set<Key>: 格式的方法,如果找到,那麼傳送給代理集合的NSMutableSet最終都會呼叫set<Key>:方法。 也就是說,mutableSetValueForKey取出的代理集合修改後,用set<Key>: 重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。
  • 如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(預設行為),會按_<key>,<key>,的順序搜尋成員變數名,如果找到,那麼傳送的NSMutableSet訊息方法直接交給這個成員變數處理。
  • 如果還是找不到,呼叫valueForUndefinedKey:
    可見,除了檢查receiver是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:的表現行為和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);

//列印結果
2016-04-19 11:54:30.846 KVCDemo[6607:198900] {
    city = "Shen Zhen";
    country = China;
    district = "Nan Shan";
    province = "Guang Dong";
}
2016-04-19 11:54:30.847 KVCDemo[6607:198900] country:USA  province:california city:Los angle

打印出來的結果完全符合預期。

KVC的內部實現機制

前面我們對析了KVC是怎麼搜尋key的。所以如果明白了key的搜尋順序,是可以自己寫程式碼實現KVC的。在考慮到集合和keyPath的情況下,KVC的實現會比較複雜,我們只寫程式碼實現最普通的取值和設值即可。

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

//列印結果:

2016-04-19 14:29:39.498 KVCDemo[7273:275129] country:UK  province:South city:Shen Zhen postCode:300169
2016-04-19 14:29:39.499 KVCDemo[7273:275129] country:UK postCode: 300169

上面就是自己寫程式碼實現KVC的部分功能。其中我省略了自定義KVC錯誤方法,省略了部分KVC搜尋key的步驟,但是邏輯是很清晰明瞭的,後面的測試也符合預期。當然這只是我自己實現KVC的思路,Apple也許並不是這麼做的。

KVC的正確性驗證

KVC提供了屬性值,用來驗證key對應的Value是否可用的方法

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

這個方法的預設實現是去探索類裡面是否有一個這樣的方法:-(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

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

KVC的使用

KVC在iOS開發中是絕不可少的利器,這種基於執行時的程式設計方式極大地提高了靈活性,簡化了程式碼,甚至實現很多難以想像的功能,KVC也是許多iOS開發黑魔法的基礎。下面我來列舉iOS開發中KVC的使用場景

動態地取值和設值

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

用KVC來訪問和修改私有變數

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

Model和字典轉換

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

修改一些控制元件的內部屬性

這也是iOS開發中必不可少的小技巧。眾所周知很多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)
//下面省略了部分屬性

可以從裡面看到其他還有很多東西可以修改,運用KVC設值可以獲得自己想要的效果。

操作集合

Apple對KVC的valueForKey:方法作了一些特殊的實現,比如說NSArray和NSSet這樣的容器類就實現了這些方法。所以可以用KVC很方便地操作集合

用KVC實現高階訊息傳遞