KVC(Key-value coding)鍵值編碼,單看這個名字可能不太好理解。其實翻譯一下就很簡單了,就是指iOS的開發中,可以允許開發者通過Key名直接訪問對象的屬性,或者給對象的屬性賦值。而不需要調用明確的存取方法。這樣就可以在運行時動態地訪問和修改對象的屬性。而不是在編譯時確定,這也是iOS開發中的黑魔法之一。很多高級的iOS開發技巧都是基於KVC實現的。目前網上關於KVC的文章在非常多,有的只是簡單地說了下用法,有的講得深入但是在使用場景和最佳實踐沒有說明,我寫下這遍文章就是給大家詳解一個最完整最詳細的KVC。
KVC在iOS中的定義
無論是Swift
還是Objective-C
,KVC的定義都是對NSObject
的擴展來實現的(Objective-C
中有個顯式的NSKeyValueCoding
類別名,而Swift沒有,也不需要)。所以對於所有繼承了NSObject
的類型,也就是幾乎所有的Objective-C
對象都能使用KVC
(一些純Swift
類和結構體是不支持KVC的),下面是KVC最爲重要的四個方法
注意,這裏的講解的是NSOBject的kvc。因爲還有字典的kvc方法。那個kvc內部跟這個不同。詳細請看--字典
@interface NSObject(NSKeyValueCoding)
- (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來設值
@end
當然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些
+ (BOOL)accessInstanceVariablesDirectly;
//默認返回YES,表示如果沒有找到Set<Key>方法的話,會按照_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<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
上面的這些方法在碰到特殊情況或者有特殊需求還是會用到的,所以也是可以c瞭解一下。後面的代碼示例會有講到其中的一些方法。
同時蘋果對一些容器類比如NSArray
或者NSSet
等,KVC有着特殊的實現。建議有基礎的或者英文好的開發者直接去看蘋果的官方文檔,相信你會對KVC的理解更上一個臺階。
KVC是怎麼尋找Key的
證明第一步,走set和_set方法:
@interface Person : NSObject
@end
#import "Person.h"
@implementation Person
- (void)setAge:(NSString *)age
{
NSLog(@"_setAge");
}
//- (void)_setAge:(NSString *)age
//{
// NSLog(@"_setAge");
//}
@end
self.person = [[Person alloc] init];
// 這裏解釋一下,這裏不添加kvc也是可以的,道理都是一樣的,添加了kvc,只是這個person不再是person了,而是NSNotifying_Person這個新類,這裏裏面走的setkey和_setkey方法都是走的新類的,如果新類沒有就走父類的,也就是person的。
NSKeyValueObservingOptions optips = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:optips context:@"sdf"];
[self.person setValue:@10 forKey:@"age"];
// 同時也證明了用了kvc的內部原理中調用了kvo。只是添不添加監聽,訪問的類是不同的。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"監聽到%@的%@屬性發生了變化-%@ -%@",object, keyPath, change, context);
}
結果:
當實現setAge時 就會打印:
2018-07-18 23:18:23.984 newxc[5009:1640392] setAge
當實現_setAge時,就會打印
2018-07-18 23:18:23.984 newxc[5009:1640392] _setAge
第二步:
不實現setAge 和_setAge
#import "Person.h"
@implementation Person
+ (BOOL)accessInstanceVariablesDirectly
{
return NO; // 不允許訪問成員變量
}
結果:
會報錯:
2018-07-18 23:20:35.584 newxc[5060:1644779] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Person 0x60800002fe40> valueForUndefinedKey:]: this class is not key value coding-compliant for the key age.'
*** First throw call stack:
(
0 CoreFoundation 0x000000010aeceb0b __exceptionPreprocess + 171
1 libobjc.A.dylib 0x000000010a933141 objc_exception_throw + 48
2 CoreFoundation 0x000000010aecea59 -[NSException raise] + 9
3 Foundation 0x000000010a511731 -[NSObject(NSKeyValueCoding) valueForUndefinedKey:] + 226
4 Foundation 0x000000010a440e1d -[NSObject(NSKeyValueCoding) valueForKey:] + 284
5 Foundation 0x000000010a43bb44 NSKeyValueWillChangeBySetting + 60
6 Foundation 0x000000010a436057 NSKeyValueWillChange + 414
7 Foundation 0x000000010a40c603 -[NSObject(NSKeyValueObserverNotification) willChangeValueForKey:] + 477
8 Foundation 0x000000010a51704b _NSSetValueAndNotifyForUndefinedKey + 45
9 Foundation 0x000000010a448e8b -[NSObject(NSKeyValueCoding) setValue:forKey:] + 292
10 newxc 0x000000010a36316c -[ViewController viewDidLoad] + 476
11 UIKit 0x000000010b494cca -[UIViewController loadViewIfRequired] + 1235
12 UIKit 0x000000010b49510a -[UIViewController view] + 27
13 UIKit 0x000000010b35d63a -[UIWindow addRootViewControllerViewIfPossible] + 65
14 UIKit 0x000000010b35dd20 -[UIWindow _setHidden:forced:] + 294
15 UIKit 0x000000010b370b6e -[UIWindow makeKeyAndVisible] + 42
16 UIKit 0x000000010b2ea31f -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4346
17 UIKit 0x000000010b2f0584 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1709
18 UIKit 0x000000010b2ed793 -[UIApplication workspaceDidEndTransaction:] + 182
19 FrontBoardServices 0x000000010e54b5f6 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
20 FrontBoardServices 0x000000010e54b46d -[FBSSerialQueue _performNext] + 186
21 FrontBoardServices 0x000000010e54b7f6 -[FBSSerialQueue _performNextFromRunLoopSource] + 45
22 CoreFoundation 0x000000010ae74c01 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
23 CoreFoundation 0x000000010ae5a0cf __CFRunLoopDoSources0 + 527
24 CoreFoundation 0x000000010ae595ff __CFRunLoopRun + 911
25 CoreFoundation 0x000000010ae59016 CFRunLoopRunSpecific + 406
26 UIKit 0x000000010b2ec02f -[UIApplication _run] + 468
27 UIKit 0x000000010b2f20d4 UIApplicationMain + 159
28 newxc 0x000000010a36381f main + 111
29 libdyld.dylib 0x000000010ddd665d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)
但是 如果返回的是YES
+ (BOOL)accessInstanceVariablesDirectly
{
return YES; // 允許訪問成員變量。默認返回值YES
}
這裏需要注意一下,這裏依然會監聽,可能是內部實現了kvo必須實現的那兩個方法。
2018-07-18 23:22:18.703 newxc[5079:1647618] 監聽到<Person: 0x608000023620>的age屬性發生了變化-{
kind = 1;
new = 10;
old = 0;
} -sdf
所以,綜上所述,kvc內部就是實現了調用kvo的兩個監聽的方法。
// [person willChangeValueForKey:@"age"];
// person->age = 10;
// [person didChangeValueForKey:@"age"];
這個賦值的屬性請看下面
上面是kvo實現邏輯的驗證過程。
下面講valueForkey 取值的內部原理。
看第一個驗證,關於方法取值
@interface Person : NSObject
@end
#import "Person.h"
@implementation Person
- (int)getAge // 這個方法會首先尋找。1
{
return 11;
}
- (int)age // 上面沒有實現,會找這個方法。2
{
return 12;
}
- (int)isAge // 上面都沒有實現,會找這個方法。3
{
return 13;
}
- (int)_Age // 上面都沒有實現,會找這個方法。4
{
return 14;
}
@end
調用:
self.person = [[Person alloc] init];
NSLog(@"personage:%@",[self.person valueForKey:@"age"]);
接下來,我們給person增加一些成員變量。賦值操作,看一下結果
@interface Person : NSObject
{
@public
int _age;
int _isAge;
int age;
int isAge;
}
@end
#import "Person.h"
@implementation Person
@end
這裏進行調用
self.person = [[Person alloc] init];
self.person->_age = 11; // 會按照這個順序 有這個屬性,那麼就直接取值
self.person->_isAge = 12; // 如果沒有上面的值,會看有沒有這個值,有就直接取值
self.person->age = 13; // 如果還沒有上面的那些值,就看有沒有這個,有就直接取
self.person->_isAge = 14; // 如果還沒有,這個有,則取值,如果都沒有,則報錯。
NSLog(@"personage:%@",[self.person valueForKey:@"age"]);
這個是forKey和ForKeyPath的解釋:
#import <Foundation/Foundation.h>
@interface Cat : NSObject
@property (nonatomic, strong) NSString *wight;
@end
@interface Person : NSObject
@property (nonatomic, strong) NSString *age;
@property (nonatomic, strong) Cat *cat;
@end
[self.person setValue:@10 forKey:@"age"]; // key只能訪問簡單的屬性
[self.person setValue:@10 forKeyPath:@"cat.wight"]; // 這個可以一層一層訪問
2018-07-19 22:09:00.866 newxc[5865:1866379] personage:11 // 這個是實現了第一個成員變量
KVC如何處理異常
KVC中最常見的異常就是不小心使用了錯誤的key
,或者在設值中不小心傳遞了nil
的值,KVC中有專門的方法來處理這些異常。
通常在用KVC操作Model時,拋出異常的那兩個方法是需要重寫的。雖然一般很小出現傳遞了錯誤的Key值這種情況,但是如果不小心出現了,直接拋出異常讓APP崩潰顯然是不合理的。一般在這裏直接讓這個key
打印出來即可,或者有些特殊情況需要特殊處理。通常情況下,KVC不允許你要在調用setValue:屬性值 forKey:@”name“
(或者keyPath)時對非對象傳遞一個nil
的值。很簡單,因爲值類型是不能爲nil
的。如果你不小心傳了,KVC會調用setNilValueForKey:
方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。
[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<People 0x100200080> setNilValueForKey]: could not set nil as the value for the key age.' // 調用setNilValueForKey拋出異常
如果重寫setNilValueForKey:
就沒問題了
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能將%@設成nil",key);
}
@end
//打印出
2016-04-17 16:19:55.298 KVCDemo[1304:92472] 不能將age設成nil
KVC處理非對象和自定義對象
不是每一個方法都返回對象,但是valueForKey:
總是返回一個id對象,如果原本的變量類型是值類型或者結構體,返回值會封裝成NSNumber
或者NSValue
對象。這兩個類會處理從數字,布爾值到指針和結構體任何類型。然後開以者需要手動轉換成原來的類型。儘管valueForKey:
會自動將值類型封裝成對象,但是setValue:forKey:
卻不行。你必須手動將值類型轉換成NSNumber
或者NSValue
類型,才能傳遞過去。
對於自定義對象,KVC也會正確地設值和取值。因爲傳遞進去和取出來的都是id
類型,所以需要開發者自己擔保類型的正確性,運行時Objective-C
在發送消息的會檢查類型,如果錯誤會直接拋出異常。
Address* add2 = [Address new];
add2.country = @"England";
[people1 setValue:add2 forKey:@"address"];
NSString* country1 = people1.address.country;
NSString * country2 = [people1 valueForKeyPath:@"address.country"];
NSLog(@"country1:%@ country2:%@",country1,country2);
//打印結果
2016-04-17 16:29:36.349 KVCDemo[1346:95910] country1:England country2:England
KVC與容器類
對象的屬性可以是一對一的,也可以是一對多的。一對多的屬性要麼是有序的(數組),要麼是無序的(集合)。
不可變的有序容器屬性(NSArray
)和無序容器屬性(NSSet
)一般可以使用valueForKey:
來獲取。比如有一個叫items
的NSArray
屬性,你可以用valurForKey:@"items"
來獲取這個屬性。前面valueForKey:
的key
搜索模式中,我們發現其實KVC使用了一種更靈活的方式來管理容器類。蘋果的官方文檔也推薦我們實現這些這些特殊的訪問器。
而當對象的屬性是可變的容器時,對於有序的容器,可以用下面的方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
該方法返回一個可變有序數組,如果調用該方法,KVC的搜索順序如下
- 搜索
insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AdIndexes
,remove<Key>AtIndexes
格式的方法
如果至少找到一個insert
方法和一個remove
方法,那麼同樣返回一個可以響應NSMutableArray
所有方法代理集合(類名是NSKeyValueFastMutableArray2
),那麼給這個代理集合發送NSMutableArray
的方法,以insertObject:in<Key>AtIndex:
,removeObjectFrom<Key>AtIndex:
或者insert<Key>AdIndexes
,remove<Key>AtIndexes
組合的形式調用。還有兩個可選實現的接口:replaceOnjectAtIndex:withObject:
,replace<Key>AtIndexes:with<Key>:
。 - 如果上步的方法沒有找到,則搜索
set<Key>:
格式的方法,如果找到,那麼發送給代理集合的NSMutableArray
最終都會調用set<Key>:
方法。 也就是說,mutableArrayValueForKey:
取出的代理集合修改後,用set<Key>:
重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。 - 如果上一步的方法還還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES(默認行爲),會按_<key>
,<key>
,的順序搜索成員變量名,如果找到,那麼發送的NSMutableArray
消息方法直接交給這個成員變量處理。 - 如果還是找不到,則調用
valueForUndefinedKey:
。 - 關於
mutableArrayValueForKey:
的適用場景,我在網上找了很多,發現其一般是用在對NSMutableArray
添加Observer上。如果對象屬性是個NSMutableArray、NSMutableSet、NSMutableDictionary
等集合類型時,你給它添加KVO時,你會發現當你添加或者移除元素時並不能接收到變化。因爲KVO的本質是系統監測到某個屬性的內存地址或常量改變時,會添加上- (void)willChangeValueForKey:(NSString *)key
和- (void)didChangeValueForKey:(NSString *)key
方法來發送通知,所以一種解決方法是手動調用者兩個方法,但是並不推薦,你永遠無法像系統一樣真正知道這個元素什麼時候被改變。另一種便是利用使用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<NSString *,id> *)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 = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
1
);
}
2016-04-18 17:48:22.677 KVCDemo[32647:505864] {
indexes = "<_NSCachedIndexSet: 0x100202c70>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 3;
old = (
1
);
}
從上面的代碼可以看出,當只是普通地調用[_arr addObject:@"1"]
時,Observer
並不會回調,只有[[self mutableArrayValueForKey:@"arr"] addObject:@"1"]
;這樣寫時才能正確地觸發KVO。打印出來的數據中,可以看出這次操作的詳情,kind
可能是指操作方法(我還不是很確認),old
和new
並不是成對出現的,當加添新數據時是new
,刪除數據時是old
而對於無序的容器,可以用下面的方法:
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
該方法返回一個可變的無序數組如果調用該方法,KVC的搜索順序如下
- 搜索
addObject<Key>Object:
,remove<Key>Object:
或者add<Key>
,remove<Key>
格式的方法
如果至少找到一個insert
方法和一個remove
方法,那麼同樣返回一個可以響應NSMutableSet
所有方法代理集合(類名是NSKeyValueFastMutableSet2
),那麼給這個代理集合發送NSMutableSet
的方法,以addObject<Key>Object:
,remove<Key>Object:
或者add<Key>
,remove<Key>
組合的形式調用。還有兩個可選實現的接口:intersect<Key> , set<Key>:
。 - 如果
receiver
是ManagedObject
,那麼就不會繼續搜索。 - 如果上一步的方法沒有找到,則搜索
set<Key>
: 格式的方法,如果找到,那麼發送給代理集合的NSMutableSet
最終都會調用set<Key>:
方法。 也就是說,mutableSetValueForKey
取出的代理集合修改後,用set<Key>:
重新賦值回去去。這樣做效率會低很多。所以推薦實現上面的方法。 - 如果上一步的方法還沒有找到,再檢查類方法
+ (BOOL)accessInstanceVariablesDirectly
,如果返回YES
(默認行爲),會按_<key>
,<key>
的順序搜索成員變量名,如果找到,那麼發送的NSMutableSet
消息方法直接交給這個成員變量處理。 - 如果還是找不到,調用
valueForUndefinedKey:
可見,除了檢查receiver
是ManagedObject
以外,其搜索順序和mutableArrayValueForKey
基本一至,
同樣,它們也有對應的keyPath
版本
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
iOS5和OSX10.7以後還有個mutableOrdered
版本
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key
這兩種KVC的用法我還不是清楚,目前只能找到用於KVO的例子。如果有讀者能在項目中用到,希望可以告訴我。
KVC和字典
當對NSDictionary
對象使用KVC時,valueForKey:
的表現行爲和objectForKey:
一樣。所以使用valueForKeyPath:
用來訪問多層嵌套的字典是比較方便的。
KVC裏面還有兩個關於NSDictionary的方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
dictionaryWithValuesForKeys:
是指輸入一組key
,返回這組key
對應的屬性,再組成一個字典。setValuesForKeysWithDictionary
是用來修改Model中對應key
的屬性。下面直接用代碼會更直觀一點
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
NSArray* arr = @[@"country",@"province",@"city",@"district"];
NSDictionary* dict = [add dictionaryWithValuesForKeys:arr]; //把對應key所有的屬性全部取出來
NSLog(@"%@",dict);
NSDictionary* modifyDict = @{@"country":@"USA",@"province":@"california",@"city":@"Los angle"};
[add setValuesForKeysWithDictionary:modifyDict]; //用key Value來修改Model的屬性
NSLog(@"country:%@ province:%@ city:%@",add.country,add.province,add.city);
//打印結果
2016-04-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的內部實現機制
前面我們對析了KVC是怎麼搜索key
的。所以如果明白了key
的搜索順序,是可以自己寫代碼實現KVC的。在考慮到集合和keyPath
的情況下,KVC的實現會比較複雜,我們只寫代碼實現最普通的取值和設值即可。
@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) { //key名要合法
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)]) { //默認優先調用set方法
[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<count; i++) {
Ivar var = vars[i];
NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
flag = true;
object_setIvar(self, var, value);
break;
}
if ([keyName isEqualToString:key]) {
flag = true;
object_setIvar(self, var, value);
break;
}
}
if (!flag) {
[self setValue:value forUndefinedKey:key];//如果需要完全自定義,那麼這裏需要寫一個self setMyValue:value forUndefinedKey:key,但是必要性不是很大,就省略了
}
}
-(id)myValueforKey:(NSString *)key{
if (key == nil || key.length == 0) {
return [NSNull new]; //其實不能這麼寫的
}
//這裏爲了更方便,我就不做相關集合的方法查詢了
NSString* funcName = [NSString stringWithFormat:@"gett%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
return [self performSelector:NSSelectorFromString(funcName)];
}
unsigned int count;
BOOL flag = false;
Ivar* vars = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i<count; i++) {
Ivar var = vars[i];
NSString* keyName = [[NSString stringWithCString:ivar_getName(var) encoding:NSUTF8StringEncoding] substringFromIndex:1];
if ([keyName isEqualToString:[NSString stringWithFormat:@"_%@",key]]) {
flag = true;
return object_getIvar(self, var);
break;
}
if ([keyName isEqualToString:key]) {
flag = true;
return object_getIvar(self, var);
break;
}
}
if (!flag) {
[self valueForUndefinedKey:key];//如果需要完全自定義,那麼這裏需要寫一個self myValueForUndefinedKey,但是必要性不是很大,就省略了
}
return [NSNull new]; //其實不能這麼寫的
}
@end
Address* add = [Address new];
add.country = @"China";
add.province = @"Guang Dong";
add.city = @"Shen Zhen";
add.district = @"Nan Shan";
[add setMyValue:nil forKey:@"area"]; //測試設置 nil value
[add setMyValue:@"UK" forKey:@"country"];
[add setMyValue:@"South" forKey:@"area"];
[add setMyValue:@"300169" forKey:@"postCode"];
NSLog(@"country:%@ province:%@ city:%@ postCode:%@",add.country,add.province,add.city,add._postCode);
NSString* postCode = [add myValueforKey:@"postCode"];
NSString* country = [add myValueforKey:@"country"];
NSLog(@"country:%@ postCode: %@",country,postCode);
//打印結果:
2016-04-19 14:29:39.498 KVCDemo[7273:275129] country:UK province:South city:Shen Zhen postCode:300169
2016-04-19 14:29:39.499 KVCDemo[7273:275129] country:UK postCode: 300169
上面就是自己寫代碼實現KVC的部分功能。其中我省略了自定義KVC錯誤方法,省略了部分KVC搜索key
的步驟,但是邏輯是很清晰明瞭的,後面的測試也符合預期。當然這只是我自己實現KVC的思路,Apple也許並不是這麼做的。
KVC的正確性驗證
KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
這個方法的默認實現是去探索類裏面是否有一個這樣的方法:-(BOOL)validate<Key>:error:
如果有這個方法,就調用這個方法來返回,沒有的話就直接返回YES
@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<Key>: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<Key>:error:
這個方法,那麼KVC就會直接調用這個方法來返回,如果沒有,就直接返回YES
,注意,KVC在設值時不會主動去做驗證,需要開發者手動去驗證。所以即使你在類裏面寫了驗證方法,但是KVC因爲不會去主動驗證,所以還是能夠設值成功。
KVC的使用
KVC在iOS開發中是絕不可少的利器,這種基於運行時的編程方式極大地提高了靈活性,簡化了代碼,甚至實現很多難以想像的功能,KVC也是許多iOS開發黑魔法的基礎。下面我來列舉iOS開發中KVC的使用場景
動態地取值和設值
利用KVC動態的取值和設值是最基本的用途了。相信每一個iOS開發者都能熟練掌握,
用KVC來訪問和修改私有變量
對於類裏的私有屬性,Objective-C是無法直接訪問的,但是KVC是可以的,請參考本文前面的Dog類的例子。
Model和字典轉換
這是KVC強大作用的又一次體現,請參考我寫的iOS開發技巧系列---打造強大的BaseMod系列文章,裏面
充分地運用了KVC和Objc的runtime
組合的技巧,只用了短短數行代碼就是完成了很多功能。
修改一些控件的內部屬性
這也是iOS開發中必不可少的小技巧。衆所周知很多UI控件都由很多內部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API,這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數情況可下可以解決這個問題。最常用的就是個性化UITextField中的placeHolderText了。下面演示如果修改placeHolder的文字樣式。這裏的關鍵點是如果獲取你要修改的樣式的屬性名,也就是key或者keyPath名。
修改placeHolder的樣式
一般情況下可以運用runtime
來獲取Apple不想開放的屬性名
let count:UnsafeMutablePointer<UInt32> = UnsafeMutablePointer<UInt32>()
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實現高階消息傳遞
當對容器類使用KVC時,valueForKey:
將會被傳遞給容器中的每一個對象,而不是容器本身進行操作。結果會被添加進返回的容器中,這樣,開發者可以很方便的操作集合來返回另一個集合。
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中的函數操作集合
KVC同時還提供了很複雜的函數,主要有下面這些
①簡單集合運算符
簡單集合運算符共有@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);
打印結果
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,區別是前者返回的元素都是唯一的,是去重以後的結果;後者返回的元素是全集。
用法如下:
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
本身就不支持重複。
1:通過kvc修改屬性會觸發kvo麼?
會觸發
2:kvc的賦值和取值過程是怎樣的?原理是什麼?
請看上面說明