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最為重要的四個方法
·
當然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些
···+ (BOOL)accessInstanceVariablesDirectly;//默認返回YES,表示如果沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索- (BOOL)validateValue:(inoutid__nullable* __nonnull)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;//KVC提供屬性值正確性?驗證的API,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設置新值并返回錯誤原因。- (NSMutableArray*)mutableArrayValueForKey:(NSString*)key;//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個NSMutableArray,那么可以用這個方法來返回。- (nullableid)valueForUndefinedKey:(NSString*)key;//如果Key不存在,且沒有KVC無法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。- (void)setValue:(nullableid)value forUndefinedKey:(NSString*)key;//和上一個方法一樣,但這個方法是設值。- (void)setNilValueForKey:(NSString*)key;//如果你在SetValue方法時面給Value傳nil,則會調用這個方法- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;//輸入一組key,返回該組key對應的Value,再轉成字典返回,用于將Model轉到字典。···
上面的這些方法在碰到特殊情況或者有特殊需求還是會用到的,所以也是可以c了解一下。后面的代碼示例會有講到其中的一些方法。
同時蘋果對一些容器類比如NSArray或者NSSet等,KVC有著特殊的實現。建議有基礎的或者英文好的開發者直接去看蘋果的官方文檔,相信你會對KVC的理解更上一個臺階。
可能有些讀者不知道怎么查官方文檔,在這里說明一下。打開Xcode,查看最上面的菜單,點最后一個Help -> Documentation and API Reference,然后就可以打開官方文檔了。
Xcode 官方文檔
KVC是怎么尋找Key的
KVC是怎么使用的,我相信絕大多數的開發者都很清楚,我在這里就不再寫簡單的使用KVC來設值和取值的代碼了,首?先我們來探討KVC在內部是按什么樣的順序來尋找key的。
設值
當調用setValue:屬性值 forKey:@”name“的代碼時,底層的執行機制如下:
程序優先調用set:屬性值方法,代碼通過setter方法完成設置。注意,這里的是指成員變量名,首字母大小寫要符合KVC的命名規則,下同
如果沒有找到setName:方法,KVC機制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認該方法會返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執行setValue:forUndefinedKey:方法,不過一般開發者不會這么做。所以KVC機制會搜索該類里面有沒有名為_的成員變量,無論該變量是在類接口處定義,還是在類實現處定義,也無論用了什么樣的訪問修飾符,只在存在以_命名的變量,KVC都可以對該成員變量賦值。
如果該類即沒有set:方法,也沒有_成員變量,KVC機制會搜索_is的成員變量。
和上面一樣,如果該類即沒有set:方法,也沒有_和_is成員變量,KVC機制再會繼續搜索和is的成員變量。再給它們賦值。
如果上面列出的方法或者成員變量都不存在,系統將會執行該對象的setValue:forUndefinedKey:方法,默認是拋出異常。
如果開發者想讓這個類禁用KVC里,那么重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set:屬性名時,會直接用setValue:forUndefinedKey:方法。
下面我們來讓代碼來測試一下上面的KVC機制
@interfaceDog:NSObject@end@implementationDog{NSString* toSetName;NSString* isName;//NSString* name;NSString* _name;NSString* _isName;}// -(void)setName:(NSString*)name{//? ? toSetName = name;// }//-(NSString*)getName{//? ? return toSetName;//}+(BOOL)accessInstanceVariablesDirectly{returnNO;}-(id)valueForUndefinedKey:(NSString*)key{NSLog(@"出現異常,該key不存在%@",key);returnnil;}-(void)setValue:(id)value forUndefinedKey:(NSString*)key{NSLog(@"出現異常,該key不存在%@",key);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...Dog* dog = [Dog new];? ? ? ? [dog setValue:@"newName"forKey:@"name"];NSString* name = [dog valueForKey:@"toSetName"];NSLog(@"%@",name);? ? }return0;}
首先我們先重寫accessInstanceVariablesDirectly方法讓其返回NO,再運行代碼(注意上面注釋的部分),Xcode直接打印出
2016-04-1515:52:12.039DemoKVC[9681:287627]出現異常,該key不存在name2016-04-1515:52:12.040DemoKVC[9681:287627]出現異常,該key不存在toSetName2016-04-1515:52:12.040DemoKVC[9681:287627](null)
這說明了重寫+(BOOL)accessInstanceVariablesDirectly方法讓其返回NO后,KVC找不到setName:方法后,不再去找name系列成員變量,而是直接調用setValue:forUndefinedKey:方法
所以開發者如果不想讓自己的類實現KVC,就可以這么做。
下面那兩個setter和getter的注釋取消掉,再把
NSString* name = [dog valueForKey:@"toSetName"]; 換成NSString* name = [dog valueForKey:@"name"];
XCode就可以正確地打印出正確的值了
2016-04-1515:56:22.130DemoKVC[9726:289258]newName
下面再注釋掉accessInstanceVariablesDirectly方法,就能測試其他的key查找順序了,為了節省篇幅,剩下的的KVC對于key尋找機制就不在這里展示了,有興趣的讀者可以寫代碼去驗證。
取值
當調用valueForKey:@”name“的代碼時,KVC對key的搜索方式不同于setValue:屬性值 forKey:@”name“,其搜索方式如下:
首先按get,,is的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者Int等值類型, 會將其包裝成一個NSNumber對象。
如果上面的getter沒有找到,KVC則會查找countOf,objectInAtIndex或AtIndexes格式的方法。如果countOf方法和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所?有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調用這個代理集合的方法,或者說給這個代理集合發送屬于NSArray的方法,就會以countOf,objectInAtIndex?或AtIndexes這幾個方法組合的形式調用。還有一個可選的get:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
如果上面的方法沒有找到,那么會同時查找countOf,enumeratorOf,memberOf格式的方法。如果這三個方法都找到,那么就返回一個可以響應NSSet所的方法的代理集合,和上面一樣,給這個代理集合發NSSet的消息,就會以countOf,enumeratorOf,memberOf組合的形式調用。
可能上面的兩條查找方案對讀者不好理解,簡單來說就是如果你在自己的類自定義了KVC的實現,并且實現了上面的方法,那么恭喜你,你可以?將返回的對象當數組(NSArray)用了,詳情見下面的示例代碼
如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),那么和先前的設值一樣,會按_,_is,,is的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會直接調用valueForUndefinedKey:
還沒有找到的話,調用valueForUndefinedKey:
下面再上代碼測試
@interfaceTwoTimesArray:NSObject-(void)incrementCount;-(NSUInteger)countOfNumbers;-(id)objectInNumbersAtIndex:(NSUInteger)index;@end@interfaceTwoTimesArray()@property(nonatomic,readwrite,assign)NSUIntegercount;@property(nonatomic,copy)NSString* arrName;@end@implementationTwoTimesArray-(void)incrementCount{self.count ++;}-(NSUInteger)countOfNumbers{returnself.count;-(id)objectInNumbersAtIndex:(NSUInteger)index{//當key使用numbers時,KVC會找到這兩個方法。return@(index *2);}-(NSInteger)getNum{//第一個,自己一個一個注釋試return10;}-(NSInteger)num{//第二個return11;}-(NSInteger)isNum{//第三個return12;}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{? ? ? ? TwoTimesArray* arr = [TwoTimesArray new];NSNumber* num =? [arr valueForKey:@"num"];NSLog(@"%@",num);idar = [arr valueForKey:@"numbers"];NSLog(@"%@",NSStringFromClass([arclass]));NSLog(@"0:%@? ? 1:%@? ? 2:%@? ? 3:%@",ar[0],ar[1],ar[2],ar[3]);? ? ? ? [arr incrementCount];//count加1NSLog(@"%lu",(unsignedlong)[ar count]);//打印出1[arr incrementCount];//count再加1NSLog(@"%lu",(unsignedlong)[ar count]);//打印出2[arr setValue:@"newName"forKey:@"arrName"];NSString* name = [arr valueForKey:@"arrName"];NSLog(@"%@",name);? ? ? ? ? ? }return0;}//打印結果 2016-04-1715:39:42.214KVCDemo[1088:74481]102016-04-1715:39:42.215KVCDemo[1088:74481]NSKeyValueArray2016-04-1715:41:24.713KVCDemo[1102:75424]0:01:22:43:6//太明顯了,直接調用-(id)objectInNumbersAtIndex:(NSUInteger)index;方法2016-04-1715:39:42.215KVCDemo[1088:74481]12016-04-1715:39:42.215KVCDemo[1088:74481]22016-04-1715:39:42.215KVCDemo[1088:74481] newName
很明顯,上面的代碼充分說明了說明了KVC在調用ValueforKey:@”name“時搜索key的機制。不過還有些功能沒有全部列出,有興趣的讀者可以寫代碼去驗證。
在KVC中使用keyPath
然而在開發過程中,一個類的成員變量有可能是自定義類或其他的復雜數據類型,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性,但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath。
- (nullableid)valueForKeyPath:(NSString*)keyPath;//通過KeyPath來取值- (void)setValue:(nullableid)value forKeyPath:(NSString*)keyPath;//通過KeyPath來設值
@interfaceAddress:NSObject@end@interfaceAddress()@property(nonatomic,copy)NSString* country;@end@implementationAddress@end@interfacePeople:NSObject@end@interfacePeople()@property(nonatomic,copy)NSString* name;@property(nonatomic,strong) Address* address;@property(nonatomic,assign)NSIntegerage;@end@implementationPeople@endintmain(intargc,constchar* 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);? ? }return0;}//打印結果 2016-04-1715:55:22.487KVCDemo[1190:82636] country1:China? country2:China2016-04-1715:55:22.489KVCDemo[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 uncaughtexception'NSInvalidArgumentException', reason:'[ setNilValueForKey]: could not set nil as the value for the key age.'// 調用setNilValueForKey拋出異常
如果重寫setNilValueForKey:就沒問題了
@implementationPeople-(void)setNilValueForKey:(NSString*)key{NSLog(@"不能將%@設成nil",key);}@end//打印出2016-04-1716:19:55.298KVCDemo[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-1716:29:36.349KVCDemo[1346:95910] country1:England? country2:England
KVC與容器類
對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要么是有序的(數組),要么是無序的(集合)。
不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)一般可以使用valueForKey:來獲取。比如有一個叫items的NSArray屬性,你可以用valurForKey:@"items"來獲取這個屬性。前面valueForKey:的key搜索模式中,我們發現其實KVC使用了一種更靈活的方式來管理容器類。蘋果的官方文檔也推薦我們實現這些這些特殊的訪問器。
而當對象的屬性是可變的容器時,對于有序的容器,可以用下面的方法:
- (NSMutableArray*)mutableArrayValueForKey:(NSString*)key;
該方法返回一個可變有序數組,如果調用該方法,KVC的搜索順序如下
搜索insertObject:inAtIndex:,removeObjectFromAtIndex:或者insertAdIndexes,removeAtIndexes格式的方法
如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray2),那么給這個代理集合發送NSMutableArray的方法,以insertObject:inAtIndex:,removeObjectFromAtIndex:或者insertAdIndexes,removeAtIndexes組合的形式調用。還有兩個可選實現的接口:replaceOnjectAtIndex:withObject:,replaceAtIndexes:with:。
如果上步的方法沒有找到,則搜索set:格式的方法,如果找到,那么發送給代理集合的NSMutableArray最終都會調用set:方法。 也就是說,mutableArrayValueForKey:取出的代理集合修改后,用set:重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。
如果上一步的方法還還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_,,的順序搜索成員變量名,如果找到,那么發送的NSMutableArray消息方法直接交給這個成員變量處理。
如果還是找不到,則調用valueForUndefinedKey:。
關于mutableArrayValueForKey:的適用場景,我在網上找了很多,發現其一般是用在對NSMutableArray添加Observer上。如果對象屬性是個NSMutableArray、NSMutableSet、NSMutableDictionary等集合類型時,你給它添加KVO時,你會發現當你添加或者移除元素時并不能接收到變化。因為KVO的本質是系統監測到某個屬性的內存地址或常量改變時,會添加上- (void)willChangeValueForKey:(NSString *)key和- (void)didChangeValueForKey:(NSString *)key方法來發送通知,所以一種解決方法是手動調用者兩個方法,但是并不推薦,你永遠無法像系統一樣真正知道這個元素什么時候被改變。另一種便是利用使用mutableArrayValueForKey:了。
@interfacedemo:NSObject@property(nonatomic,strong)NSMutableArray* arr;@end@implementationdemo-(id)init{if(self== [superinit]){? ? ? ? _arr = [NSMutableArraynew];? ? ? ? [selfaddObserver:selfforKeyPath:@"arr"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:nil];? ? }returnself;}-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context{NSLog(@"%@",change);}-(void)dealloc{? ? [selfremoveObserver:selfforKeyPath:@"arr"];//一定要在dealloc里面移除觀察}-(void)addItem{? ? [_arr addObject:@"1"];}-(void)addItemObserver{? ? [[selfmutableArrayValueForKey:@"arr"] addObject:@"1"];}-(void)removeItemObserver{? ? [[selfmutableArrayValueForKey:@"arr"] removeLastObject];}@end然后再:demo* d = [demo new];[d addItem];[d addItemObserver];[d removeItemObserver];? ? ? ? 打印結果2016-04-1817:48:22.675KVCDemo[32647:505864] {? ? indexes ="<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";? ? kind =2;? ? new =? ? (1);}2016-04-1817:48:22.677KVCDemo[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的搜索順序如下
搜索addObjectObject:,removeObject:或者add,remove格式的方法
如果至少找到一個insert方法和一個remove方法,那么同樣返回一個可以響應NSMutableSet所有方法代理集合(類名是NSKeyValueFastMutableSet2),那么給這個代理集合發送NSMutableSet的方法,以addObjectObject:,removeObject:或者add,remove組合的形式調用。還有兩個可選實現的接口:intersect , set:。
如果receiver是ManagedObject,那么就不會繼續搜索。
如果上一步的方法沒有找到,則搜索set: 格式的方法,如果找到,那么發送給代理集合的NSMutableSet最終都會調用set:方法。 也就是說,mutableSetValueForKey取出的代理集合修改后,用set:重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。
如果上一步的方法還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認行為),會按_,的順序搜索成員變量名,如果找到,那么發送的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 *)dictionaryWithValuesForKeys:(NSArray *)keys;- (void)setValuesForKeysWithDictionary:(NSDictionary *)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-1911:54:30.846KVCDemo[6607:198900] {? ? city ="Shen Zhen";? ? country = China;? ? district ="Nan Shan";? ? province ="Guang Dong";}2016-04-1911:54:30.847KVCDemo[6607:198900] country:USA? province:california city:Los angle
打印出來的結果完全符合預期。
KVC的內部實現機制
前面我們對析了KVC是怎么搜索key的。所以如果明白了key的搜索順序,是可以自己寫代碼實現KVC的。在考慮到集合和keyPath的情況下,KVC的實現會比較復雜,我們只寫代碼實現最普通的取值和設值即可。
@interfaceNSObject(MYKVC)-(void)setMyValue:(id)value forKey:(NSString*)key;-(id)myValueforKey:(NSString*)key;@end@implementationNSObject(MYKVC)-(void)setMyValue:(id)value forKey:(NSString*)key{if(key ==nil|| key.length ==0) {//key名要合法return;? ? }if([value isKindOfClass:[NSNullclass]]) {? ? ? ? [selfsetNilValueForKey:key];//如果需要完全自定義,那么這里需要寫一個setMyNilValueForKey,但是必要性不是很大,就省略了return;? ? }if(![value isKindOfClass:[NSObjectclass]]) {@throw@"must be s NSObject type";return;? ? }NSString* funcName = [NSStringstringWithFormat:@"set%@:",key.capitalizedString];if([selfrespondsToSelector:NSSelectorFromString(funcName)]) {//默認優先調用set方法[selfperformSelector:NSSelectorFromString(funcName) withObject:value];return;? ? }unsignedintcount;BOOLflag =false;? ? Ivar* vars = class_copyIvarList([selfclass], &count);for(NSIntegeri =0; i
上面就是自己寫代碼實現KVC的部分功能。其中我省略了自定義KVC錯誤方法,省略了部分KVC搜索key的步驟,但是邏輯是很清晰明了的,后面的測試也符合預期。當然這只是我自己實現KVC的思路,Apple也許并不是這么做的。
KVC的正確性驗證
KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
- (BOOL)validateValue:(inoutid__nullable* __nonnull)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;
這個方法的默認實現是去探索類里面是否有一個這樣的方法:-(BOOL)validate:error:如果有這個方法,就調用這個方法來返回,沒有的話就直接返回YES
@implementationAddress-(BOOL)validateCountry:(id*)value error:(outNSError* _Nullable __autoreleasing *)outError{//在implementation里面加這個方法,它會驗證是否設了非法的valueNSString* country = *value;? ? country = country.capitalizedString;if([country isEqualToString:@"Japan"]) {returnNO;//如果國家是日本,就返回NO,這里省略了錯誤提示,}returnYES;}@endNSError* error;idvalue =@"japan";NSString* key =@"country";BOOLresult = [add validateValue:&value forKey:key error:&error];//如果沒有重寫-(BOOL)-validate:error:,默認返回Yesif(result) {NSLog(@"鍵值匹配");? ? [add setValue:value forKey:key];}else{NSLog(@"鍵值不匹配");//不能設為日本,基他國家都行}NSString* country = [add valueForKey:@"country"];NSLog(@"country:%@",country);//打印結果 2016-04-2014:55:12.055KVCDemo[867:58871] 鍵值不匹配2016-04-2014:55:12.056KVCDemo[867:58871] country:China
如上面的代碼,當開發者需要驗證能不能用KVC設定某個值時,可以調用validateValue: forKey:這個方法來驗證,如果這個類的開發者實現了-(BOOL)validate: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 =? UnsafeMutablePointer()var properties = class_copyIvarList(UITextField.self, count)whileproperties.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實現高階消息傳遞
當對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是容器本身進行操作。結果會被添加進返回的容器中,這樣,開發者可以很方便的操作集合來返回另一個集合。
NSArray* arrStr = @[@"english",@"franch",@"chinese"];NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];for(NSString* strinarrCapStr) {NSLog(@"%@",str);}NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];for(NSNumber* lengthinarrCapStrLength) {NSLog(@"%ld",(long)length.integerValue);}打印結果2016-04-2016:29:14.239KVCDemo[1356:118667] English2016-04-2016:29:14.240KVCDemo[1356:118667] Franch2016-04-2016:29:14.240KVCDemo[1356:118667] Chinese2016-04-2016:29:14.240KVCDemo[1356:118667]72016-04-2016:29:14.241KVCDemo[1356:118667]62016-04-2016:29:14.241KVCDemo[1356:118667]7
方法capitalizedString被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執行capitalizedString并返回一個包含結果的新的NSArray。從打印結果可以看出,所有String都成功以轉成了大寫。
同樣如果要執行多個方法也可以用valueForKeyPath:方法。它先會對每一個成員調用capitalizedString方法,然后再調用length,因為lenth方法返回是一個數字,所以返回結果以NSNumber的形式保存在新數組里。
用KVC中的函數操作集合
KVC同時還提供了很復雜的函數,主要有下面這些
①簡單集合運算符
簡單集合運算符共有@avg, @count , @max , @min ,@sum5種,都表示啥不用我說了吧, 目前還不支持自定義。
@interfaceBook:NSObject@property(nonatomic,copy)NSString* name;@property(nonatomic,assign)CGFloatprice;@end@implementationBook@endBook *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);打印結果2016-04-2016:45:54.696KVCDemo[1484:127089] sum:256.0000002016-04-2016:45:54.697KVCDemo[1484:127089] avg:64.0000002016-04-2016:45:54.697KVCDemo[1484:127089] count:4.0000002016-04-2016:45:54.697KVCDemo[1484:127089] min:12.0000002016-04-2016:45:54.697KVCDemo[1484:127089] max:111.000000
②對象運算符
比集合運算符稍微復雜,能以數組的方式返回指定的內容,一共有兩種:
@distinctUnionOfObjects
@unionOfObjects
它們的返回值都是NSArray,區別是前者返回的元素都是唯一的,是去重以后的結果;后者返回的元素是全集。
用法如下:
NSLog(@"distinctUnionOfObjects");NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];for(NSNumber*priceinarrDistinct) {NSLog(@"%f",price.floatValue);}NSLog(@"unionOfObjects");NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];for(NSNumber*priceinarrUnion) {NSLog(@"%f",price.floatValue);}2016-04-2016:47:34.490KVCDemo[1522:128840] distinctUnionOfObjects2016-04-2016:47:34.490KVCDemo[1522:128840]111.0000002016-04-2016:47:34.490KVCDemo[1522:128840]12.0000002016-04-2016:47:34.490KVCDemo[1522:128840]22.0000002016-04-2016:47:34.490KVCDemo[1522:128840] unionOfObjects2016-04-2016:47:34.490KVCDemo[1522:128840]22.0000002016-04-2016:47:34.490KVCDemo[1522:128840]12.0000002016-04-2016:47:34.490KVCDemo[1522:128840]111.0000002016-04-2016:47:34.490KVCDemo[1522:128840]111.000000
前者會將重復的價格去除后返回所有價格,后者直接返回所有的圖書價格。(因為只返回價格,沒有返回圖書,感覺用處不大。)
③Array和Set操作符
這種情況更復雜了,說的是集合中包含集合的情況,我們執行了如下的一段代碼:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:該操作會返回一個數組,這個數組包含不同的對象,不同的對象是在從關鍵路徑到操作器右邊的被指定的屬性里
@unionOfArrays該操作會返回一個數組,這個數組包含的對象是在從關鍵路徑到操作器右邊的被指定的屬性里和@distinctUnionOfArrays不一樣,重復的對象不會被移除
@distinctUnionOfSets和@distinctUnionOfArrays類似。因為Set本身就不支持重復。
KVO
你沒看錯,KVO是基于KVC實現的。那么是怎么用KVC實現KVO的呢,請期待下章。
總結
本文全方位介紹了KVC的原理和各種用法。相信讀者看完后對會KVC會有更完全的理解,也會在項目里更好的運用KVC。其實這里面所有的東西在官方文檔里都有詳細的講解說明。只不過全是英文的,我也看過幾遍,但是英語不好會看得很吃力,比如官方在介紹@distinctUnionOfArrays時的那句話我想了很好久也不是很明白,而且官方的示例代碼也做得不夠好,所以很難找出某些功能的適用場景。但我還是推薦各位開發者能夠學好英語去看官方文檔。再結合StackOverFlow和Google。真的可以解決絕大多數開發中碰到的難題了。這篇文章就到這里,下篇我向大家介紹KVO。 現在網上KVO的文章非常多,質量也不錯,讀者可以搜索閱讀。
作者:黑暗中的孤影
鏈接:http://www.lxweimin.com/p/45cbd324ea65
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。