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
: 或者 _set 设置方法,如果方法存在,结束查找。 - 检查 + (BOOL)accessInstanceVariablesDirectly 方法是否返回 YES。该方法默认返回 YES,继续查找。如果重写设置为 NO,执行 - (void)setValue:forUndefinedKey: 方法,默认是抛出异常,不推荐设置为 NO。
- 按照 _
,_is , 和 is 的顺序在类接口定义和实现处查找实例变量,再赋值。如果上述方法和实例变量都不存在,就执行 - (void)setValue:forUndefinedKey:。
valueForKey:
- 首先按get
is 的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者int等值类型, 会做NSNumber转换,如果不存在,继续查找。 - 如果countOf
和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合,它是NSKeyValueArray,是NSArray的子类,这个代理集合将拥有以上方法的组合,还有一个可选的get : range:方法;
countOf & objectInAtIndex & AtIndex
所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
- 如果上面的方法没有找到,那么会查找:
countOf & enumeratorOf & memberOf
以上三种格式的方法,如果这三个方法都找到,那么就返回一个可以响应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
测试代码:
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 *)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 *)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://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html
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/