詳解KVC

簡(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è)方法

689DEE32-AE30-4D6F-8667-E4DB867E30FD.png
- (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é)果:

E064FD62-792A-4DE1-A0DB-64E05DA0C534.png

這說明了重寫+(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é)果:

185110EF-10D0-4B67-B110-75FD4881D368.png
    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)的爽醋。

為什么說KVO是基于KVC實(shí)現(xiàn)的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蚁署,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蚂四,更是在濱河造成了極大的恐慌光戈,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遂赠,死亡現(xiàn)場(chǎng)離奇詭異久妆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)跷睦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門筷弦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事烂琴〉猓” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵奸绷,是天一觀的道長(zhǎng)梗夸。 經(jīng)常有香客問我,道長(zhǎng)号醉,這世上最難降的妖魔是什么反症? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮扣癣,結(jié)果婚禮上惰帽,老公的妹妹穿的比我還像新娘憨降。我一直安慰自己父虑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布授药。 她就那樣靜靜地躺著士嚎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悔叽。 梳的紋絲不亂的頭發(fā)上莱衩,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音娇澎,去河邊找鬼笨蚁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛趟庄,可吹牛的內(nèi)容都是我干的括细。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼戚啥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奋单!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起猫十,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤览濒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拖云,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贷笛,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年宙项,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乏苦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杉允,死狀恐怖邑贴,靈堂內(nèi)的尸體忽然破棺而出席里,到底是詐尸還是另有隱情,我是刑警寧澤拢驾,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布奖磁,位于F島的核電站,受9級(jí)特大地震影響繁疤,放射性物質(zhì)發(fā)生泄漏咖为。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一稠腊、第九天 我趴在偏房一處隱蔽的房頂上張望躁染。 院中可真熱鬧,春花似錦架忌、人聲如沸吞彤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饰恕。三九已至,卻和暖如春井仰,著一層夾襖步出監(jiān)牢的瞬間埋嵌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工俱恶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雹嗦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓合是,卻偏偏與公主長(zhǎng)得像了罪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子端仰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 原文地址:http://ios.jobbole.com/84954/ KVC(Key-value coding)鍵...
    小曼blog閱讀 689評(píng)論 0 1
  • 什么是KVC捶惜? KVC(Key-value coding)鍵值編碼,單看這個(gè)名字可能不太好理解荔烧。其實(shí)是指iOS的開...
    薩繆閱讀 4,637評(píng)論 1 13
  • 什么是KVC吱七? KVC(Key-value coding)鍵值編碼,單看這個(gè)名字可能不太好理解鹤竭。其實(shí)是指iOS的開...
    薩繆閱讀 814評(píng)論 0 5
  • 什么是KVC踊餐? KVC(Key-value coding)鍵值編碼,單看這個(gè)名字可能不太好理解臀稚。其實(shí)是指iOS的開...
    祀夢(mèng)_閱讀 928評(píng)論 0 7
  • KVC(Key-value coding)鍵值編碼吝岭,iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對(duì)象的屬性,...
    CALayer_Sai閱讀 2,523評(píng)論 0 4