iOS KVC和KVO詳解

KVC定義

KVC(Key-value coding)鍵值編碼究抓,就是指iOS的開發(fā)中亦镶,可以允許開發(fā)者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值廓脆。而不需要調(diào)用明確的存取方法进萄。這樣就可以在運(yùn)行時動態(tài)地訪問和修改對象的屬性捻脖。而不是在編譯時確定烦秩,這也是iOS開發(fā)中的黑魔法之一。很多高級的iOS開發(fā)技巧都是基于KVC實(shí)現(xiàn)的郎仆。

在實(shí)現(xiàn)了訪問器方法的類中,使用點(diǎn)語法和KVC訪問對象其實(shí)差別不大兜蠕,二者可以任意混用扰肌。但是沒有訪問起方法的類中,點(diǎn)語法無法使用熊杨,這時KVC就有優(yōu)勢了曙旭。

KVC的定義都是對NSObject的擴(kuò)展來實(shí)現(xiàn)的,Objective-C中有個顯式的NSKeyValueCoding類別名晶府,所以對于所有繼承了NSObject的類型桂躏,都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的,因?yàn)闆]有繼承NSObject)川陆,下面是KVC最為重要的四個方法:

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

NSKeyValueCoding類別中其他的一些方法:

+ (BOOL)accessInstanceVariablesDirectly;//默認(rèn)返回YES剂习,表示如果沒有找到Set<Key>方法的話,會按照_key较沪,_iskey鳞绕,key,iskey的順序搜索成員尸曼,設(shè)置成NO就不這樣搜索- (BOOL)validateValue:(inoutid__nullable* __nonnull)ioValue forKey:(NSString*)inKey error:(outNSError**)outError;//KVC提供屬性值正確性?驗(yàn)證的API们何,它可以用來檢查set的值是否正確、為不正確的值做一個替換值或者拒絕設(shè)置新值并返回錯誤原因控轿。- (NSMutableArray*)mutableArrayValueForKey:(NSString*)key;//這是集合操作的API冤竹,里面還有一系列這樣的API,如果屬性是一個NSMutableArray茬射,那么可以用這個方法來返回鹦蠕。- (nullableid)valueForUndefinedKey:(NSString*)key;//如果Key不存在,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性躲株,則會調(diào)用這個方法片部,默認(rèn)是拋出異常。- (void)setValue:(nullableid)value forUndefinedKey:(NSString*)key;//和上一個方法一樣霜定,但這個方法是設(shè)值档悠。- (void)setNilValueForKey:(NSString*)key;//如果你在SetValue方法時面給Value傳nil,則會調(diào)用這個方法- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;//輸入一組key,返回該組key對應(yīng)的Value望浩,再轉(zhuǎn)成字典返回辖所,用于將Model轉(zhuǎn)到字典。

同時蘋果對一些容器類比如NSArray或者NSSet等磨德,KVC有著特殊的實(shí)現(xiàn)缘回。

有序集合對應(yīng)方法如下:

-countOf//必須實(shí)現(xiàn)吆视,對應(yīng)于NSArray的基本方法count:2? -objectInAtIndex:-AtIndexes://這兩個必須實(shí)現(xiàn)一個,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:-get:range://不是必須實(shí)現(xiàn)的酥宴,但實(shí)現(xiàn)后可以提高性能啦吧,其對應(yīng)于 NSArray 方法 getObjects:range:-insertObject:inAtIndex:-insert:atIndexes://兩個必須實(shí)現(xiàn)一個,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:-removeObjectFromAtIndex:-removeAtIndexes://兩個必須實(shí)現(xiàn)一個拙寡,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:-replaceObjectInAtIndex:withObject:-replaceAtIndexes:with://可選的授滓,如果在此類操作上有性能問題,就需要考慮實(shí)現(xiàn)之

無序集合對應(yīng)方法如下:

-countOf//必須實(shí)現(xiàn)肆糕,對應(yīng)于NSArray的基本方法count:-objectInAtIndex:-AtIndexes://這兩個必須實(shí)現(xiàn)一個般堆,對應(yīng)于 NSArray 的方法 objectAtIndex: 和 objectsAtIndexes:-get:range://不是必須實(shí)現(xiàn)的,但實(shí)現(xiàn)后可以提高性能诚啃,其對應(yīng)于 NSArray 方法 getObjects:range:-insertObject:inAtIndex:-insert:atIndexes://兩個必須實(shí)現(xiàn)一個淮摔,類似于 NSMutableArray 的方法 insertObject:atIndex: 和 insertObjects:atIndexes:-removeObjectFromAtIndex:-removeAtIndexes://兩個必須實(shí)現(xiàn)一個,類似于 NSMutableArray 的方法 removeObjectAtIndex: 和 removeObjectsAtIndexes:-replaceObjectInAtIndex:withObject:-replaceAtIndexes:with://這兩個都是可選的始赎,如果在此類操作上有性能問題和橙,就需要考慮實(shí)現(xiàn)之

通過以下幾個方面講解KVC相關(guān)的技術(shù)概念以及使用:

KVC設(shè)值

KVC取值

KVC使用keyPath

KVC處理異常

KVC處理數(shù)值和結(jié)構(gòu)體類型屬性

KVC鍵值驗(yàn)證(Key-Value Validation)

KVC處理集合

KVC處理字典

KVC相關(guān)技術(shù)概念

KVC設(shè)值

KVC要設(shè)值,那么就要對象中對應(yīng)的key造垛,KVC在內(nèi)部是按什么樣的順序來尋找key的胃碾。當(dāng)調(diào)用setValue:屬性值 forKey:@”name“的代碼時,底層的執(zhí)行機(jī)制如下:

程序優(yōu)先調(diào)用set<Key>:屬性值方法筋搏,代碼通過setter方法完成設(shè)置仆百。注意,這里的<key>是指成員變量名奔脐,首字母大小寫要符合KVC的命名規(guī)則俄周,下同

如果沒有找到setName:方法,KVC機(jī)制會檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES髓迎,默認(rèn)該方法會返回YES峦朗,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會執(zhí)行setValue:forUndefinedKey:方法排龄,不過一般開發(fā)者不會這么做波势。所以KVC機(jī)制會搜索該類里面有沒有名為<key>的成員變量,無論該變量是在類接口處定義橄维,還是在類實(shí)現(xiàn)處定義尺铣,也無論用了什么樣的訪問修飾符,只在存在以命名的變量争舞,KVC都可以對該成員變量賦值凛忿。

如果該類即沒有set<key>:方法,也沒有_<key>成員變量竞川,KVC機(jī)制會搜索_is<Key>的成員變量店溢。

和上面一樣叁熔,如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量床牧,KVC機(jī)制再會繼續(xù)搜索<key>和is<Key>的成員變量荣回。再給它們賦值。

如果上面列出的方法或者成員變量都不存在戈咳,系統(tǒng)將會執(zhí)行該對象的setValue:forUndefinedKey:方法驹马,默認(rèn)是拋出異常。

簡單來說就是如果沒有找到Set<Key>方法的話除秀,會按照_key,_iskey算利,key册踩,iskey的順序搜索成員并進(jìn)行賦值操作。

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

下面看例子:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{NSString*_name;}@end@implementationTest@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC賦值name[obj setValue:@"xiaoming"forKey:@"name"];//通過KVC取值name打印NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 15:36:52.354405+0800 KVCKVO[35231:6116188] obj的名字是xiaoming

可以看到通過- (void)setValue:(nullable id)value forKey:(NSString *)key;和- (nullable id)valueForKey:(NSString *)key;成功設(shè)置和取出obj對象的name值慕的。

再看一下設(shè)置accessInstanceVariablesDirectly為NO的效果:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{NSString*_name;}@end@implementationTest+ (BOOL)accessInstanceVariablesDirectly {returnNO;}- (id)valueForUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常,該key不存在%@",key);returnnil;}- (void)setValue:(id)value forUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常挤渔,該key不存在%@", key);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC賦值name[obj setValue:@"xiaoming"forKey:@"name"];//通過KVC取值name打印NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 15:45:22.399021+0800 KVCKVO[35290:6145826] 出現(xiàn)異常肮街,該key不存在name

2018-05-05 15:45:22.399546+0800 KVCKVO[35290:6145826] 出現(xiàn)異常,該key不存在name

2018-05-05 15:45:22.399577+0800 KVCKVO[35290:6145826] obj的名字是(null)

可以看到accessInstanceVariablesDirectly為NO的時候KVC只會查詢setter和getter這一層判导,下面尋找key的相關(guān)變量執(zhí)行就會停止嫉父,直接報錯。

設(shè)置accessInstanceVariablesDirectly為YES眼刃,再修改_name為_isName绕辖,看看執(zhí)行是否成功。

#import<Foundation/Foundation.h>@interfaceTest:NSObject{NSString*_isName;}@end@implementationTest+ (BOOL)accessInstanceVariablesDirectly {returnYES;}- (id)valueForUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常擂红,該key不存在%@",key);returnnil;}- (void)setValue:(id)value forUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常仪际,該key不存在%@", key);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC賦值name[obj setValue:@"xiaoming"forKey:@"name"];//通過KVC取值name打印NSLog(@"obj的名字是%@", [obj valueForKey:@"name"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 15:49:53.444350+0800 KVCKVO[35303:6157671] obj的名字是xiaoming

從打印可以看到設(shè)置accessInstanceVariablesDirectly為YES,KVC會繼續(xù)按照順序查找昵骤,并成功設(shè)值和取值了树碱。

KVC取值

當(dāng)調(diào)用valueForKey:@”name“的代碼時,KVC對key的搜索方式不同于setValue:屬性值 forKey:@”name“变秦,其搜索方式如下:

首先按get<Key>,<key>,is<Key>的順序方法查找getter方法赴恨,找到的話會直接調(diào)用。如果是BOOL或者Int等值類型伴栓, 會將其包裝成一個NSNumber對象伦连。

如果上面的getter沒有找到雨饺,KVC則會查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個方法中的一個被找到惑淳,那么就會返回一個可以響應(yīng)NSArray所有方法的代理集合(它是NSKeyValueArray额港,是NSArray的子類),調(diào)用這個代理集合的方法歧焦,或者說給這個代理集合發(fā)送屬于NSArray的方法移斩,就會以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes這幾個方法組合的形式調(diào)用。還有一個可選的get<Key>:range:方法绢馍。所以你想重新定義KVC的一些功能向瓷,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標(biāo)準(zhǔn)命名方法舰涌,包括方法簽名猖任。

如果上面的方法沒有找到,那么會同時查找countOf<Key>瓷耙,enumeratorOf<Key>,memberOf<Key>格式的方法朱躺。如果這三個方法都找到,那么就返回一個可以響應(yīng)NSSet所的方法的代理集合搁痛,和上面一樣长搀,給這個代理集合發(fā)NSSet的消息,就會以countOf<Key>鸡典,enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用源请。

如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為)彻况,那么和先前的設(shè)值一樣巢钓,會按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做疗垛,因?yàn)檫@樣直接訪問實(shí)例變量破壞了封裝性症汹,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話贷腕,那么會直接調(diào)用valueForUndefinedKey:方法背镇,默認(rèn)是拋出異常。

給Test類添加getAge方法泽裳,例如如下:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{}@end@implementationTest- (NSUInteger)getAge {return10;}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC取值age打印NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:00:04.207857+0800 KVCKVO[35324:6188613] obj的年齡是10

可以看到[obj valueForKey:@"age"]瞒斩,找到了getAge方法,并且取到了值涮总。

下面把getAge改成age胸囱,例子如下:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{}@end@implementationTest- (NSUInteger)age {return10;}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC取值age打印NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:02:27.270954+0800 KVCKVO[35337:6195086] obj的年齡是10

可以看到[obj valueForKey:@"age"],找到了age方法瀑梗,并且取到了值烹笔。

下面把getAge改成isAge裳扯,例子如下:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{}@end@implementationTest- (NSUInteger)isAge {return10;}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象Test *obj = [[Test alloc] init];//通過KVC取值age打印NSLog(@"obj的年齡是%@", [obj valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:03:56.234338+0800 KVCKVO[35345:6201242] obj的年齡是10

可以看到[obj valueForKey:@"age"],找到了isAge方法谤职,并且取到了值饰豺。

上面的代碼說明了說明了KVC在調(diào)用valueforKey:@"age"時搜索key的機(jī)制。

KVC使用keyPath

在開發(fā)過程中允蜈,一個類的成員變量有可能是自定義類或其他的復(fù)雜數(shù)據(jù)類型冤吨,你可以先用KVC獲取該屬性,然后再次用KVC來獲取這個自定義類的屬性,

但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑keyPath信卡。顧名思義,就是按照路徑尋找key。

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

用代碼實(shí)現(xiàn)如下:

#import<Foundation/Foundation.h>@interfaceTest1:NSObject{NSString*_name;}@end@implementationTest1@end@interfaceTest:NSObject{? ? Test1 *_test1;}@end@implementationTest@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//Test生成對象Test *test = [[Test alloc] init];//Test1生成對象Test1 *test1 = [[Test1 alloc] init];//通過KVC設(shè)值test的"test1"[test setValue:test1 forKey:@"test1"];//通過KVC設(shè)值test的"test1的name"[test setValue:@"xiaoming"forKeyPath:@"test1.name"];//通過KVC取值age打印NSLog(@"test的\"test1的name\"是%@", [test valueForKeyPath:@"test1.name"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:19:02.613394+0800 KVCKVO[35436:6239788] test的"test1的name"是xiaoming

從打印結(jié)果來看我們成功的通過keyPath設(shè)置了test1的值。

KVC對于keyPath是搜索機(jī)制第一步就是分離key,用小數(shù)點(diǎn).來分割key褐奥,然后再像普通key一樣按照先前介紹的順序搜索下去咖耘。

KVC處理異常

KVC中最常見的異常就是不小心使用了錯誤的key,或者在設(shè)值中不小心傳遞了nil的值撬码,KVC中有專門的方法來處理這些異常儿倒。

KVC處理nil異常

通常情況下,KVC不允許你要在調(diào)用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值呜笑。很簡單夫否,因?yàn)橹殿愋褪遣荒転閚il的。如果你不小心傳了叫胁,KVC會調(diào)用setNilValueForKey:方法凰慈。這個方法默認(rèn)是拋出異常,所以一般而言最好還是重寫這個方法驼鹅。

代碼實(shí)現(xiàn)如下:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{NSUIntegerage;}@end@implementationTest- (void)setNilValueForKey:(NSString*)key {NSLog(@"不能將%@設(shè)成nil", key);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//Test生成對象Test *test = [[Test alloc] init];//通過KVC設(shè)值test的age[test setValue:nilforKey:@"age"];//通過KVC取值age打印NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:24:30.302134+0800 KVCKVO[35470:6258307] 不能將age設(shè)成nil

2018-05-05 16:24:30.302738+0800 KVCKVO[35470:6258307] test的年齡是0

KVC處理UndefinedKey異常

通常情況下微谓,KVC不允許你要在調(diào)用setValue:屬性值 forKey:(或者keyPath)時對不存在的key進(jìn)行操作。

不然输钩,會報錯forUndefinedKey發(fā)生崩潰豺型,重寫forUndefinedKey方法避免崩潰。

#import<Foundation/Foundation.h>@interfaceTest:NSObject{}@end@implementationTest- (id)valueForUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常买乃,該key不存在%@",key);returnnil;}- (void)setValue:(id)value forUndefinedKey:(NSString*)key {NSLog(@"出現(xiàn)異常姻氨,該key不存在%@", key);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//Test生成對象Test *test = [[Test alloc] init];//通過KVC設(shè)值test的age[test setValue:@10forKey:@"age"];//通過KVC取值age打印NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:30:18.564680+0800 KVCKVO[35487:6277523] 出現(xiàn)異常,該key不存在age

2018-05-05 16:30:18.565190+0800 KVCKVO[35487:6277523] 出現(xiàn)異常剪验,該key不存在age

2018-05-05 16:30:18.565216+0800 KVCKVO[35487:6277523] test的年齡是(null)

KVC處理數(shù)值和結(jié)構(gòu)體類型屬性

不是每一個方法都返回對象肴焊,但是valueForKey:總是返回一個id對象前联,如果原本的變量類型是值類型或者結(jié)構(gòu)體,返回值會封裝成NSNumber或者NSValue對象抖韩。

這兩個類會處理從數(shù)字蛀恩,布爾值到指針和結(jié)構(gòu)體任何類型。然后開以者需要手動轉(zhuǎn)換成原來的類型茂浮。

盡管valueForKey:會自動將值類型封裝成對象双谆,但是setValue:forKey:卻不行。你必須手動將值類型轉(zhuǎn)換成NSNumber或者NSValue類型席揽,才能傳遞過去顽馋。

因?yàn)閭鬟f進(jìn)去和取出來的都是id類型,所以需要開發(fā)者自己擔(dān)保類型的正確性幌羞,運(yùn)行時Objective-C在發(fā)送消息的會檢查類型寸谜,如果錯誤會直接拋出異常。

舉個例子属桦,Person類有個NSInteger類型的age屬性熊痴,如下:

//? Person.m#import"Person.h"@interfacePerson()@property(nonatomic,assign)NSIntegerage;@end@implementationPerson@end

修改值

我們通過KVC技術(shù)使用如下方式設(shè)置age屬性的值:

[person setValue:[NSNumber numberWithInteger:5]forKey:@"age"];

我們賦給age的是一個NSNumber對象,KVC會自動的將NSNumber對象轉(zhuǎn)換成NSInteger對象聂宾,然后再調(diào)用相應(yīng)的訪問器方法設(shè)置age的值果善。

獲取值

同樣,以如下方式獲取age屬性值:

[person valueForKey:@"age"];

這時系谐,會以NSNumber的形式返回age的值巾陕。

//? ViewController.m#import"ViewController.h"#import"Person.h"@interfaceViewController()@end@implementationViewController- (void)viewDidLoad {? ? [superviewDidLoad];? ? ? ? Person *person = [[Person alloc]init];? ? [person setValue:[NSNumbernumberWithInteger:5] forKey:@"age"];NSLog(@"age=%@",[person valueForKey:@"age"]);? ? }@end

打印結(jié)果:

2017-01-16 16:31:55.709 Test[28586:2294177] age=5

需要注意的是我們不能直接將一個數(shù)值通過KVC賦值的,我們需要把數(shù)據(jù)轉(zhuǎn)為NSNumber和NSValue類型傳入纪他,那到底哪些類型數(shù)據(jù)要用NSNumber封裝哪些類型數(shù)據(jù)要用NSValue封裝呢鄙煤?看下面這些方法的參數(shù)類型就知道了:

可以使用NSNumber的數(shù)據(jù)類型有:

+ (NSNumber*)numberWithChar:(char)value;+ (NSNumber*)numberWithUnsignedChar:(unsignedchar)value;+ (NSNumber*)numberWithShort:(short)value;+ (NSNumber*)numberWithUnsignedShort:(unsignedshort)value;+ (NSNumber*)numberWithInt:(int)value;+ (NSNumber*)numberWithUnsignedInt:(unsignedint)value;+ (NSNumber*)numberWithLong:(long)value;+ (NSNumber*)numberWithUnsignedLong:(unsignedlong)value;+ (NSNumber*)numberWithLongLong:(longlong)value;+ (NSNumber*)numberWithUnsignedLongLong:(unsignedlonglong)value;+ (NSNumber*)numberWithFloat:(float)value;+ (NSNumber*)numberWithDouble:(double)value;+ (NSNumber*)numberWithBool:(BOOL)value;+ (NSNumber*)numberWithInteger:(NSInteger)valueNS_AVAILABLE(10_5,2_0);+ (NSNumber*)numberWithUnsignedInteger:(NSUInteger)valueNS_AVAILABLE(10_5,2_0);

就是一些常見的數(shù)值型數(shù)據(jù)。

可以使用NSValue的數(shù)據(jù)類型有:

+ (NSValue*)valueWithCGPoint:(CGPoint)point;+ (NSValue*)valueWithCGSize:(CGSize)size;+ (NSValue*)valueWithCGRect:(CGRect)rect;+ (NSValue*)valueWithCGAffineTransform:(CGAffineTransform)transform;+ (NSValue*)valueWithUIEdgeInsets:(UIEdgeInsets)insets;+ (NSValue*)valueWithUIOffset:(UIOffset)insetsNS_AVAILABLE_IOS(5_0);

NSValue主要用于處理結(jié)構(gòu)體型的數(shù)據(jù)茶袒,它本身提供了如上集中結(jié)構(gòu)的支持梯刚。任何結(jié)構(gòu)體都是可以轉(zhuǎn)化成NSValue對象的,包括其它自定義的結(jié)構(gòu)體薪寓。

KVC鍵值驗(yàn)證(Key-Value Validation)

KVC提供了驗(yàn)證Key對應(yīng)的Value是否可用的方法:

-(BOOL)validateValue:(inoutid*)ioValueforKey:(NSString*)inKeyerror:(outNSError**)outError;

該方法默認(rèn)的實(shí)現(xiàn)是調(diào)用一個如下格式的方法:

- (BOOL)validate:error:

例如:

#import<Foundation/Foundation.h>@interfaceTest:NSObject{NSUInteger_age;}@end@implementationTest- (BOOL)validateValue:(inoutid_Nullable __autoreleasing *)ioValue forKey:(NSString*)inKey error:(outNSError* _Nullable __autoreleasing *)outError {NSNumber*age = *ioValue;if(age.integerValue ==10) {returnNO;? ? }returnYES;}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//Test生成對象Test *test = [[Test alloc] init];//通過KVC設(shè)值test的ageNSNumber*age = @10;NSError* error;NSString*key =@"age";BOOLisValid = [test validateValue:&age forKey:key error:&error];if(isValid) {NSLog(@"鍵值匹配");? ? ? ? ? ? [test setValue:age forKey:key];? ? ? ? }else{NSLog(@"鍵值不匹配");? ? ? ? }//通過KVC取值age打印NSLog(@"test的年齡是%@", [test valueForKey:@"age"]);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 16:59:06.111671+0800 KVCKVO[35777:6329982] 鍵值不匹配

2018-05-05 16:59:06.112215+0800 KVCKVO[35777:6329982] test的年齡是0

這樣就給了我們一次糾錯的機(jī)會乾巧。需要指出的是,KVC是不會自動調(diào)用鍵值驗(yàn)證方法的预愤,就是說我們?nèi)绻胍I值驗(yàn)證則需要手動驗(yàn)證沟于。但是有些技術(shù),比如CoreData會自動調(diào)用植康。

KVC處理集合

KVC同時還提供了很復(fù)雜的函數(shù)旷太,主要有下面這些:

簡單集合運(yùn)算符

簡單集合運(yùn)算符共有@avg, @count , @max 供璧, @min 存崖,@sum5種,都表示什么直接看下面例子就明白了睡毒, 目前還不支持自定義来惧。

#import<Foundation/Foundation.h>@interfaceBook:NSObject@property(nonatomic,copy)NSString* name;@property(nonatomic,assign)CGFloatprice;@end@implementationBook@endintmain(intargc,constchar* argv[]) {@autoreleasepool{? ? ? ? ? ? ? ? Book *book1 = [Book new];? ? ? ? book1.name =@"The Great Gastby";? ? ? ? book1.price =10;? ? ? ? Book *book2 = [Book new];? ? ? ? book2.name =@"Time History";? ? ? ? book2.price =20;? ? ? ? Book *book3 = [Book new];? ? ? ? book3.name =@"Wrong Hole";? ? ? ? book3.price =30;? ? ? ? ? ? ? ? Book *book4 = [Book new];? ? ? ? book4.name =@"Wrong Hole";? ? ? ? book4.price =40;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);? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 17:04:50.674243+0800 KVCKVO[35785:6351239] sum:100.000000

2018-05-05 17:04:50.675007+0800 KVCKVO[35785:6351239] avg:25.000000

2018-05-05 17:04:50.675081+0800 KVCKVO[35785:6351239] count:4.000000

2018-05-05 17:04:50.675146+0800 KVCKVO[35785:6351239] min:10.000000

2018-05-05 17:04:50.675204+0800 KVCKVO[35785:6351239] max:40.000000

對象運(yùn)算符

比集合運(yùn)算符稍微復(fù)雜,能以數(shù)組的方式返回指定的內(nèi)容演顾,一共有兩種:

@distinctUnionOfObjects

@unionOfObjects

它們的返回值都是NSArray供搀,區(qū)別是前者返回的元素都是唯一的,是去重以后的結(jié)果钠至;后者返回的元素是全集葛虐。

例如:

#import<Foundation/Foundation.h>@interfaceBook:NSObject@property(nonatomic,copy)NSString* name;@property(nonatomic,assign)CGFloatprice;@end@implementationBook@endintmain(intargc,constchar* argv[]) {@autoreleasepool{? ? ? ? ? ? ? ? Book *book1 = [Book new];? ? ? ? book1.name =@"The Great Gastby";? ? ? ? book1.price =40;? ? ? ? Book *book2 = [Book new];? ? ? ? book2.name =@"Time History";? ? ? ? book2.price =20;? ? ? ? Book *book3 = [Book new];? ? ? ? book3.name =@"Wrong Hole";? ? ? ? book3.price =30;? ? ? ? ? ? ? ? Book *book4 = [Book new];? ? ? ? book4.name =@"Wrong Hole";? ? ? ? book4.price =10;NSArray* arrBooks = @[book1,book2,book3,book4];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);? ? ? ? }? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 17:06:21.832401+0800 KVCKVO[35798:6358293] distinctUnionOfObjects

2018-05-05 17:06:21.833079+0800 KVCKVO[35798:6358293] 10.000000

2018-05-05 17:06:21.833112+0800 KVCKVO[35798:6358293] 20.000000

2018-05-05 17:06:21.833130+0800 KVCKVO[35798:6358293] 30.000000

2018-05-05 17:06:21.833146+0800 KVCKVO[35798:6358293] 40.000000

2018-05-05 17:06:21.833165+0800 KVCKVO[35798:6358293] unionOfObjects

2018-05-05 17:06:21.833297+0800 KVCKVO[35798:6358293] 40.000000

2018-05-05 17:06:21.833347+0800 KVCKVO[35798:6358293] 20.000000

2018-05-05 17:06:21.833371+0800 KVCKVO[35798:6358293] 30.000000

2018-05-05 17:06:21.833388+0800 KVCKVO[35798:6358293] 10.000000

KVC處理字典

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

KVC里面還有兩個關(guān)于NSDictionary的方法:

- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;

dictionaryWithValuesForKeys:是指輸入一組key,返回這組key對應(yīng)的屬性宪卿,再組成一個字典的诵。

setValuesForKeysWithDictionary是用來修改Model中對應(yīng)key的屬性。下面直接用代碼會更直觀一點(diǎn):

#import<Foundation/Foundation.h>@interfaceAddress:NSObject@end@interfaceAddress()@property(nonatomic,copy)NSString* country;@property(nonatomic,copy)NSString* province;@property(nonatomic,copy)NSString* city;@property(nonatomic,copy)NSString* district;@end@implementationAddress@endintmain(intargc,constchar* argv[]) {@autoreleasepool{//模型轉(zhuǎ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];//把對應(yīng)key所有的屬性全部取出來NSLog(@"%@",dict);//字典轉(zhuǎn)模型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);? ? ? ? ? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 17:08:48.824653+0800 KVCKVO[35807:6368235] {

city = "Shen Zhen";

country = China;

district = "Nan Shan";

province = "Guang Dong";

}

2018-05-05 17:08:48.825075+0800 KVCKVO[35807:6368235] country:USA province:california city:Los angle

打印出來的結(jié)果完全符合預(yù)期佑钾。

KVC使用

KVC在iOS開發(fā)中是絕不可少的利器西疤,這種基于運(yùn)行時的編程方式極大地提高了靈活性,簡化了代碼次绘,甚至實(shí)現(xiàn)很多難以想像的功能瘪阁,KVC也是許多iOS開發(fā)黑魔法的基礎(chǔ)撒遣。

下面列舉iOS開發(fā)中KVC的使用場景.

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

利用KVC動態(tài)的取值和設(shè)值是最基本的用途了邮偎。

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

對于類里的私有屬性,Objective-C是無法直接訪問的义黎,但是KVC是可以的禾进。

Model和字典轉(zhuǎn)換

這是KVC強(qiáng)大作用的又一次體現(xiàn),KVC和Objc的runtime組合可以很容易的實(shí)現(xiàn)Model和字典的轉(zhuǎn)換廉涕。

修改一些控件的內(nèi)部屬性

這也是iOS開發(fā)中必不可少的小技巧泻云。眾所周知很多UI控件都由很多內(nèi)部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API狐蜕,這樣我們就無法正常地訪問和修改這些控件的樣式宠纯。

而KVC在大多數(shù)情況可下可以解決這個問題。最常用的就是個性化UITextField中的placeHolderText了层释。

操作集合

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

用KVC實(shí)現(xiàn)高階消息傳遞

當(dāng)對容器類使用KVC時廉白,valueForKey:將會被傳遞給容器中的每一個對象个初,而不是容器本身進(jìn)行操作。結(jié)果會被添加進(jìn)返回的容器中猴蹂,這樣院溺,開發(fā)者可以很方便的操作集合來返回另一個集合。

#import<Foundation/Foundation.h>intmain(intargc,constchar* argv[]) {@autoreleasepool{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);? ? ? ? }? ? ? ? ? ? }return0;}

打印結(jié)果:

2018-05-05 17:16:21.975983+0800 KVCKVO[35824:6395514] English

2018-05-05 17:16:21.976296+0800 KVCKVO[35824:6395514] Franch

2018-05-05 17:16:21.976312+0800 KVCKVO[35824:6395514] Chinese

2018-05-05 17:16:21.976508+0800 KVCKVO[35824:6395514] 7

2018-05-05 17:16:21.976533+0800 KVCKVO[35824:6395514] 6

2018-05-05 17:16:21.976550+0800 KVCKVO[35824:6395514] 7

方法capitalizedString被傳遞到NSArray中的每一項(xiàng)磅轻,這樣珍逸,NSArray的每一員都會執(zhí)行capitalizedString并返回一個包含結(jié)果的新的NSArray。

從打印結(jié)果可以看出瓢省,所有String都成功以轉(zhuǎn)成了大寫弄息。

同樣如果要執(zhí)行多個方法也可以用valueForKeyPath:方法。它先會對每一個成員調(diào)用 capitalizedString方法勤婚,然后再調(diào)用length摹量,因?yàn)閘enth方法返回是一個數(shù)字,所以返回結(jié)果以NSNumber的形式保存在新數(shù)組里馒胆。

實(shí)現(xiàn)KVO

KVO是基于KVC實(shí)現(xiàn)的缨称,下面講一下KVO的概念和實(shí)現(xiàn)。

KVO

KVO定義

KVO 即 Key-Value Observing祝迂,翻譯成鍵值觀察睦尽。它是一種觀察者模式的衍生。其基本思想是型雳,對目標(biāo)對象的某屬性添加觀察当凡,當(dāng)該屬性發(fā)生變化時,通過觸發(fā)觀察者對象實(shí)現(xiàn)的KVO接口方法纠俭,來自動的通知觀察者沿量。

觀察者模式是什么

一個目標(biāo)對象管理所有依賴于它的觀察者對象,并在它自身的狀態(tài)改變時主動通知觀察者對象冤荆。這個主動通知通常是通過調(diào)用各觀察者對象所提供的接口方法來實(shí)現(xiàn)的朴则。觀察者模式較完美地將目標(biāo)對象與觀察者對象解耦。

簡單來說KVO可以通過監(jiān)聽key钓简,來獲得value的變化乌妒,用來在對象之間監(jiān)聽狀態(tài)變化。KVO的定義都是對NSObject的擴(kuò)展來實(shí)現(xiàn)的外邓,Objective-C中有個顯式的NSKeyValueObserving類別名撤蚊,所以對于所有繼承了NSObject的類型,都能使用KVO(一些純Swift類和結(jié)構(gòu)體是不支持KVC的损话,因?yàn)闆]有繼承NSObject)侦啸。

KVO使用

注冊與解除注冊

如果我們已經(jīng)有了包含可供鍵值觀察屬性的類,那么就可以通過在該類的對象(被觀察對象)上調(diào)用名為 NSKeyValueObserverRegistration 的 category 方法將觀察者對象與被觀察者對象注冊與解除注冊:

- (void)addObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options context:(void*)context;- (void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath;

observer:觀察者,也就是KVO通知的訂閱者匹中。訂閱著必須實(shí)現(xiàn)observeValueForKeyPath:ofObject:change:context:方法keyPath:描述將要觀察的屬性夏漱,相對于被觀察者。options:KVO的一些屬性配置顶捷;有四個選項(xiàng)挂绰。context: 上下文,這個會傳遞到訂閱著的函數(shù)中服赎,用來區(qū)分消息葵蒂,所以應(yīng)當(dāng)是不同的。

options所包括的內(nèi)容

NSKeyValueObservingOptionNew:change字典包括改變后的值NSKeyValueObservingOptionOld:change字典包括改變前的值NSKeyValueObservingOptionInitial:注冊后立刻觸發(fā)KVO通知NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變后通知兩次)

這兩個方法的定義在 Foundation/NSKeyValueObserving.h 中重虑,NSObject践付,NSArray,NSSet均實(shí)現(xiàn)了以上方法缺厉,因此我們不僅可以觀察普通對象永高,還可以觀察數(shù)組或結(jié)合類對象。在該頭文件中提针,我們還可以看到 NSObject 還實(shí)現(xiàn)了 NSKeyValueObserverNotification 的 category 方法(更多類似方法命爬,請查看該頭文件NSKeyValueObserving.h):

- (void)willChangeValueForKey:(NSString*)key;- (void)didChangeValueForKey:(NSString*)key;

這兩個方法在手動實(shí)現(xiàn)鍵值觀察時會用到。注意在不用的時候辐脖,不要忘記解除注冊饲宛,否則會導(dǎo)致內(nèi)存泄露。

處理變更通知

每當(dāng)監(jiān)聽的keyPath發(fā)生變化了嗜价,就會在這個函數(shù)中回調(diào)艇抠。

- (void)observeValueForKeyPath:(NSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(id)object? ? ? ? ? ? ? ? ? ? ? ? change:(NSDictionary*)change? ? ? ? ? ? ? ? ? ? ? context:(void*)context

在這里,change 這個字典保存了變更信息久锥,具體是哪些信息取決于注冊時的 NSKeyValueObservingOptions家淤。

手動KVO(禁用KVO)

KVO的實(shí)現(xiàn),是對注冊的keyPath中自動實(shí)現(xiàn)了兩個函數(shù)奴拦,在setter中媒鼓,自動調(diào)用届吁。

- (void)willChangeValueForKey:(NSString*)key- (void)didChangeValueForKey:(NSString*)key

可能有時候错妖,我們要實(shí)現(xiàn)手動的KVO,或者我們實(shí)現(xiàn)的類庫不希望被KVO疚沐。

這時候需要關(guān)閉自動生成KVO通知暂氯,然后手動的調(diào)用,手動通知的好處就是亮蛔,可以靈活加上自己想要的判斷條件痴施。下面看個例子如下:

@interfaceTarget:NSObject{intage;}// for manual KVO - age- (int) age;- (void) setAge:(int)theAge;@end@implementationTarget- (id) init{self= [superinit];if(nil!=self)? ? {? ? ? ? age =10;? ? }returnself;}// for manual KVO - age- (int) age{returnage;}- (void) setAge:(int)theAge{? ? [selfwillChangeValueForKey:@"age"];? ? age = theAge;? ? [selfdidChangeValueForKey:@"age"];}+ (BOOL) automaticallyNotifiesObserversForKey:(NSString*)key {if([key isEqualToString:@"age"]) {returnNO;? ? }return[superautomaticallyNotifiesObserversForKey:key];}@end

首先,需要手動實(shí)現(xiàn)屬性的 setter 方法,并在設(shè)置操作的前后分別調(diào)用 willChangeValueForKey: 和 didChangeValueForKey方法辣吃,這兩個方法用于通知系統(tǒng)該 key 的屬性值即將和已經(jīng)變更了动遭;

其次,要實(shí)現(xiàn)類方法 automaticallyNotifiesObserversForKey神得,并在其中設(shè)置對該 key 不自動發(fā)送通知(返回 NO 即可)厘惦。這里要注意,對其它非手動實(shí)現(xiàn)的 key哩簿,要轉(zhuǎn)交給 super 來處理宵蕉。

如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey返回NO,實(shí)現(xiàn)屬性的 setter 方法节榜,不進(jìn)行調(diào)用willChangeValueForKey: 和 didChangeValueForKey方法羡玛。

鍵值觀察依賴鍵

有時候一個屬性的值依賴于另一對象中的一個或多個屬性,如果這些屬性中任一屬性的值發(fā)生變更宗苍,被依賴的屬性值也應(yīng)當(dāng)為其變更進(jìn)行標(biāo)記稼稿。因此,object 引入了依賴鍵讳窟。

觀察依賴鍵

觀察依賴鍵的方式與前面描述的一樣渺杉,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加處理變更通知的代碼:

#import"TargetWrapper.h"- (void) observeValueForKeyPath:(NSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(id)object? ? ? ? ? ? ? ? ? ? ? ? ? change:(NSDictionary*)change? ? ? ? ? ? ? ? ? ? ? ? context:(void*)context{if([keyPath isEqualToString:@"age"])? ? {? ? ? ? Class classInfo = (Class)context;NSString* className = [NSStringstringWithCString:object_getClassName(classInfo)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? encoding:NSUTF8StringEncoding];NSLog(@" >> class: %@, Age changed", className);NSLog(@" old age is %@", [change objectForKey:@"old"]);NSLog(@" new age is %@", [change objectForKey:@"new"]);? ? }elseif([keyPath isEqualToString:@"information"])? ? {? ? ? ? Class classInfo = (Class)context;NSString* className = [NSStringstringWithCString:object_getClassName(classInfo)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? encoding:NSUTF8StringEncoding];NSLog(@" >> class: %@, Information changed", className);NSLog(@" old information is %@", [change objectForKey:@"old"]);NSLog(@" new information is %@", [change objectForKey:@"new"]);? ? }else{? ? ? ? [superobserveValueForKeyPath:keyPath? ? ? ? ? ? ? ? ? ? ? ? ? ? ofObject:object? ? ? ? ? ? ? ? ? ? ? ? ? ? ? change:change? ? ? ? ? ? ? ? ? ? ? ? ? ? ? context:context];? ? }}

實(shí)現(xiàn)依賴鍵

在這里,觀察的是 TargetWrapper 類的 information 屬性挪钓,該屬性是依賴于 Target 類的 age 和 grade 屬性是越。為此,我在 Target 中添加了 grade 屬性:

@interfaceTarget:NSObject@property(nonatomic,readwrite)intgrade;@property(nonatomic,readwrite)intage;@end@implementationTarget@synthesizeage;// for automatic KVO - age@synthesizegrade;@end

下面來看看 TragetWrapper 中的依賴鍵屬性是如何實(shí)現(xiàn)的碌上。

@classTarget;@interfaceTargetWrapper:NSObject{@privateTarget * _target;}@property(nonatomic,assign)NSString* information;@property(nonatomic,retain) Target * target;-(id) init:(Target *)aTarget;@end#import"TargetWrapper.h"#import"Target.h"@implementationTargetWrapper@synthesizetarget = _target;-(id) init:(Target *)aTarget{self= [superinit];if(nil!=self) {? ? ? ? _target = [aTargetretain];? ? }returnself;}-(void) dealloc{self.target =nil;? ? [superdealloc];}- (NSString*)information{return[[[NSStringalloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];}- (void)setInformation:(NSString*)theInformation{NSArray* array = [theInformation componentsSeparatedByString:@"#"];? ? [_target setGrade:[[array objectAtIndex:0] intValue]];? ? [_target setAge:[[array objectAtIndex:1] intValue]];}+ (NSSet*)keyPathsForValuesAffectingInformation{NSSet* keyPaths = [NSSetsetWithObjects:@"target.age",@"target.grade",nil];returnkeyPaths;}//+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key//{//? ? NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];//? ? NSArray * moreKeyPaths = nil;//? ? //? ? if ([key isEqualToString:@"information"])//? ? {//? ? ? ? moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];//? ? }//? ? //? ? if (moreKeyPaths)//? ? {//? ? ? ? keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];//? ? }//? ? //? ? return keyPaths;//}@end

首先倚评,要手動實(shí)現(xiàn)屬性 information 的 setter/getter 方法,在其中使用 Target 的屬性來完成其 setter 和 getter馏予。

其次天梧,要實(shí)現(xiàn) keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法來告訴系統(tǒng) information 屬性依賴于哪些其他屬性,這兩個方法都返回一個key-path 的集合霞丧。在這里要多說幾句呢岗,如果選擇實(shí)現(xiàn) keyPathsForValuesAffectingValueForKey,要先獲取 super 返回的結(jié)果 set蛹尝,然后判斷 key 是不是目標(biāo) key后豫,如果是就將依賴屬性的 key-path 結(jié)合追加到 super 返回的結(jié)果 set 中,否則直接返回 super的結(jié)果突那。

在這里挫酿,information 屬性依賴于 target 的 age 和 grade 屬性,target 的 age/grade 屬性任一發(fā)生變化愕难,information 的觀察者都會得到通知早龟。

Observer * observer = [[[Observer alloc] init] autorelease];Target * target = [[[Target alloc] init] autorelease];TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];[wrapper addObserver:observer? ? ? ? ? forKeyPath:@"information"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:[TargetWrapperclass]];[target setAge:30];[target setGrade:1];[wrapper removeObserver:observer forKeyPath:@"information"];

打印結(jié)果:

class: TargetWrapper, Information changed

old information is 0#10

new information is 0#30

class: TargetWrapper, Information changed

old information is 0#30

new information is 1#30

KVO和線程

一個需要注意的地方是惫霸,KVO 行為是同步的,并且發(fā)生與所觀察的值發(fā)生變化的同樣的線程上葱弟。沒有隊(duì)列或者 Run-loop 的處理壹店。手動或者自動調(diào)用 -didChange... 會觸發(fā) KVO 通知。

所以芝加,當(dāng)我們試圖從其他線程改變屬性值的時候我們應(yīng)當(dāng)十分小心茫打,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來說妖混,我們不推薦把 KVO 和多線程混起來老赤。如果我們要用多個隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO制市。

KVO 是同步運(yùn)行的這個特性非常強(qiáng)大抬旺,只要我們在單一線程上面運(yùn)行(比如主隊(duì)列 main queue),KVO 會保證下列兩種情況的發(fā)生:

首先祥楣,如果我們調(diào)用一個支持 KVO 的 setter 方法开财,如下所示:

self.exchangeRate =2.345;

KVO 能保證所有 exchangeRate 的觀察者在 setter 方法返回前被通知到。

其次误褪,如果某個鍵被觀察的時候附上了 NSKeyValueObservingOptionPrior 選項(xiàng)责鳍,直到 -observe... 被調(diào)用之前, exchangeRate 的 accessor 方法都會返回同樣的值兽间。

KVO實(shí)現(xiàn)

KVO 是通過 isa-swizzling 實(shí)現(xiàn)的历葛。

基本的流程就是編譯器自動為被觀察對象創(chuàng)造一個派生類,并將被觀察對象的isa 指向這個派生類嘀略。如果用戶注冊了對某此目標(biāo)對象的某一個屬性的觀察恤溶,那么此派生類會重寫這個方法,并在其中添加進(jìn)行通知的代碼帜羊。Objective-C 在發(fā)送消息的時候咒程,會通過 isa 指針找到當(dāng)前對象所屬的類對象。而類對象中保存著當(dāng)前對象的實(shí)例方法讼育,因此在向此對象發(fā)送消息時候帐姻,實(shí)際上是發(fā)送到了派生類對象的方法。由于編譯器對派生類的方法進(jìn)行了 override奶段,并添加了通知代碼饥瓷,因此會向注冊的對象發(fā)送通知。注意派生類只重寫注冊了觀察者的屬性方法忧饭。

蘋果官方文檔的說明如下:

Key-Value Observing Implementation Details

Automatic key-value observing is implemented using a technique called?isa-swizzling.

The?isa?pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.

When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.

You should never rely on the?isa?pointer to determine class membership. Instead, you should use the?class?method to determine the class of an object instance.

KVO的實(shí)現(xiàn)依賴于Runtime的強(qiáng)大動態(tài)能力扛伍。

即當(dāng)一個類型為 ObjectA 的對象筷畦,被添加了觀察后词裤,系統(tǒng)會生成一個 NSKVONotifying_ObjectA 類刺洒,并將對象的isa指針指向新的類,也就是說這個對象的類型發(fā)生了變化吼砂。這個類相比較于ObjectA逆航,會重寫以下幾個方法。

重寫setter

在 setter 中渔肩,會添加以下兩個方法的調(diào)用因俐。

- (void)willChangeValueForKey:(NSString*)key;- (void)didChangeValueForKey:(NSString*)key;

然后在?didChangeValueForKey:?中,去調(diào)用:

- (void)observeValueForKeyPath:(nullableNSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(nullableid)object? ? ? ? ? ? ? ? ? ? ? ? change:(nullableNSDictionary *)change? ? ? ? ? ? ? ? ? ? ? context:(nullablevoid*)context;

包含了新值和舊值的通知周偎。

于是實(shí)現(xiàn)了屬性值修改的通知抹剩。因?yàn)?KVO 的原理是修改 setter 方法,因此使用 KVO 必須調(diào)用 setter 蓉坎。若直接訪問屬性對象則沒有效果澳眷。

重寫class

當(dāng)修改了isa指向后,class的返回值不會變蛉艾,但isa的值則發(fā)生改變钳踊。

#import<Foundation/Foundation.h>#import<objc/runtime.h>@interfaceObjectA:NSObject@property(nonatomic)NSIntegerage;@end@implementationObjectA@end@interfaceObjectB:NSObject@end@implementationObjectB- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context {NSLog(@"%@", change);}@endintmain(intargc,constchar* argv[]) {@autoreleasepool{// insert code here...//生成對象ObjectA *objA = [[ObjectA alloc] init];? ? ? ? ObjectB *objB = [[ObjectB alloc] init];// 添加Observer之后[objA addObserver:objB forKeyPath:@"age"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOldcontext:nil];// 輸出ObjectANSLog(@"%@", [objAclass]);// 輸出NSKVONotifying_ObjectA(object_getClass方法返回isa指向)NSLog(@"%@", object_getClass(objA));? ? }return0;}

打印結(jié)果:

2018-05-06 22:47:05.538899+0800 KVCKVO[38474:13343992] ObjectA

2018-05-06 22:47:05.539242+0800 KVCKVO[38474:13343992] NSKVONotifying_ObjectA

重寫dealloc

系統(tǒng)重寫 dealloc 方法來釋放資源。

重寫_isKVOA

這個私有方法是用來標(biāo)示該類是一個 KVO 機(jī)制聲稱的類勿侯。

如何證明被觀察的類被重寫了以上方法

參考用代碼探討 KVC/KVO 的實(shí)現(xiàn)原理這篇文章拓瞪,通過代碼一步步分析,從斷點(diǎn)截圖來看助琐,可以很好證明以上被重寫的方法祭埂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市兵钮,隨后出現(xiàn)的幾起案子沟堡,更是在濱河造成了極大的恐慌,老刑警劉巖矢空,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航罗,死亡現(xiàn)場離奇詭異,居然都是意外死亡屁药,警方通過查閱死者的電腦和手機(jī)粥血,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酿箭,“玉大人复亏,你說我怎么就攤上這事$缘眨” “怎么了缔御?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長妇蛀。 經(jīng)常有香客問我耕突,道長笤成,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任眷茁,我火速辦了婚禮炕泳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘上祈。我一直安慰自己培遵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布登刺。 她就那樣靜靜地躺著籽腕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纸俭。 梳的紋絲不亂的頭發(fā)上节仿,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音掉蔬,去河邊找鬼廊宪。 笑死,一個胖子當(dāng)著我的面吹牛女轿,可吹牛的內(nèi)容都是我干的箭启。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼蛉迹,長吁一口氣:“原來是場噩夢啊……” “哼傅寡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起北救,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤荐操,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后珍策,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體托启,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年攘宙,在試婚紗的時候發(fā)現(xiàn)自己被綠了屯耸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹭劈,死狀恐怖疗绣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铺韧,我是刑警寧澤多矮,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站哈打,受9級特大地震影響塔逃,放射性物質(zhì)發(fā)生泄漏讯壶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一患雏、第九天 我趴在偏房一處隱蔽的房頂上張望鹏溯。 院中可真熱鬧罢维,春花似錦淹仑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至平窘,卻和暖如春吓肋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瑰艘。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工是鬼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人紫新。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓均蜜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親芒率。 傳聞我的和親對象是個殘疾皇子囤耳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345