簡(jiǎn)介
KVC(Key-value coding)鍵值編碼撬碟,翻譯一下就是指iOS的開發(fā)中炊汹,可以允許開發(fā)者通過Key名直接訪問對(duì)象的屬性勘畔,或者給對(duì)象的屬性賦值醋寝。而不需要調(diào)用明確的存取方法。這樣就可以在運(yùn)行時(shí)動(dòng)態(tài)在訪問和修改對(duì)象的屬性缅阳。而不是在編譯時(shí)確定磕蛇,這也是iOS開發(fā)中的黑魔法之一,很多高級(jí)的iOS開發(fā)技巧都是基于KVC實(shí)現(xiàn)的。
在實(shí)現(xiàn)了訪問器(setter/getter)方法的類中十办,使用點(diǎn)語法和KVC訪問對(duì)象其實(shí)差別不大秀撇,二者可以任意混用。但是 沒有訪問器 方法的類中向族,點(diǎn)語法無法使用呵燕,這時(shí)KVC就有優(yōu)勢(shì)了。
KVC在iOS中的定義
無論是Swift還是Objective-C件相,KVC的定義都是對(duì)NSObject的擴(kuò)展來實(shí)現(xiàn)的(Objective-c中有個(gè)顯式的 NSKeyValueCoding 類別名再扭,而Swift沒有,也不需要)所以對(duì)于所有繼承了NSObject在類型夜矗,都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的)泛范,下面是KVC最為重要的四個(gè)方法
- (nullable id)valueForKey:(NSString *)key; //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設(shè)值
當(dāng)然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些
+ (BOOL)accessInstanceVariablesDirectly;
//默認(rèn)返回YES紊撕,表示如果沒有找到Set方法的話罢荡,會(huì)按照_key,_iskey对扶,key区赵,iskey的順序搜索成員,設(shè)置成NO就不這樣搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值確認(rèn)的API浪南,它可以用來檢查set的值是否正確笼才、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API络凿,里面還有一系列這樣的API患整,如果屬性是一個(gè)NSMutableArray,那么可以用這個(gè)方法來返回
- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在喷众,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性各谚,則會(huì)調(diào)用這個(gè)方法,默認(rèn)是拋出異常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個(gè)方法一樣到千,只不過是設(shè)值昌渤。
- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時(shí)面給Value傳nil,則會(huì)調(diào)用這個(gè)方法
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
//輸入一組key,返回該組key對(duì)應(yīng)的Value憔四,再轉(zhuǎn)成字典返回膀息,用于將Model轉(zhuǎn)到字典般眉。
上面的這些方法在碰到特殊情況或者有特殊需求還是會(huì)用到的,所以也是可以了解一下潜支。后面的代碼示例會(huì)有講到其中的一些方法甸赃。
同時(shí)蘋果對(duì)一些容器類比如NSArray或者NSSet等,KVC有著特殊的實(shí)現(xiàn)冗酿。建議有基礎(chǔ)的或者英文好的開發(fā)者直接去看蘋果的官方文檔埠对,相信你會(huì)對(duì)KVC的理解更上一個(gè)臺(tái)階。
KVC是怎么尋找Key的
KVC是怎么使用的裁替,我相信絕大多數(shù)的開發(fā)者都很清楚项玛,我在這里就不再寫簡(jiǎn)單的使用KVC來設(shè)值和取值的代碼了,首頁我們來探討KVC在內(nèi)部是按什么樣的順序來尋找key的弱判。
當(dāng)調(diào)用setValue:xxx forKey:@”name“
的代碼時(shí)襟沮,底層的執(zhí)行機(jī)制如下:
- 程序優(yōu)先調(diào)用set:屬性值方法,代碼通過setter方法完成設(shè)置昌腰。注意开伏,這里的是指成員變量名,首字母大清寫要符合KVC的全名規(guī)則遭商,下同
- 如果沒有找到setName:方法固灵,KVC機(jī)制會(huì)檢查
+ (BOOL)accessInstanceVariablesDirectly
方法有沒有返回YES,默認(rèn)該方法會(huì)返回YES株婴,如果你重寫了該方法讓其返回NO的話怎虫,那么在這一步KVC會(huì)執(zhí)行setValue:forUNdefinedKey:
方法暑认,不過一般開發(fā)者不會(huì)這么做困介。所以KVC機(jī)制會(huì)搜索該類里面有沒有名為的成員變量,無論該變量是在類接口部分定義蘸际,還是在類實(shí)現(xiàn)部分定義座哩,也無論用了什么樣的訪問修飾符,只在存在以命名的變量粮彤,KVC都可以對(duì)該成員變量賦值根穷。 - 如果該類即沒有
set:
方法,也沒有_
成員變量导坟,KVC機(jī)制會(huì)搜索_is
的成員變量屿良, - 和上面一樣,如果該類即沒有set:方法惫周,也沒有_和_is成員變量尘惧,KVC機(jī)制再會(huì)繼續(xù)搜索和is的成員變量。再給它們賦值递递。
- 如果上面列出的方法或者成員變量都不存在喷橙,系統(tǒng)將會(huì)執(zhí)行該對(duì)象的
setValue:forUNdefinedKey:
方法啥么,默認(rèn)是拋出異常。
如果開發(fā)者想讓這個(gè)類禁用KVC里贰逾,那么重寫+ (BOOL)accessInstanceVariablesDirectly
方法讓其返回NO即可悬荣,這樣的話如果KVC沒有找到set:屬性名時(shí),會(huì)直接用setValue:forUNdefinedKey:方法疙剑。
下面我們來讓代碼來測(cè)試一下上面的KVC機(jī)制
@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(@"出現(xiàn)異常氯迂,該key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出現(xiàn)異常,該key不存在%@",key);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// insert code here...
Dog* dog = [Dog new];
[dog setValue:@"newName" forKey:@"name"];
NSString* name = [dog valueForKey:@"toSetName"];
NSLog(@"%@",name);
}
控制臺(tái)打印結(jié)果:
這說明了重寫+(BOOL)accessInstanceVariablesDirectly方法讓其返回NO后,KVC找不到SetName:方法后核芽,不再去找name系列成員變量囚戚,而是直接調(diào)用forUndefinedKey方法。所以開發(fā)者如果不想讓自己的類實(shí)現(xiàn)KVC轧简,就可以這么做驰坊。
下面那兩個(gè)setter和gettr的注釋取消掉,再把
NSString* name = [dog valueForKey:@"toSetName"]; 換成 NSString* name = [dog valueForKey:@"name"];
Xcode就可以正確地打印出正確的值了
2017-02-14 13:51:19.986 KVO底層實(shí)現(xiàn)原理[2008:125405] newName
下面再注釋到accessInstanceVariablesDirectly方法哮独,就能測(cè)試其他的key查找順序了拳芙,為了節(jié)省篇幅,剩下的的KVC對(duì)于key尋找機(jī)制就不在這里展示了皮璧,有興趣的讀者可以寫代碼去驗(yàn)證舟扎。
KVC取值
當(dāng)調(diào)用ValueforKey:@”name“的代碼時(shí),KVC對(duì)key的搜索方式不同于setValue:屬性值 forKey:@”name“悴务,其搜索方式如下
- 首先按 get+key名, key名,is+key名 的順序方法查找getter方法睹限,找到的話會(huì)直接調(diào)用。如果是BOOL或者int等值類型讯檐, 會(huì)將其包裝成一個(gè)NSNumber對(duì)象羡疗。
- 如果上面的getter沒有找到,KVC則會(huì)查找countOf,objectInAtIndex,AtIndex格式的方法别洪。如果countOf和另外兩個(gè)方法中的一個(gè)被找到叨恨,那么就會(huì)返回一個(gè)可以響應(yīng)NSArray所的方法的代理集合(它是NSKeyValueArray,是NSArray的子類)挖垛,調(diào)用這個(gè)代理集合的方法痒钝,或者說給這個(gè)代理集合發(fā)送NSArray的方法,就會(huì)以countOf,objectInAtIndex,AtIndex這幾個(gè)方法組合的形式調(diào)用痢毒。還有一個(gè)可選的get:range:方法送矩。所以你想重新定義KVC的一些功能,你可以添加這些方法哪替,需要注意的是你的方法名要符合KVC的標(biāo)準(zhǔn)命名方法栋荸,包括方法簽名。
- 如果上面的方法沒有找到,那么會(huì)查找countOf蒸其,enumeratorOf,memberOf格式的方法敏释。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所的方法的代理集合摸袁,以送給這個(gè)代理集合消息方法钥顽,就會(huì)以countOf,enumeratorOf,memberOf組合的形式調(diào)用靠汁。
- 如果還沒有找到蜂大,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為),那么和先前的設(shè)值一樣蝶怔,會(huì)按 _,_is,,is 的順序搜索成員變量名奶浦,這里不推薦這么做,因?yàn)檫@樣直接訪問實(shí)例變量破壞了封裝性踢星,使代碼更脆弱澳叉。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會(huì)直接調(diào)用valueForUndefinedKey:
- 還沒有找到的話沐悦,調(diào)用valueForUndefinedKey:
下面再上代碼測(cè)試:
@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{
//當(dāng)key使用numbers時(shí)成洗,KVC會(huì)找到這兩個(gè)方法。
return @(index * 2);
}
-(NSInteger)getNum{ //第一個(gè),自己一個(gè)一個(gè)注釋試
return 10;
}
-(NSInteger)num{ //第二個(gè)
return 11;
}
-(NSInteger)isNum{ //第三個(gè)
return 12;
}
-(id)valueForUndefinedKey:(NSString *)key
{
return @(13);
}
@end
在控制器里面代碼:
- (void)viewDidLoad {
[super viewDidLoad];
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);
}
控制臺(tái)打印結(jié)果:
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] 10
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] NSKeyValueArray
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] 0:0 1:2 2:4 3:6
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] 1
2017-02-14 14:17:45.840 KVO底層實(shí)現(xiàn)[2117:136702] 2
2017-02-14 14:17:45.840 KVO底層實(shí)現(xiàn)[2117:136702] newName
很明顯藏否,上面的代碼充分說明了說明了KVC在調(diào)用ValueforKey:@”name“時(shí)搜索key的機(jī)制瓶殃。不過還有些功能沒有全部列出,有興趣的讀者可以寫代碼去驗(yàn)證副签。
在KVC中使用KeyPath
然而在開發(fā)過程中遥椿,一個(gè)類的成員變量有可能是其他的自定義類,你可以先用KVC獲取出來再該屬性淆储,然后再次用KVC來獲取這個(gè)自定義類的屬性冠场,但這樣是比較繁瑣的,對(duì)此遏考,KVC提供了一個(gè)解決方案慈鸠,那就是鍵路徑KeyPath蓝谨。
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通過KeyPath來設(shè)值
@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
測(cè)試代碼:
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);
//打印結(jié)果
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] country1:China country2:China
2017-02-14 14:17:45.839 KVO底層實(shí)現(xiàn)[2117:136702] country1:USA country2:USA
上面的代碼簡(jiǎn)單在展示了KeyPath是怎么用的。如果你不小心錯(cuò)誤的使用了key而非KeyPath的話譬巫,KVC會(huì)直接查找address.country這個(gè)屬性咖楣,很明顯,這個(gè)屬性并不存在芦昔,所以會(huì)再調(diào)用UndefinedKey相關(guān)方法诱贿。而KVC對(duì)于KeyPath是搜索機(jī)制第一步就是分離key,用小數(shù)點(diǎn).來分割key,然后再像普通key一樣按照先前介紹的順序搜索下去珠十。
KVC如何處理異常
KVC中最常見的異常就是不小心使用了錯(cuò)誤的Key料扰,或者在設(shè)值中不小心傳遞了nil的值,KVC中有專門的方法來處理這些異常焙蹭。
KVC處理nil異常
通常在用KVC操作Model時(shí)晒杈,拋出異常的那兩個(gè)方法是需要重寫的。雖然一般很小出現(xiàn)傳遞了錯(cuò)誤的Key值這種情況孔厉,但是如果不小心出現(xiàn)了拯钻,直接拋出異常讓APP崩潰顯然是不合理的。
一般在這里直接讓這個(gè)Key打印出來即可撰豺,或者有些特殊情況需要特殊處理粪般。
通常情況下,KVC不允許你要在調(diào)用setValue:屬性值 forKey:@”name“(或者keyPath)時(shí)對(duì)非對(duì)象傳遞一個(gè)nil的值污桦。很簡(jiǎn)單亩歹,因?yàn)橹殿愋褪遣荒転?nil 的。如果你不小心傳了凡橱,KVC會(huì)調(diào)用 setNilValueForKey: 方法捆憎。這個(gè)方法默認(rèn)是拋出異常,所以一般而言最好還是重寫這個(gè)方法梭纹。
[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.' // 調(diào)用setNilValueForKey拋出異常
如果重寫setNilValueForKey:就沒問題了
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能將%@設(shè)成nil",key);
}
KVC處理UndefinedKey異常
通常情況下躲惰,KVC不允許你要在調(diào)用setValue:屬性值 forKey:(或者keyPath)時(shí)對(duì)不存在的key進(jìn)行操作。
不然变抽,會(huì)報(bào)錯(cuò)forUndefinedKey發(fā)生崩潰础拨,重寫 forUndefinedKey 方法避免崩潰。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"出現(xiàn)異常绍载,該key不存在%@", key);
}
KVC處理非對(duì)象和自定義對(duì)象
不是每一個(gè)方法都返回對(duì)象诡宗,但是valueForKey:總是返回一個(gè)id對(duì)象,如果原本的變量類型是值類型或者結(jié)構(gòu)體击儡,返回值會(huì)封裝成NSNumber或者NSValue對(duì)象塔沃。這兩個(gè)類會(huì)處理從數(shù)字,布爾值到指針和結(jié)構(gòu)體任何類型阳谍。然后開以者需要手動(dòng)轉(zhuǎn)換成原來的類型蛀柴。盡管valueForKey:會(huì)自動(dòng)將值類型封裝成對(duì)象,但是setValue:forKey:卻不行矫夯。你必須手動(dòng)將值類型轉(zhuǎn)換成NSNumber或者NSValue類型鸽疾,才能傳遞過去。
對(duì)于自定義對(duì)象训貌,KVC也會(huì)正確以設(shè)值和取值制肮。因?yàn)閭鬟f進(jìn)去和取出來的都是id類型冒窍,所以需要開發(fā)者自己擔(dān)保類型的正確性,運(yùn)行時(shí)Objective-C在發(fā)送消息的會(huì)檢查類型豺鼻,如果錯(cuò)誤會(huì)直接拋出異常综液。
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);
KVC與容器類
對(duì)象的屬性可以是一對(duì)一的,也可以是一對(duì)多的儒飒。一對(duì)多的屬性要么是有序的(數(shù)組)意乓,要么是無序的(數(shù)組)
不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)一般可以使用valueForKey:來獲取
- 比如有一個(gè)叫items的NSArray屬性,你可以用valurForKey:@"items"來獲取這個(gè)屬性约素。
- 前面valueForKey:的key搜索模式中届良,我們發(fā)現(xiàn)其實(shí)KVC使用了一種更靈活的方式來管理容器類。蘋果的官方文檔也推薦我們實(shí)現(xiàn)這些這些特殊的訪問器
而當(dāng)對(duì)象的屬性是可變的容器時(shí)圣猎,對(duì)于有序的容器士葫,可以用下面的方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
該方法返回一個(gè)可變有序數(shù)組,如果調(diào)用該方法送悔,KVC的搜索順序如下
- 搜索insertObject:inAtIndex: , removeObjectFromAtIndex: 或者 insertAtIndexes , removeAtIndexes 格式的方法
如果至少找到一個(gè)insert方法和一個(gè)remove方法慢显,那么同樣返回一個(gè)可以響應(yīng)NSMutableArray所有方法代理集合(類名是NSKeyValueFastMutableArray2),那么給這個(gè)代理集合發(fā)送NSMutableArray的方法欠啤,以insertObject:inAtIndex: , removeObjectFromAtIndex: 或者 insertAdIndexes , removeAtIndexes組合的形式調(diào)用荚藻。還有兩個(gè)可選實(shí)現(xiàn)的接口:replaceOnjectAtIndex:withObject: , replaceAtIndexes:with: 。 - 如果上步的方法沒有找到洁段,則搜索set: 格式的方法应狱,如果找到,那么發(fā)送給代理集合的NSMutableArray最終都會(huì)調(diào)用set:方法祠丝。 也就是說疾呻,mutableArrayValueForKey:取出的代理集合修改后,用·set:· 重新賦值回去去写半。這樣做效率會(huì)低很多岸蜗。所以推薦實(shí)現(xiàn)上面的方法。
- 如果上一步的方法還還沒有找到叠蝇,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為)璃岳,會(huì)按_,,的順序搜索成員變量名,如果找到悔捶,那么發(fā)送的NSMutableArray消息方法直接交給這個(gè)成員變量處理铃慷。
- 如果還是找不到,調(diào)用valueForUndefinedKey:
關(guān)于mutableArrayValueForKey:的適用場(chǎng)景炎功,我在網(wǎng)上找了很多枚冗,發(fā)現(xiàn)其一般是用在對(duì)NSMutableArray添加Observer上缓溅。
如果對(duì)象屬性是個(gè)NSMutableAArray蛇损、NSMutableSet、NSMutableDictionary等集合類型時(shí),你給它添加KVO時(shí)淤齐,你會(huì)發(fā)現(xiàn)當(dāng)你添加或者移除元素時(shí)并不能接收到變化股囊。因?yàn)镵VO的本質(zhì)是系統(tǒng)監(jiān)測(cè)到某個(gè)屬性的內(nèi)存地址或常量改變時(shí),會(huì)添加上- (void)willChangeValueForKey:(NSString *)key
和- (void)didChangeValueForKey:(NSString *)key方法來發(fā)送通知更啄,所以一種解決方法是手動(dòng)調(diào)用者兩個(gè)方法稚疹,但是并不推薦,你永遠(yuǎn)無法像系統(tǒng)一樣真正知道這個(gè)元素什么時(shí)候被改變祭务。另一種便是利用使用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 *)change context:(void *)context{
NSLog(@"%@",change);
}
-(void)dealloc{
[self removeObserver:self forKeyPath:@"arr"]; //一定要在dealloc里面移除觀察
}
-(void)addItem{
NSLog(@"%s",__func__);
[_arr addObject:@"1"];
}
-(void)addItemObserver{
NSLog(@"%s",__func__);
[[self mutableArrayValueForKey:@"arr"] addObject:@"1"];
}
-(void)removeItemObserver{
NSLog(@"%s",__func__);
[[self mutableArrayValueForKey:@"arr"] removeLastObject];
}
@end
然后再:
demo* d = [demo new];
[d addItem];
[d addItemObserver];
[d removeItemObserver];
打印結(jié)果:
2017-02-14 15:54:52.642 KVO底層實(shí)現(xiàn)[2402:164206] -[demo addItem]
2017-02-14 15:54:52.642 KVO底層實(shí)現(xiàn)[2402:164206] -[demo addItemObserver]
2017-02-14 15:54:52.643 KVO底層實(shí)現(xiàn)[2402:164206] {
indexes = "<_NSCachedIndexSet: 0x618000034c20>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
1
);
}
2017-02-14 15:54:52.643 KVO底層實(shí)現(xiàn)[2402:164206] -[demo removeItemObserver]
2017-02-14 15:54:52.643 KVO底層實(shí)現(xiàn)[2402:164206] {
indexes = "<_NSCachedIndexSet: 0x618000034c20>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 3;
old = (
1
);
}
從上面的代碼可以看出,當(dāng)只是普通地調(diào)用[_arr addObject:@"1"]時(shí)义锥,Observer并不會(huì)回調(diào)柳沙,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"1"];這樣寫時(shí)才能正確地觸發(fā)KVO。打印出來的數(shù)據(jù)中拌倍,可以看出這次操作的詳情赂鲤,kind可能是指操作方法(我還不是很確認(rèn)),old和new并不是成對(duì)出現(xiàn)的柱恤,當(dāng)加添新數(shù)據(jù)時(shí)是new数初,刪除數(shù)據(jù)時(shí)是old。
而對(duì)于無序的容器梗顺,可以用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
該方法返回一個(gè)可變的無序數(shù)組如果調(diào)用該方法泡孩,KVC的搜索順序如下:
- 搜索addObjectObject: , removeObject: 或者 add , remove 格式的方法
如果至少找到一個(gè)insert方法和一個(gè)remove方法,那么同樣返回一個(gè)可以響應(yīng)NSMutableSet所有方法代理集合(類名是NSKeyValueFastMutableSet2)寺谤,那么給這個(gè)代理集合發(fā)送NSMutableSet的方法珍德,以addObjectObject: , removeObject: 或者 add , remove組合的形式調(diào)用。還有兩個(gè)可選實(shí)現(xiàn)的接口:intersect , set: 矗漾。 - 如果reciever是ManagedObject锈候,那么就不會(huì)繼續(xù)搜索。
- 如果上步的方法沒有找到敞贡,則搜索set: 格式的方法泵琳,如果找到,那么發(fā)送給代理集合的NSMutableSet最終都會(huì)調(diào)用set:方法誊役。 也就是說获列,mutableSetValueForKey取出的代理集合修改后,用set: 重新賦值回去去蛔垢。這樣做效率會(huì)低很多击孩。所以推薦實(shí)現(xiàn)上面的方法。
- 如果上一步的方法還還沒有找到鹏漆,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為)巩梢,會(huì)按_,,的順序搜索成員變量名创泄,如果找到,那么發(fā)送的NSMutableSet消息方法直接交給這個(gè)成員變量處理括蝠。
- 如果還是找不到鞠抑,調(diào)用valueForUndefinedKey:
可見,除了檢查reciever是ManagedObject以外忌警,其搜索順序和mutableArrayValueForKey基本一至搁拙,
同樣,它們也有對(duì)應(yīng)的keyPath版本
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
OS5和OSX10.7以后還有個(gè)mutableOrdered版本
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key
KVC和字典
當(dāng)對(duì)NSDictionary對(duì)象使用KVC時(shí)法绵,valueForKey:的表現(xiàn)行為和objectForKey:一樣箕速。所以使用valueForKeyPath:用來訪問多層嵌套的字典是比較方便的。
KVC里面還有兩個(gè)關(guān)于NSDictionary的方法
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
dictionaryWithValuesForKeys:是指輸入一組key朋譬,返回這組key對(duì)應(yīng)的屬性弧满,再組成一個(gè)字典。
setValuesForKeysWithDictionary是用來修改Model中對(duì)應(yīng)key的屬性此熬。下面直接用代碼會(huì)更直觀一點(diǎn)
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]; //把對(duì)應(yīng)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);
//打印結(jié)果
{
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
country:USA province:california city:Los angle
KVC的正確性驗(yàn)證
KVC提供了屬性值,用來驗(yàn)證key對(duì)應(yīng)的Value是否可用的方法
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
這個(gè)方法的默認(rèn)實(shí)現(xiàn)是去探索類里面是否有一個(gè)這樣的方法:-(BOOL)validate:error:如果有這個(gè)方法庭呜,就調(diào)用這個(gè)方法來返回,沒有的話就直接返回YES
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{
//在implementation里面加這個(gè)方法犀忱,它會(huì)驗(yàn)證是否設(shè)了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO;
//如果國(guó)家是日本募谎,就返回NO,這里省略了錯(cuò)誤提示阴汇,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果沒有重寫-(BOOL)-validate:error:数冬,默認(rèn)返回Yes
if (result) {
NSLog(@"鍵值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"鍵值不匹配"); //不能設(shè)為日本,基他國(guó)家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
如上面的代碼搀庶,當(dāng)開發(fā)者需要驗(yàn)證能不能用KVC設(shè)定某個(gè)值時(shí)拐纱,可以調(diào)用validateValue: forKey:這個(gè)方法來驗(yàn)證,如果這個(gè)類的開發(fā)者實(shí)現(xiàn)了-(BOOL)validate:error:這個(gè)方法哥倔,那么KVC就會(huì)直接調(diào)用這個(gè)方法來返回秸架,如果沒有,就直接返回YES咆蒿,注意东抹,KVC在設(shè)值時(shí)不會(huì)主動(dòng)去做驗(yàn)證,需要開發(fā)者手動(dòng)去驗(yàn)證沃测。所以即使你在類里面寫了驗(yàn)證方法缭黔,但是KVC因?yàn)椴粫?huì)去主動(dòng)驗(yàn)證,所以還是能夠設(shè)值成功蒂破。
KVC的使用
KVC在iOS開發(fā)中是絕不可少的利器馏谨,這種基于運(yùn)行時(shí)的編程方式極大地提高了靈活性,簡(jiǎn)化了代碼附迷,甚至實(shí)現(xiàn)很多難以想像的功能惧互,KVC也是許多iOS開發(fā)黑魔法的基礎(chǔ)哎媚。下面我來列舉iOS開發(fā)中KVC的使用場(chǎng)景.
動(dòng)態(tài)地取值和設(shè)值
用KVC動(dòng)態(tài)的取值和設(shè)值是最基本的用途了。相信每一個(gè)iOS開發(fā)者都能熟練掌握.
用KVC來訪問和修改私有變量
對(duì)于類里的私有屬性壹哺,Objective-C是無法直接訪問的抄伍,但是KVC是可以的艘刚,請(qǐng)參考本文前面的Dog類的例子管宵。
Model和字典轉(zhuǎn)換
這是KVC強(qiáng)大作用的又一次體現(xiàn),請(qǐng)參考iOS開發(fā)技巧系列—打造強(qiáng)大的BaseMod系列文章攀甚,里面
充分地運(yùn)用了KVC和Objc的runtime組合的技巧箩朴,只用了短短數(shù)行代碼就是完成了很多功能
修改一些控件的內(nèi)部屬性
這也是iOS開發(fā)中必不可少的小技巧。眾所周知很多UI控件都由很多內(nèi)部UI控件組合而成的秋度,但是Apple度沒有提供這訪問這些空間的API炸庞,這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數(shù)情況可下可以解決這個(gè)問題荚斯。最常用的就是個(gè)性化UITextField中的placeHolderText了埠居。
下面演示如果修改placeHolder的文字樣式。這里的關(guān)鍵點(diǎn)是如果獲取你要修改的樣式的屬性名事期,也就是key或者keyPath名,一般情況下可以運(yùn)用runtime來獲取Apple不想開放的屬性名:
- (NSArray *)getAllProperties
{
u_int count;
Ivar *arr = class_copyIvarList([UITextField class], &count);
NSMutableArray *propertiesArray = [NSMutableArray arrayWithCapacity:count];
for (int i = 0; i < count ; i++)
{
const char* propertyName =ivar_getName(arr[i]);
[propertiesArray addObject: [NSString stringWithUTF8String: propertyName]];
}
free(arr);
return propertiesArray;
}
打印結(jié)果:
UITextField *textField = [UITextField new];
textField.frame = CGRectMake(100, 100, 300, 30);
[self.view addSubview:textField];
textField.placeholder = @"這是一個(gè)測(cè)試";
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
這是運(yùn)行就會(huì)發(fā)現(xiàn)UITextField的placeholder的字體顏色變成紅色了滥壕,可以從里面看到其他還有很多東西可以修改,運(yùn)用KVC設(shè)值可以獲得自己想要的效果兽泣。
操作集合
Apple對(duì)KVC的valueForKey:方法作了一些特殊的實(shí)現(xiàn)绎橘,比如說NSArray和NSSet這樣的容器類就實(shí)現(xiàn)了這些方法。所以可以用KVC很方便地操作集合
用KVC實(shí)現(xiàn)高階消息傳遞
當(dāng)對(duì)容器類使用KVC時(shí)唠倦,valueForKey:將會(huì)被傳遞給容器中的每一個(gè)對(duì)象称鳞,而不是容器本身進(jìn)行操作。結(jié)果會(huì)被添加進(jìn)返回的容器中稠鼻,這樣冈止,開發(fā)者可以很方便的操作集合來返回另一個(gè)集合。
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);
}
打印結(jié)果:
Franch
Chinese
7
6
7
方法capitalizedString被傳遞到NSArray中的每一項(xiàng)候齿,這樣靶瘸,NSArray的每一員都會(huì)執(zhí)行capitalizedString并返回一個(gè)包含結(jié)果的新的NSArray。從打印結(jié)果可以看出毛肋,所有String都成功以轉(zhuǎn)成了大寫怨咪。
同樣如果要執(zhí)行多個(gè)方法也可以用valueForKeyPath:方法。它先會(huì)對(duì)每一個(gè)成員調(diào)用 capitalizedString方法润匙,然后再調(diào)用length诗眨,因?yàn)閘enth方法返回是一個(gè)數(shù)字,所以返回結(jié)果以NSNumber的形式保存在新數(shù)組里孕讳。
用KVC中的函數(shù)操作集合
KVC同時(shí)還提供了很復(fù)雜的函數(shù)匠楚,主要有下面這些
①簡(jiǎn)單集合運(yùn)算符
簡(jiǎn)單集合運(yùn)算符共有@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);
打印結(jié)果
sum:256.000000
avg:64.000000
count:4.000000
min:12.000000
max:111.000000
②對(duì)象運(yùn)算符
比集合運(yùn)算符稍微復(fù)雜肪康,能以數(shù)組的方式返回指定的內(nèi)容,一共有兩種:
@distinctUnionOfObjects
@unionOfObjects
它們的返回值都是NSArray撩穿,區(qū)別是前者返回的元素都是唯一的磷支,是去重以后的結(jié)果;后者返回的元素是全集食寡。
用法如下:
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);
}
distinctUnionOfObjects
111.000000
12.000000
22.000000
unionOfObjects
22.000000
12.000000
111.000000
111.000000
前者會(huì)將重復(fù)的價(jià)格去除后返回所有價(jià)格雾狈,后者直接返回所有的圖書價(jià)格。(因?yàn)橹环祷貎r(jià)格抵皱,沒有返回圖書善榛,感覺用處不大。)
③Array和Set操作符
這種情況更復(fù)雜了呻畸,說的是集合中包含集合的情況移盆,我們執(zhí)行了如下的一段代碼:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets
@distinctUnionOfArrays:該操作會(huì)返回一個(gè)數(shù)組碾牌,這個(gè)數(shù)組包含不同的對(duì)象胧辽,不同的對(duì)象是在從關(guān)鍵路徑到操作器右邊的被指定的屬性里
@unionOfArrays 該操作會(huì)返回一個(gè)數(shù)組,這個(gè)數(shù)組包含的對(duì)象是在從關(guān)鍵路徑到操作器右邊的被指定的屬性里和@distinctUnionOfArrays不一樣疙挺,重復(fù)的對(duì)象不會(huì)被移除
@distinctUnionOfSets 和@distinctUnionOfArrays類似钮呀。因?yàn)镾et本身就不支持重復(fù)剑鞍。
KVO
你沒看錯(cuò),KVO是基于KVC實(shí)現(xiàn)的爽醋。