KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iOS的開發中,能夠容許開發者經過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不須要調用明確的存取方法。這樣就能夠在運行時動態在訪問和修改對象的屬性。而不是在編譯時肯定,這也是iOS開發中的黑魔法之一。不少高級的iOS開發技巧都是基於KVC實現的。目前網上關於KVC的文章在很是多,有的只是簡單地說了下用法,有的講得深刻可是在使用場景和最佳實踐沒有說明,我寫下這遍文章就是給你們詳解一個最完整最詳細的KVC。git
不管是Swift仍是Objective-C,KVC的定義都是對NSObject的擴展來實現的(Objective-c中有個顯式的NSKeyValueCoding
類別名,而Swift沒有,也不須要)因此對於全部繼承了NSObject在類型,都能使用KVC(一些純Swift類和結構體是不支持KVC的),下面是KVC最爲重要的四個方法github
1
2
3
4
|
- (nullable id)valueForKey:(NSString *)key; //直接經過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //經過Key來設值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //經過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //經過KeyPath來設值
|
固然NSKeyValueCoding類別中還有其餘的一些方法,下面列舉一些編程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
+ (BOOL)accessInstanceVariablesDirectly;
//默認返回YES,表示若是沒有找到Set方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值確認的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回
- (nullable id)valueForUndefinedKey:(NSString *)key;
//若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個方法同樣,只不過是設值。
- (void)setNilValueForKey:(NSString *)key;
//若是你在SetValue方法時面給Value傳nil,則會調用這個方法
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
//輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
|
上面的這些方法在碰到特殊狀況或者有特殊需求仍是會用到的,因此也是能夠了解一下。後面的代碼示例會有講到其中的一些方法。
同時蘋果對一些容器類好比NSArray或者NSSet等,KVC有着特殊的實現。建議有基礎的或者英文好的開發者直接去看蘋果的官方文檔,相信你會對KVC的理解更上一個臺階。api
KVC是怎麼使用的,我相信絕大多數的開發者都很清楚,我在這裏就再也不寫簡單的使用KVC來設值和取值的代碼了,首頁咱們來探討KVC在內部是按什麼樣的順序來尋找key的。
當調用setValue:屬性值 forKey:@」name「
的代碼時,底層的執行機制以下:數組
set:屬性值
方法,代碼經過setter方法完成設置。注意,這裏的是指成員變量名,首字母大清寫要符合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:
方法。app
下面咱們來讓代碼來測試一下上面的KVC機制函數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
@interface Dog : NSObject
@end
@implementation Dog
{
NSString* toSetName;
NSString* isName;
//NSString* name;
NSString* _name;
NSString* _isName;
}
// -(void)setName:(NSString*)name{
// toSetName = name;
// }
//-(NSString*)getName{
// return toSetName;
//}
+(BOOL)accessInstanceVariablesDirectly{
return NO;
}
-(id)valueForUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
return nil;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
NSLog(@"出現異常,該key不存在%@",key);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Dog* dog = [Dog new];
[dog setValue:@"newName" forKey:@"name"];
NSString* name = [dog valueForKey:@"toSetName"];
NSLog(@"%@",name);
}
return 0;
}
|
首先咱們先重寫accessInstanceVariablesDirectly
方法讓其返回NO,再運行代碼(注意上面註釋的部分),XCode直接打印出測試
1
2
3
|
2016-04-15 15:52:12.039 DemoKVC[9681:287627] 出現異常,該key不存在name
2016-04-15 15:52:12.040 DemoKVC[9681:287627] 出現異常,該key不存在toSetName
2016-04-15 15:52:12.040 DemoKVC[9681:287627] (null)
|
這說明了重寫+(BOOL)accessInstanceVariablesDirectly
方法讓其返回NO後,KVC找不到SetName:方法後,再也不去找name系列成員變量,而是直接調用forUndefinedKey方法
因此開發者若是不想讓本身的類實現KVC,就能夠這麼作。
下面那兩個setter和gettr的註釋取消掉,再把編碼
1
|
NSString* name = [dog valueForKey:@"toSetName"]; 換成 NSString* name = [dog valueForKey:@"name"];
|
XCode就能夠正確地打印出正確的值了atom
1
|
2016-04-15 15:56:22.130 DemoKVC[9726:289258] newName
|
下面再註釋到accessInstanceVariablesDirectly
方法,就能測試其餘的key查找順序了,爲了節省篇幅,剩下的的KVC對於key尋找機制就不在這裏展現了,有興趣的讀者能夠寫代碼去驗證。
當調用ValueforKey:@」name「
的代碼時,KVC對key的搜索方式不一樣於setValue:屬性值 forKey:@」name「
,其搜索方式以下
get
,
,is
的順序方法查找getter方法,找到的話會直接調用。若是是BOOL或者int等值類型, 會作NSNumber轉換countOf
,objectInAtIndex
,AtIndex
格式的方法。若是countOf
和另外兩個方法中的要個被找到,那麼就會返回一個能夠響應NSArray
所的方法的代理集合(它是NSKeyValueArray
,是NSArray
的子類),調用這個代理集合的方法,或者說給這個代理集合發送NSArray
的方法,就會以countOf
,objectInAtIndex
,AtIndex
這幾個方法組合的形式調用。還有一個可選的get:range:
方法。因此你想從新定義KVC的一些功能,你能夠添加這些方法,須要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。countOf
,enumeratorOf
,memberOf
格式的方法。若是這三個方法都找到,那麼就返回一個能夠響應NSSet所的方法的代理集合,以送給這個代理集合消息方法,就會以countOf
,enumeratorOf
,memberOf
組合的形式調用。+ (BOOL)accessInstanceVariablesDirectly
,若是返回YES(默認行爲),那麼和先前的設值同樣,會按_,_is,,is
的順序搜索成員變量名,這裏不推薦這麼作,由於這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱。若是重寫了類方法+ (BOOL)accessInstanceVariablesDirectly
返回NO的話,那麼會直接調用valueForUndefinedKey:
valueForUndefinedKey:
下面再上代碼測試
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
@interface TwoTimesArray : NSObject
-(void)incrementCount;
-(NSUInteger)countOfNumbers;
-(id)objectInNumbersAtIndex:(NSUInteger)index;
@end
@interface TwoTimesArray()
@property (nonatomic,readwrite,assign) NSUInteger count;
@property (nonatomic,copy) NSString* arrName;
@end
@implementation TwoTimesArray
-(void)incrementCount{
self.count ++;
}
-(NSUInteger)countOfNumbers{
return self.count;
-(id)objectInNumbersAtIndex:(NSUInteger)index{ //當key使用numbers時,KVC會找到這兩個方法。
return @(index * 2);
}
-(NSInteger)getNum{ //第一個,本身一個一個註釋試
return 10;
}
-(NSInteger)num{ //第二個
return 11;
}
-(NSInteger)isNum{ //第三個
return 12;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TwoTimesArray* arr = [TwoTimesArray new];
NSNumber* num = [arr valueForKey:@"num"];
NSLog(@"%@",num);
id ar = [arr valueForKey:@"numbers"];
NSLog(@"%@",NSStringFromClass([ar class]));
NSLog(@"0:%@ 1:%@ 2:%@ 3:%@",ar[0],ar[1],ar[2],ar[3]);
[arr incrementCount]; //count加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出1
[arr incrementCount]; //count再加1
NSLog(@"%lu",(unsigned long)[ar count]); //打印出2
[arr setValue:@"newName" forKey:@"arrName"];
NSString* name = [arr valueForKey:@"arrName"];
NSLog(@"%@",name);
}
return 0;
}
//打印結果
2016-04-17 15:39:42.214 KVCDemo[1088:74481] 10
2016-04-17 15:39:42.215 KVCDemo[1088:74481] NSKeyValueArray
2016-04-17 15:41:24.713 KVCDemo[1102:75424] 0:0 1:2 2:4 3:6 //太明顯了,直接調用-(id)objectInNumbersAtIndex:(NSUInteger)index;方法
2016-04-17 15:39:42.215 KVCDemo[1088:74481] 1
2016-04-17 15:39:42.215 KVCDemo[1088:74481] 2
2016-04-17 15:39:42.215 KVCDemo[1088:74481] newName
|
很明顯,上面的代碼充分說明了說明了KVC在調用ValueforKey:@」name「
時搜索key的機制。不過還有些功能沒有所有列出,有興趣的讀者能夠寫代碼去驗證。
然而在開發過程當中,一個類的成員變量有多是其餘的自定義類,你能夠先用KVC獲取出來再該屬性,而後再次用KVC來獲取這個自定義類的屬性,但這樣是比較繁瑣的,對此,KVC提供了一個解決方案,那就是鍵路徑KeyPath。
1
2
|
- (nullable id)valueForKeyPath:(NSString *)keyPath; //經過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //經過KeyPath來設值
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@interface Address : NSObject
@end
@interface Address()
@property (nonatomic,copy)NSString* country;
@end
@implementation Address
@end
@interface People : NSObject
@end
@interface People()
@property (nonatomic,copy) NSString* name;
@property (nonatomic,strong) Address* address;
@property (nonatomic,assign) NSInteger age;
@end
@implementation People
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
People* people1 = [People new];
Address* add = [Address new];
add.country = @"China";
people1.address = add;
NSString* country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
[people1 setValue:@"USA" forKeyPath:@"address.country"];
country1 = people1.address.country;
country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
}
return 0;
}
//打印結果
2016-04-17 15:55:22.487 KVCDemo[1190:82636] country1:China country2:China
2016-04-17 15:55:22.489 KVCDemo[1190:82636] country1:USA country2:USA
|
上面的代碼簡單在展現了KeyPath是怎麼用的。若是你不當心錯誤的使用了key而非KeyPath的話,KVC會直接查找address.country
這個屬性,很明顯,這個屬性並不存在,因此會再調用UndefinedKey
相關方法。而KVC對於KeyPath是搜索機制第一步就是分離key,用小數點.
來分割key,而後再像普通key同樣按照先前介紹的順序搜索下去。
KVC中最多見的異常就是不當心使用了錯誤的Key,或者在設值中不當心傳遞了nil的值,KVC中有專門的方法來處理這些異常。
一般在用KVC操做Model時,拋出異常的那兩個方法是須要重寫的。雖然通常很小出現傳遞了錯誤的Key值這種狀況,可是若是不當心出現了,直接拋出異常讓APP崩潰顯然是不合理的。
通常在這裏直接讓這個Key打印出來便可,或者有些特殊狀況須要特殊處理。
一般狀況下,KVC不容許你要在調用setValue:屬性值 forKey:@」name「
(或者keyPath)時對非對象傳遞一個nil的值。很簡單,由於值類型是不能爲nil的。若是你不當心傳了,KVC會調用setNilValueForKey:
方法。這個方法默認是拋出異常,因此通常而言最好仍是重寫這個方法。
1
2
|
[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.' // 調用setNilValueForKey拋出異常
|
若是重寫setNilValueForKey:
就沒問題了
1
2
3
4
5
6
7
8
9
|
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能將%@設成nil",key);
}
@end
//打印出
2016-04-17 16:19:55.298 KVCDemo[1304:92472] 不能將age設成nil
|
不是每個方法都返回對象,可是valueForKey:
老是返回一個id對象,若是本來的變量類型是值類型或者結構體,返回值會封裝成NSNumber或者NSValue對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。而後開以者須要手動轉換成原來的類型。儘管valueForKey:
會自動將值類型封裝成對象,可是setValue:forKey:
卻不行。你必須手動將值類型轉換成NSNumber
或者NSValue
類型,才能傳遞過去。
對於自定義對象,KVC也會正確以設值和取值。由於傳遞進去和取出來的都是id類型,因此須要開發者本身擔保類型的正確性,運行時Objective-C在發送消息的會檢查類型,若是錯誤會直接拋出異常。
1
2
3
4
5
6
7
8
|
Address* add2 = [Address new];
add2.country = @"England";
[people1 setValue:add2 forKey:@"address"];
NSString* country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
//打印結果
2016-04-17 16:29:36.349 KVCDemo[1346:95910] country1:England country2:England
|
對象的屬性能夠是一對一的,也能夠是一對多的。一對多的屬性要麼是有序的(數組),要麼是無序的(數組)
不可變的有序容器屬性(NSArray)和無序容器屬性(NSSet)通常可使用valueForKey:
來獲取。好比有一個叫items的NSArray屬性,你能夠用valurForKey:@"items"
來獲取這個屬性。前面valueForKey:
的key搜索模式中,咱們發現其實KVC使用了一種更靈活的方式來管理容器類。蘋果的官方文檔也推薦咱們實現這些這些特殊的訪問器。
而當對象的屬性是可變的容器時,對於有序的容器,能夠用下面的方法:
1
|
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
|
該方法返回一個可變有序數組,若是調用該方法,KVC的搜索順序以下
insertObject:inAtIndex:
, removeObjectFromAtIndex:
或者 insertAdIndexes
, removeAtIndexes
格式的方法NSKeyValueFastMutableArray2
),那麼給這個代理集合發送NSMutableArray的方法,以insertObject:inAtIndex:
, removeObjectFromAtIndex:
或者 insertAdIndexes
, removeAtIndexes
組合的形式調用。還有兩個可選實現的接口:replaceOnjectAtIndex:withObject: , replaceAtIndexes:with: 。set:
格式的方法,若是找到,那麼發送給代理集合的NSMutableArray
最終都會調用set:
方法。 也就是說,mutableArrayValueForKey:
取出的代理集合修改後,用·set:· 從新賦值回去去。這樣作效率會低不少。因此推薦實現上面的方法。+ (BOOL)accessInstanceVariablesDirectly
,若是返回YES(默認行爲),會按_,,的順序搜索成員變量名,若是找到,那麼發送的NSMutableArray
消息方法直接交給這個成員變量處理。valueForUndefinedKey:
mutableArrayValueForKey:
的適用場景,我在網上找了不少,發現其通常是用在對NSMutableArray
添加Observer上。NSMutableAArray、NSMutableSet、NSMutableDictionary
等集合類型時,你給它添加KVO時,你會發現當你添加或者移除元素時並不能接收到變化。由於KVO的本質是系統監測到某個屬性的內存地址或常量改變時,會添加上- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)
key方法來發送通知,因此一種解決方法是手動調用者兩個方法,可是並不推薦,你永遠沒法像系統同樣真正知道這個元素何時被改變。另外一種即是利用使用mutableArrayValueForKey:
了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
@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{
[_arr addObject:@"1"];
}
-(void)addItemObserver{
[[self mutableArrayValueForKey:@"arr"] addObject:@"1"];
}
-(void)removeItemObserver{
[[self mutableArrayValueForKey:@"arr"] removeLastObject];
}
@end
而後再
:
demo* d = [demo new];
[d addItem];
[d addItemObserver];
[d removeItemObserver];
打印結果
2016-04-18 17:48:22.675 KVCDemo[32647:505864] {
indexes = "[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
1
);
}
2016-04-18 17:48:22.677 KVCDemo[32647:505864] {
indexes = "[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
而對於無序的容器,能夠用下面的方法:
1
|
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
|
該方法返回一個可變的無序數組若是調用該方法,KVC的搜索順序以下
addObjectObject:
, removeObject:
或者 add
, remove
格式的方法NSKeyValueFastMutableSet2
),那麼給這個代理集合發送NSMutableSet的方法,以addObjectObject:
, removeObject:
或者 add
, remove
組合的形式調用。還有兩個可選實現的接口:intersect , set:
。set
: 格式的方法,若是找到,那麼發送給代理集合的NSMutableSet
最終都會調用set:
方法。 也就是說,mutableSetValueForKey
取出的代理集合修改後,用set:
從新賦值回去去。這樣作效率會低不少。因此推薦實現上面的方法。+ (BOOL)accessInstanceVariablesDirectly
,若是返回YES(默認行爲),會按_,,的順序搜索成員變量名,若是找到,那麼發送的NSMutableSet
消息方法直接交給這個成員變量處理。valueForUndefinedKey:
mutableArrayValueForKey
基本一至,一樣,它們也有對應的keyPath版本
1
2
|
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
|
iOS5和OSX10.7之後還有個mutableOrdered版本
1
|
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key
|
這兩種KVC的用法我還不是清楚,目前只能找到用於KVO的例子。若是有讀者能在項目中用到,但願能夠告訴我。
當對NSDictionary對象使用KVC時,valueForKey:
的表現行爲和objectForKey:
同樣。因此使用valueForKeyPath:
用來訪問多層嵌套的字典是比較方便的。
KVC裏面還有兩個關於NSDictionary的方法
1
2
|
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
|
dictionaryWithValuesForKeys:
是指輸入一組key,返回這組key對應的屬性,再組成一個字典。setValuesForKeysWithDictionary
是用來修改Model中對應key的屬性。下面直接用代碼會更直觀一點
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key全部的屬性所有取出來
NSLog(@"%@",dict);
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value來修改Model的屬性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
//打印結果
2016-04-19 11:54:30.846 KVCDemo[6607:198900] {
city = "Shen Zhen";
country = China;
district = "Nan Shan";
province = "Guang Dong";
}
2016-04-19 11:54:30.847 KVCDemo[6607:198900] country:USA province:california city:Los angle
|
打印出來的結果徹底符合預期。
前面咱們對析了KVC是怎麼搜索key的。因此若是明白了key的搜索順序,是能夠本身寫代碼實現KVC的。在考慮到集合和keyPath的狀況下,KVC的實現會比較複雜,咱們只寫代碼實現最普通的取值和設值便可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@interface NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString*)key;
-(id)myValueforKey:(NSString*)key;
@end
@implementation NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString *)key{
if (key == nil || key.length == 0) {
return;
}
if ([value isKindOfClass:[NSNull class]]) {
[self setNilValueForKey:key]; //若是須要徹底自定義,那麼這裏須要寫一個setMyNilValueForKey,可是必要性不是很大,就省略了
return;
}
if (![value isKindOfClass:[NSObject class]]) {
@throw @"must be s NSobject type";
return;
}
NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
[self performSelector:NSSelectorFromString(funcName) withObject:value];
return;
}
unsigned int count;
BOOL flag = false;
Ivar* vars = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i
|
上面就是本身寫代碼實現KVC的部分功能。其中我省略了自定義KVC錯誤方法,省略了部分KVC搜索key的步驟,可是邏輯是很清晰明瞭的,後面的測試也符合預期。固然這只是我本身實現KVC的思路,Apple也許並非這麼作的。
KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
1
|
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
|
這個方法的默認實現是去探索類裏面是否有一個這樣的方法:-(BOOL)validate:error:
若是有這個方法,就調用這個方法來返回,沒有的話就直接返回YES
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{ //在implementation裏面加這個方法,它會驗證是否設了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO; //若是國家是日本,就返回NO,這裏省略了錯誤提示,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //若是沒有重寫-(BOOL)-validate:error:,默認返回Yes
if (result) {
NSLog(@"鍵值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"鍵值不匹配"); //不能設爲日本,基他國家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印結果
2016-04-20 14:55:12.055 KVCDemo[867:58871] 鍵值不匹配
2016-04-20 14:55:12.056 KVCDemo[867:58871] country:China
|
如上面的代碼,當開發者須要驗證能不能用KVC設定某個值時,能夠調用validateValue: forKey:
這個方法來驗證,若是這個類的開發者實現了-(BOOL)validate:error:
這個方法,那麼KVC就會直接調用這個方法來返回,若是沒有,就直接返回YES,注意,KVC在設值時不會主動去作驗證,須要開發者手動去驗證。因此即便你在類裏面寫了驗證方法,可是KVC由於不會去主動驗證,因此仍是可以設值成功。
KVC在iOS開發中是毫不可少的利器,這種基於運行時的編程方式極大地提升了靈活性,簡化了代碼,甚至實現不少難以想像的功能,KVC也是許多iOS開發黑魔法的基礎。下面我來列舉iOS開發中KVC的使用場景
利用KVC動態的取值和設值是最基本的用途了。相信每個iOS開發者都能熟練掌握,
對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的,請參考本文前面的Dog類的例子。
這是KVC強大做用的又一次體現,請參考我寫的iOS開發技巧系列—打造強大的BaseMod系列文章,裏面
充分地運用了KVC和Objc的runtime組合的技巧,只用了短短數行代碼就是完成了不少功能。
這也是iOS開發中必不可少的小技巧。衆所周知不少UI控件都由不少內部UI控件組合而成的,可是Apple度沒有提供這訪問這些空間的API,這樣咱們就沒法正常地訪問和修改這些控件的樣式。而KVC在大多數狀況可下能夠解決這個問題。最經常使用的就是個性化UITextField中的placeHolderText了。
下面演示若是修改placeHolder的文字樣式。這裏的關鍵點是若是獲取你要修改的樣式的屬性名,也就是key或者keyPath名。
通常狀況下能夠運用runtime來獲取Apple不想開放的屬性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
let count:UnsafeMutablePointer = UnsafeMutablePointer()
var properties = class_copyIvarList(UITextField.self, count)
while properties.memory.debugDescription != "0x0000000000000000"{
let t = ivar_getName(properties.memory)
let n = NSString(CString: t, encoding: NSUTF8StringEncoding)
print(n) //打印出全部屬性,這裏我用了Swift語言
properties = properties.successor()
}
//上面省略了部分屬性
Optional(_disabledBackgroundView)
Optional(_systemBackgroundView)
Optional(_floatingContentView)
Optional(_contentBackdropView)
Optional(_fieldEditorBackgroundView)
Optional(_fieldEditorEffectView)
Optional(_displayLabel)
Optional(_placeholderLabel) //這個正是我想要修改的屬性。
Optional(_dictationLabel)
Optional(_suffixLabel)
Optional(_prefixLabel)
Optional(_iconView)
//下面省略了部分屬性
|
能夠從裏面看到其餘還有不少東西能夠修改,運用KVC設值能夠得到本身想要的效果。
Apple對KVC的valueForKey:
方法做了一些特殊的實現,好比說NSArray和NSSet這樣的容器類就實現了這些方法。因此能夠用KVC很方便地操做集合
當對容器類使用KVC時,valueForKey:
將會被傳遞給容器中的每個對象,而不是容器自己進行操做。結果會被添加進返回的容器中,這樣,開發者能夠很方便的操做集合來返回另外一個集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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);
}
打印結果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7
|
方法capitalizedString
被傳遞到NSArray中的每一項,這樣,NSArray的每一員都會執行capitalizedString
並返回一個包含結果的新的NSArray。從打印結果能夠看出,全部String都成功以轉成了大寫。
一樣若是要執行多個方法也能夠用valueForKeyPath:
方法。它先會對每個成員調用 capitalizedString
方法,而後再調用length,由於lenth方法返回是一個數字,因此返回結果以NSNumber的形式保存在新數組裏。
KVC同時還提供了很複雜的函數,主要有下面這些
①簡單集合運算符
簡單集合運算符共有@avg, @count , @max , @min ,@sum5
種,都表示啥不用我說了吧, 目前還不支持自定義。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@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);
打印結果
2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.000000
|
②對象運算符
比集合運算符稍微複雜,能以數組的方式返回指定的內容,一共有兩種:@distinctUnionOfObjects
@unionOfObjects
它們的返回值都是NSArray,區別是前者返回的元素都是惟一的,是去重之後的結果;後者返回的元素是全集。
用法以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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);
}
2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
|
前者會將重複的價格去除後返回全部價格,後者直接返回全部的圖書價格。(由於只返回價格,沒有返回圖書,感受用處不大。)
③Array和Set操做符
這種狀況更復雜了,說的是集合中包含集合的狀況,咱們執行了以下的一段代碼:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets@distinctUnionOfArrays:
該操做會返回一個數組,這個數組包含不一樣的對象,不一樣的對象是在從關鍵路徑到操做器右邊的被指定的屬性裏@unionOfArrays
該操做會返回一個數組,這個數組包含的對象是在從關鍵路徑到操做器右邊的被指定的屬性裏和@distinctUnionOfArrays不同,重複的對象不會被移除@distinctUnionOfSets
和@distinctUnionOfArrays
相似。由於Set自己就不支持重複。
你沒看錯,KVO是基於KVC實現的。那麼是怎麼用KVC實現KVO的呢,請期待下章。
本文全方位介紹了KVC的原理和各類用法。相信讀者看完後對會KVC會有更徹底的理解,也會在項目裏更好的運用KVC。其實這裏面全部的東西在官方文檔裏都有詳細的講解說明。只不過全是英文的,我也看過幾遍,可是英語很差會看得很吃力,好比官方在介紹@distinctUnionOfArrays時的那句話我想了很很久也不是很明白,並且官方的示例代碼也作得不夠好,因此很難找出某些功能的適用場景。但我仍是推薦各位開發者可以學好英語去看官方文檔。再結合StackOverFlow和Google。真的能夠解決絕大多數開發中碰到的難題了。這篇文章就到這裏,下篇我向你們介紹KVO。
感謝大神分享