iOS - KVC 和 KVO 的使用和原理
KVC
KVC(键值编码),即 Key-Value Coding,一个非正式的 Protocol,使用字符串(键)访问一个对象实例变量的机制。而不是通过调用 Setter、Getter 方法等显式的存取方式去访问。
简介
KVC(Key-value coding),键值编码;是指在iOS开发中,可以允许开发者通过属性名Key直接访问对象的属性并给属性编码(赋值value),而不是调用对应的getter/setter方法。很多高级的iOS开发技巧都是基于KVC实现,例如修改系统控件内部属性;json->model的映射框架等。
KVC最重要的四个方法
- (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来设值
valueForKey 和 valueForKeyPath区别
先来看一段代码
- (void)viewDidLoad {
[super viewDidLoad];
NSDictionary * dict = @{@"key1":@"value1",
@"second":@{
@"k1":@"v1",
@"k2":@"v2",
@"third":@{
@"t1":@"h1",
@"t2":@"h2"
}
}
};
NSDictionary * second = [dict valueForKey:@"second"];
NSDictionary * third1 = [second valueForKey:@"third"];
NSLog(@"%@",third1);
NSDictionary * third2 = [dict valueForKeyPath:@"second.third"];
NSLog(@"%@",third2);
}
输出结果:
1
2
3
4
5
6
7
8
2018-05-23 13:52:33.430617+0800 KVC-Demo[1584:403407] {
t1 = h1;
t2 = h2;
}
2018-05-23 13:52:33.430800+0800 KVC-Demo[1584:403407] {
t1 = h1;
t2 = h2;
}
小结
通过代码我们可以看出,我们想要从dict这个字典中获取到third这个key所对应的值得话,使用valueForKey需要通过一层一层的对象才能取到想要的字典,而使用valueForKeyPath则只需要输入third在字典中的路径(path)一次就可以取到third字典。
参考博客
https://www.jianshu.com/p/c0e099f72a3b https://www.jianshu.com/p/a6a0abac1c4a
KVC的使用
代替getter/setter
@interface Model : NSObject
@property (copy, nonatomic) NSString * text;
@property (copy, nonatomic) SubModel * subModel;
@end
@interface SubModel : Model
@property (copy, nonatomic) NSString * subText;
@end
- 不使用kvc
//赋值 Model *model = [[Model alloc]init]; model.text = @"text"; SubModel *subModel = [[SubModel alloc]init]; subModel.subText = @"subText"; model.subModel = subModel; //取值 NSString *text = model.text; NSString *subText = model.subModel.subText; - 使用kvc
//赋值 Model *model = [[Model alloc]init]; [model setValue:@"text" forKey:@"text"]; [model setValue:@"subText" forKeyPath:@"subModel.subText"]; //取值 NSString *text = [model valueForKey:@"text"]; NSString *subText = [model valueForKeyPath:@"subModel.subText"];字典转模型(仿YYModel)
- 创建NSObject的扩展NSObject+Model ```Objc @interface NSObject (Model)
- (instancetype) tb_modelWithDictionary:(NSDictionary * )dictionary; @end ```
- 实现SObject+Model 利用Runtime取到对应类的属性列表,在使用kvc对所有属性进行赋值 ```Objc @implementation NSObject (Model)
(NSArray * )getPropertyList:(Class)cls{ NSArray * array = objc_getAssociatedObject(self,
_cmd); if (array != nil){ return array; } NSMutableArray * arrM = [NSMutableArray array]; //输出个数 unsigned int outCount; //获取属性列表( objc_property_t * ) objc_property_t * properties = class_copyPropertyList(cls, &outCount); for (NSInteger i=0; i<outCount; ++i) { objc_property_t property = properties[i]; //属性名字 NSString * name = [NSString stringWithUTF8String:property_getName(property)]; [arrM addObject:name]; } objc_setAssociatedObject(self, @selector(getPropertyList:), [arrM copy], OBJC_ASSOCIATION_RETAIN); free(properties); return [arrM copy]; }(instancetype)tb_modelWithDictionary:(NSDictionary * )dictionary{ NSObject * object = [[self alloc]init];
NSArray * array = [self getPropertyList:[self class]]; [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString * key, id value, BOOL * stop) { if ([array containsObject:key]){ [object setValue:value forKey:key]; } }];
return object; } @end ```
- 使用tb_modelWithDictionary模仿YYModel的字典转模型方式 ```Objc
- (void)viewDidLoad { [super viewDidLoad]; NSDictionary * dict = @{ @”text”:@”text” }; Model * model = [Model tb_modelWithDictionary:dict]; NSLog(@”%@”,model); } ```
修改系统控件内部属性(runtime+kvc)
- 需求:修改UIPageControl小圆点的背景图片
- 查看UIPageControl.h如下 ```Objc 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
1
2
3
4
5
6
7
没有发现UIPageControl暴露的操作中有设置小圆点背景图片的方法和属性,那么就可以利用runtime遍历UIPageControl类的成员变量(`ivar`)和属性(`property`)
- 利用runtime遍历UIPageControl成员变量
导入头文件:
```Objc
#import <objc/runtime.h>
遍历成员变量:
- (void)viewDidLoad {
[super viewDidLoad];
UIPageControl * pc = [[UIPageControl alloc]init];
NSArray * array = [self getIvarList:[pc class]];
NSLog(@"%@",array);
}
- (NSArray * )getIvarList:(Class)cls{
NSMutableArray * arrM = [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];
[arrM addObject:str];
}
free(ivars);
return [arrM copy];
}
输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2018-05-23 16:45:18.380346+0800 KVC-Demo[4400:856521] (
"_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
UIPageControl *pc = [[UIPageControl alloc]init];
[pc setValue:[UIImage imageNamed:@"pageImage"] forKeyPath:@"_pageImage"];
[pc setValue:[UIImage imageNamed:@"currentPageImage"] forKeyPath:@"_currentPageImage"];
XIB/Storyboard
在xib/Storyboard中,也可以使用KVC,例如下面是在xib中使用KVC把图片边框设置成圆角。 
KVO
KVO(键值监听),即 Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,对象就会接受到通知,前提是执行了 setter 方法、或者使用了 KVC 赋值。
简介
KVO 是 Objective-C 对观察者设计模式的一种实现;[另外一种是:通知机制(notification)]。
使用(Swift)
需求:UIScrollView内包含一部分原生控件和UIWebView的组合;这种情况下UIWebView的高度无法得知,因为UIWebView写完中包含UIScrollerView,所以需要利用KVO技术监听UIWebView中UIScrollerView的contentSize的变化以达到需求的目的。
addOberver
- 一般在viewDidLoad中添加监听
if let scrollView = mWebView.subviews.first as? UIScrollView { scrollView.alwaysBounceVertical = false scrollView.alwaysBounceHorizontal = false scrollView.bounces = false scrollView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil) }
observerValueForkeyPath
- 当contentSize发生变化时,会回调到
observerValueForkeyPath这个方法override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "contentSize" && change != nil { let value = (change![NSKeyValueChangeKey.newKey] as! NSValue).cgSizeValue.height desViewHeightCons.constant = value } }
removeObserver
- 当界面销毁时,移除监听
deinit { if let scrollView = mWebView?.subviews.first as? UIScrollView { scrollView.removeObserver(self, forKeyPath: "contentSize") } }
KVO的原理
键值编码(KVC)和键值观察(KVO)是根据isa-swizzling技术来实现的,主要依据runtime的强大动态能力。 当某个类第一次被观察时,系统会在运行时期动态的创建一个该类的派生类,在这个派生类中重写任何被观察属性的setter方法。派生类在被重写的setter方法实现真正的通知机制,这么设计是基于设置属性会调用setter方法,而通过重写就获得了KVO需要的通知机制,当然前提是要遵循KVO的属性设置方式来变更属性值,如果直接修改属性对应的成员变量是无法实现KVO的。 同时派生类还重写了class方法欺骗外部调用者它就是起初的那个类,然后系统将isa指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因为在该对象上对setter的调用就会调用的重写的setter,从而激活键值通知机制。此外派生类还重写了delloc方法来释放资源。
在Runtime篇章中介绍过,isa指针其实指向的是类的元类,如果添加监听之前的类名为Person,那么添加监听之后被runtime更改以后的类名会变成:NSKVONotifying_Person
新的派生类NSKVONotifying_Person会重写以下方法: 增加了监听的属性对应的setter,class,delloc,_isKVOA
class
重写class方法是为了方便我们调用它的时候,返回跟重写继承类之前同样的内容。
Person *person = [[Person alloc]init];
NSLog(@"before isa:%@ class:%@",object_getClass(person), [person class]);
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
NSLog(@"end isa:%@ class:%@",object_getClass(person), [person class]);
_person = person;
输出结果:
1
2
2018-05-23 19:55:23.430908+0800 KVO-Demo[6395:1312349] before isa:Person class:Person
2018-05-23 19:55:23.431456+0800 KVO-Demo[6395:1312349] end isa:NSKVONotifying_Person class:Person
这也是isa指针和class方法的一个区别,使用的时候要特别注意。
setter
新的派生类会重写对应的setter方法,其实是为了在setter中增加另外两个方法的调用
- (void)willChangeValueForKey:(NSString * )key
- (void)didChangeValueForKey:(NSString * )key
其中 didChangeValueForKey负责触发:observeValueForKeyPath:keyPath :object :change :context方法,这就是kvo的原理。 如果没有执行setter之类的调用,那么使用setValue:forKey方法也会直接调用observeValueForKeyPath:keyPath :object :change :context方法 再如果既没有调用setter也没有调用setValue:forKey,那么
- (void)willChangeValueForKey:(NSString * )key
- (void)didChangeValueForKey:(NSString * )key
我们只需要显示调用上述两个方法,就会触发observeValueForKeyPath:keyPath :object :change :context方法,同样可以使用KVO。
_isKVOA
这个私有方法是用来表示该类是一个KVO机制声明的类
小结(触发KVO的三种方法)
- 使用KVC (运行时会在
setValue:forKey中来调用will/didChangeValueForKey:) - 使用setter方法(运行时会在setter方法中调用
will/didChangeValueForKey:) - 显示调用
will/didChangeValueForKey:方法
如何更优雅的使用KVO
只需要使用 Facebook 开源的 KVOController 框架就可以优雅地解决这些问题了。
[self.KVOController observe:person
keyPath:@"age"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
block:^(id observer, id object, NSDictionary<NSString * , id> * change) {
NSLog(@"%@", change);
}];
我们可以在任意对象上获得 KVOController 对象,然后调用它的实例方法 -observer:keyPath:options:block: 就可以检测某个对象对应的属性了,该方法传入的参数非常容易理解,在 block 中也可以获得所有与 KVO 有关的参数。
使用 KVOController 进行键值观测可以说完美地解决了在使用原生 KVO 时遇到的各种问题:
不需要手动移除观察者; 实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数; 使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验; 每一个 keyPath 会对应一个属性,不需要在 block 中使用 if 判断 keyPath;
参考文档和博客
http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/KeyValueCoding/Articles/KeyValueCoding.html#//apple_ref/doc/uid/10000107-SW1 https://blog.csdn.net/wzzvictory/article/details/9674431 https://blog.csdn.net/kesalin/article/details/8194240 https://draveness.me/kvocontroller