KVC(鍵值編碼),即 Key-Value Coding,一個非正式的 Protocol,使用字符串(鍵)訪問一個對象實例變量的機制。而不是通過調用 Setter、Getter 方法等顯式的存取方式去訪問。
KVC
KVC有兩種讀取方式,一種通過key讀取,一種通過keypath讀取.
- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable ObjectType)valueForKey:(NSString *)key;
/* Key-path-taking variants of like-named methods. The default implementation of each parses the key path enough to determine whether or not it has more than one component (key path components are separated by periods). If so, -valueForKey: is invoked with the first key path component as the argument, and the method being invoked is invoked recursively on the result, with the remainder of the key path passed as an argument. If not, the like-named non-key-path-taking method is invoked.
*/
- (nullable id)valueForKeyPath:(NSString *)keyPath;
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
簡單對比一下兩種取值方式的差別,代碼如下:
NSDictionary *dict = @{@"name":@"FlyElephant",
@"address":@{
@"provice":@"a1",
@"detail":@{
@"floor":@"10",
}
}
};
NSDictionary *address = [dict valueForKey:@"address"];
NSDictionary *detail = [address valueForKey:@"detail"];
NSLog(@"%@",detail);
NSDictionary *detail2 = [dict valueForKeyPath:@"address.detail"];
NSLog(@"%@",detail2);
取值結果一致,valueForKey只能去最上層的結果,對下層嵌套的數據無法獲取。valueForKeyPath可以進行嵌套取值,層次較深,取值較方便。
{
floor = 10;
}
{
floor = 10;
}
KVC 尋找key
setValue:forKey:尋找過程
- 調用 set<Key>: 或者 _set<Key> 設置方法,如果方法存在,結束查找。
- 檢查 + (BOOL)accessInstanceVariablesDirectly 方法是否返回 YES。該方法默認返回 YES,繼續查找。如果重寫設置為 NO,執行 - (void)setValue:forUndefinedKey: 方法,默認是拋出異常,不推薦設置為 NO。
- 按照 _<key>,_is<Key>,<key> 和 is<Key> 的順序在類接口定義和實現處查找實例變量,再賦值。如果上述方法和實例變量都不存在,就執行 - (void)setValue:forUndefinedKey:。
valueForKey:
- 首先按get<Key> <key> is<Key>的順序方法查找getter方法,找到的話會直接調用。如果是BOOL或者int等值類型, 會做NSNumber轉換,如果不存在,繼續查找。
- 如果countOf<Key>和另外兩個方法中的一個被找到,那么就會返回一個可以響應NSArray所有方法的代理集合,它是NSKeyValueArray,是NSArray的子類,這個代理集合將擁有以上方法的組合,還有一個可選的get<Key>: range:方法;
countOf<Key> & objectIn<Key>AtIndex & <Key>AtIndex
所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標準命名方法,包括方法簽名。
- 如果上面的方法沒有找到,那么會查找:
countOf<Key> & enumeratorOf<Key> & memberOf<Key>
以上三種格式的方法,如果這三個方法都找到,那么就返回一個可以響應NSSet所有方法的代理集合,這個代理集合將擁有以上三種方法。
- 如果還沒有找到,再檢查類方法:
+ (BOOL)accessInstanceVariablesDirectly
如果返回YES(默認行為),那么和先前的設值一樣,會按_Key,_isKey,Key,isKey的順序搜索成員變量名,這里不推薦這么做,因為這樣直接訪問實例變量破壞了封裝性,使代碼更脆弱;返回NO,繼續執行;
- 調用valueForUndefinedKey
KVC 的應用
getter / setter
非KVC設值:
//賦值
Account *account = [[Account alloc] init];
account.userName = @"FlyElephant";
Address *address = [[Address alloc] init];
address.province = @"北京";
account.address = address;
//取值
NSString *userName = account.userName;
NSString *province = account.address.province;
NSLog(@"%@---%@",userName,province);
KVC 設置值:
// 賦值
Account *account = [[Account alloc] init];
[account setValue:@"FlyElephant" forKey:@"userName"];
Address *address = [[Address alloc] init];
[account setValue:address forKey:@"address"];
[account setValue:@"北京" forKeyPath:@"address.province"];
// 取值
NSString *userName = [account valueForKey:@"userName"];
NSString *province = [account valueForKeyPath:@"address.province"];
NSLog(@"%@---%@",userName,province);
JSON轉Model
JSON轉Model通過runtime獲取類的所有屬性,然后對屬性進行賦值,開源框架YYModel非常值得學習。
NSValue *value = [self valueForKey:NSStringFromSelector(propertyMeta->_getter)];
if (value) {
[one setValue:value forKey:propertyMeta->_name];
}
修改系統控件隱藏屬性
UIPageControl頭文件代碼如下:
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIPageControl : UIControl
@property(nonatomic) NSInteger numberOfPages; // default is 0
@property(nonatomic) NSInteger currentPage; // default is 0. value pinned to 0..numberOfPages-1
@property(nonatomic) BOOL hidesForSinglePage; // hide the the indicator if there is only one page. default is NO
@property(nonatomic) BOOL defersCurrentPageDisplay; // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO
- (void)updateCurrentPageDisplay; // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately
- (CGSize)sizeForNumberOfPages:(NSInteger)pageCount; // returns minimum size required to display dots for given page count. can be used to size control if page count could change
@property(nullable, nonatomic,strong) UIColor *pageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic,strong) UIColor *currentPageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
@end
獲取UIPageControl類的隱藏變量,方法代碼如下:
- (NSArray *)getIvarList:(Class)cls {
NSMutableArray *arr = [NSMutableArray array];
unsigned int outCount;
Ivar *ivars = class_copyIvarList(cls, &outCount);
for (NSInteger i=0; i<outCount; ++i) {
Ivar ivar = ivars[i];
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivar)];
NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
NSString *str = [name stringByAppendingFormat:@"---%@",type];
[arr addObject:str];
}
free(ivars);
return [arr copy];
}
測試代碼:
UIPageControl *pageControl = [[UIPageControl alloc] init];
NSArray *array = [self getIvarList:[pageControl class]];
NSLog(@"%@",array);
測試結果:
"_lastUserInterfaceIdiom---q",
"_indicators---@\"NSMutableArray\"",
"_currentPage---q",
"_displayedPage---q",
"_pageControlFlags---{?=\"hideForSinglePage\"b1\"defersCurrentPageDisplay\"b1}",
"_currentPageImage---@\"UIImage\"",
"_pageImage---@\"UIImage\"",
"_currentPageImages---@\"NSMutableArray\"",
"_pageImages---@\"NSMutableArray\"",
"_backgroundVisualEffectView---@\"UIVisualEffectView\"",
"_currentPageIndicatorTintColor---@\"UIColor\"",
"_pageIndicatorTintColor---@\"UIColor\"",
"_legibilitySettings---@\"_UILegibilitySettings\"",
"_numberOfPages---q"
可以利用KVC設置_currentPageImage和_pageImage.
Storyboard
在Storyboard中,也可以使用KVC,設置控件的屬性,如圖所示:
- 不建議使用,如果團隊開發,其他成員經手代碼很容易忽略這塊的代碼,最好都在代碼中進行設置;
KVO
KVO(Key Value Observer)鍵值觀察者,是觀察者設計模式的一種。KVO的觀察者,監測被觀察者的某屬性是否發生變化,若被監測的屬性發生的更改,會觸發觀察者的一個方法。使用KVO需要注冊監聽器,也需要刪除監聽器。
KVO 基本使用
- 監聽和移除方法
/* Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- NSKeyValueObservingOptions枚舉:
/* Options for use with -addObserver:forKeyPath:options:context: and -addObserver:toObjectsAtIndexes:forKeyPath:options:context:.
*/
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
/* Whether the change dictionaries sent in notifications should contain NSKeyValueChangeNewKey and NSKeyValueChangeOldKey entries, respectively.
*/
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
/* Whether a notification should be sent to the observer immediately, before the observer registration method even returns. The change dictionary in the notification will always contain an NSKeyValueChangeNewKey entry if NSKeyValueObservingOptionNew is also specified but will never contain an NSKeyValueChangeOldKey entry. (In an initial notification the current value of the observed property may be old, but it's new to the observer.) You can use this option instead of explicitly invoking, at the same time, code that is also invoked by the observer's -observeValueForKeyPath:ofObject:change:context: method. When this option is used with -addObserver:toObjectsAtIndexes:forKeyPath:options:context: a notification will be sent for each indexed object to which the observer is being added.
*/
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
/* Whether separate notifications should be sent to the observer before and after each change, instead of a single notification after the change. The change dictionary in a notification sent before a change always contains an NSKeyValueChangeNotificationIsPriorKey entry whose value is [NSNumber numberWithBool:YES], but never contains an NSKeyValueChangeNewKey entry. You can use this option when the observer's own KVO-compliance requires it to invoke one of the -willChange... methods for one of its own properties, and the value of that property depends on the value of the observed object's property. (In that situation it's too late to easily invoke -willChange... properly in response to receiving an -observeValueForKeyPath:ofObject:change:context: message after the change.)
When this option is specified, the change dictionary in a notification sent after a change contains the same entries that it would contain if this option were not specified, except for ordered unique to-many relationships represented by NSOrderedSets. For those, for NSKeyValueChangeInsertion and NSKeyValueChangeReplacement changes, the change dictionary for a will-change notification contains an NSKeyValueChangeIndexesKey (and NSKeyValueChangeOldKey in the case of Replacement where the NSKeyValueObservingOptionOld option was specified at registration time) which give the indexes (and objects) which *may* be changed by the operation. The second notification, after the change, contains entries reporting what did actually change. For NSKeyValueChangeRemoval changes, removals by index are precise.
*/
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
- 添加和監聽觀察者:
self.account = [[Account alloc] init];
self.account.userName = @"Fly";
[self.account addObserver:self forKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"userName"]) {
NSLog(@"%@",change);
}
}
- 取消對鍵值的監聽
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"userName"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
實現原理
KVO是根據isa-swizzling技術來實現的,主要依據runtime的強大動態能力。當類A第一次被觀察時,系統會在運行時期動態的創建一個該類的派生類NSKVONotifying_A。NSKVONotifying_A類中重寫任何被觀察屬性的setter方法。
account監聽之后類及方法的改變:
self.account = [[Account alloc] init];
self.account.userName = @"Fly";
NSLog(@"before observer isa:%@---class:%@",object_getClass(self.account), [self.account class]);
NSArray *originMethod = [self getMethodList:object_getClass(self.account)];
NSLog(@"class:%@---method:%@",object_getClass(self.account),originMethod);
[self.account addObserver:self forKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"after observer isa:%@---class:%@",object_getClass(self.account), [self.account class]);
NSArray *newMethod = [self getMethodList:object_getClass(self.account)];
NSLog(@"class:%@---method:%@",object_getClass(self.account),newMethod);
- 添加觀察者之前的類及方法:
before observer isa:Account---class:Account
class:Account---method:(
"setUserName:---v24@0:8@16",
"userName---@16@0:8",
"address---@16@0:8",
".cxx_destruct---v16@0:8",
"password---@16@0:8",
"setPassword:---v24@0:8@16",
"setAddress:---v24@0:8@16"
)
- 添加觀察者之后的類及方法:
after observer isa:NSKVONotifying_Account---class:Account
class:NSKVONotifying_Account---method:(
"setUserName:---v24@0:8@16",
"class---#16@0:8",
"dealloc---v16@0:8",
"_isKVOA---B16@0:8"
)
新增NSKVONotifying_Account的四個方法:
- setUserName
會調用
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
然后在didChangeValueForKey 中,去調用observeValueForKeyPath方法:
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context;
如果沒有執行setter之類的調用,那么使用setValue:forKey方法也會直接調用observeValueForKeyPath:keyPath :object :change :context方法。
如果既沒有調用setter也沒有調用setValue:forKey,那么顯示調用:
[self.account willChangeValueForKey:@"userName"];
[self.account didChangeValueForKey:@"userName"];
就會觸發observeValueForKeyPath:keyPath :object :change :context方法,同樣可以使用KVO。
- class
當修改了isa指向后,isa的值則發生改變,class返回跟重寫繼承類之前同樣的內容。 - dealloc
觀察移除后使class的isa指向原來的類,釋放資源; - _isKVO
判斷被觀察者自己是否同時也觀察了其他對象
參考鏈接
https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html
https://lpd-ios.github.io/2017/03/11/KVC-KVO/
https://techbird.me/2018/05/23/ios-kvc-and-kvo/#KVO%E7%9A%84%E5%8E%9F%E7%90%86
http://southpeak.github.io/2015/04/23/cocoa-foundation-nskeyvalueobserving/
https://tianziyao.github.io/2016/02/08/iOS%E6%A8%A1%E5%9E%8B%20-%20KVC/