KVC & KVO 原理剖析
1.KVO
前沿
KVO(Key-Value Observing, 键值观察), KVO的实现也依赖于runtime. 当你对一个对象进行观察时, 系统会动态创建一个类继承自原类, 然后重写被观察属性的setter方法. 然后重写的setter方法会负责在调用原setter方法前后通知观察者. KVO还会修改原对象的isa指针指向这个新类.
我们知道, 对象是通过isa指针去查找自己是属于哪个类, 并去所在类的方法列表中查找方法的, 所以这个时候这个对象就自然地变成了新类的实例对象.
不仅如此, Apple还重写了原类的- class方法, 试图欺骗我们, 这个类没有变, 还是原来的那个类(偷龙转凤). 只要我们懂得Runtime的原理, 这一切都只是掩耳盗铃罢了.
KVO的缺陷
Apple给我们提供的KVO不能通过block来回调处理, 只能通过下面这个方法来处理, 如果监听的属性多了, 或者监听了多个对象的属性, 那么这里就痛苦了, 要一直判断判断if else if else....多麻烦啊, 说实话我也不懂为什么Apple不提供多一个传block参数的方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
那么, 既然Apple没有帮我们实现, 那我们就手动实现一个呗, 先看下我们最终目标是什么样的 :
小插曲:
假设被观察者为A类的实例L,实现流程如下:
1.在运行时,为A类创建一个子类B。
2.强行将实例L的类型改为B。
3.为B类添加新的setter方法。
4.为B类添加观察者列表属性M。
5.将观察者的信息封装为类放入B类的M。
重点在第三项——kvo的setter方法如何写:
因为是将实例L的类更改为了原类A的子类B,需要调用父类的对应的setter方法。
由于在整个KVO过程中,观察的属性不一致则setter方法的名字也不一致。无法直接运用super调用,最简单的方法就是通过runtime来实现。
1. 获得setter方法名
2. 根据setter方法名获得对应的setter消息
3. 根据setter方法名获得getter方法名
4. 根据getter方法名获得被观察属性当前值
5. 创建消息传递结构体(为了把setter消息转发给父类)
6. 把setter消息转发给父类
7. 遍历观察者列表,得到观察者信息,执行操作
依据代码来分析原理:
//
// NSObject+LeeKVO.m
// LeeSDWebImageLearn
//
// Created by LiYang on 2017/7/19.
// Copyright © 2017年 LiYang. All rights reserved.
//
#import "NSObject+LeeKVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
//自添加观察者
NSString *const kLEEKVOClassPrefix = @"LEEKVOClassPrefix_";
//自添加观察者数组属性
NSString *const kLEEKVOAssociatedObservers = @"LEEKVOAssociatedObservers";
#pragma mark - LEEObservationInfo
//观察者信息聚合类
@interface LEEObservationInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) LEEKVOCallBack block;
@end
@implementation LEEObservationInfo
- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(LEEKVOCallBack)block
{
self = [super init];
if (self) {
_observer = observer;//观察者
_key = key;//观察者观察的属性
_block = block;//观察者察觉属性变化后执行的block
}
return self;
}
@end
#pragma mark - Debug Help Methods
static NSArray *ClassMethodNames(Class c){
//根据类名,获取类的方法列表。
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString *name, id obj){
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tRuntime class %s\n\timplements methods <%@>\n\n",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
#pragma mark - Helpers
//根据setter方法名生成key
static NSString * getterForSetter(NSString *setter){
if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// remove 'set' at the begining and ':' at the end
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *key = [setter substringWithRange:range];
// lower case the first letter
NSString *firstLetter = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)
withString:firstLetter];
NSLog(@"key === %@",key);
return key;
}
//根据getter方法名生成setter方法名
static NSString * setterForGetter(NSString *getter){
if (getter.length <= 0) {
return nil;
}
// upper case the first letter
NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
NSString *remainingLetters = [getter substringFromIndex:1];
// add 'set' at the begining and ':' at the end
NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
return setter;
}
#pragma mark - Overridden Methods
static void kvo_setter(id self, SEL _cmd, id newValue){
//根据SEL获得setter方法名
NSString *setterName = NSStringFromSelector(_cmd);
//进而获得getter方法名(就知道了属性的名字,即为被观察者的key)
NSString *getterName = getterForSetter(setterName);
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
//获取被观察者的属性的当前值
id oldValue = [self valueForKey:getterName];
//构建消息传递类,class为父类,实际接受者为自己
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
//定义消息转发
void (*objc_msgSendSuperSetValue)(void *, SEL, id) = (void *)objc_msgSendSuper;
//利用消息传递类,转发消息——实质是调用父类的setter方法
//更改被观察的属性的父类的值
objc_msgSendSuperSetValue(&superclazz, _cmd, newValue);
//获得观察者列表
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kLEEKVOAssociatedObservers));
//轮训列表中的观察者哪些观察了这个属性,进而执行传入的block
for (LEEObservationInfo *each in observers) {
if ([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
each.block(self, getterName, oldValue, newValue);
});
}
}
}
//欺骗用户,返回的是父类的class。。。笑点。。。
static Class kvo_class(id self, SEL _cmd){
Class clazz = object_getClass(self); // kvo_class
Class superClazz = class_getSuperclass(clazz); // origin_class
return superClazz;
}
#pragma mark - KVO Category
@implementation NSObject (LeeKVO)
-(void)Lee_addObserver:(NSObject *)observer
forKey:(NSString *)key
andHandler:(LEEKVOCallBack)handlerBack{
//获得setter方法名,然后生成SEL
SEL setterSelector = NSSelectorFromString(setterForGetter(key));
//在类方法列表中寻找setter方法
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
//如果没有,则抛出异常
if (!setterMethod) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
//得到当前类名
Class clazz = object_getClass(self);
NSString *clazzName = NSStringFromClass(clazz);
//创建KVO子类
if (![clazzName hasPrefix:kLEEKVOClassPrefix]) {
clazz = [self makeKvoClassWithOriginalClassName:clazzName];
//强行更改自身类型,该函数的作用是为一个 对象 设置一个指定的带有前缀的类
object_setClass(self, clazz);
}
//如果不存在setter方法则添加setter方法
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}
//存放观察者信息的类
LEEObservationInfo *info = [[LEEObservationInfo alloc] initWithObserver:observer Key:key block:handlerBack];
//获得观察者信息列表
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kLEEKVOAssociatedObservers));
//如果不存在则添加观察者列表属性
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void *)(kLEEKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//将观察者信息放入观察者列表
[observers addObject:info];
}
//移除观察者
-(void)Lee_removeObserver:(NSObject *)observer forKey:(NSString *)key {
NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(kLEEKVOAssociatedObservers));
LEEObservationInfo *infoToRemove;
for (LEEObservationInfo* info in observers) {
if (info.observer == observer && [info.key isEqual:key]) {
infoToRemove = info;
break;
}
}
[observers removeObject:infoToRemove];
}
//新建子类
- (Class)makeKvoClassWithOriginalClassName:(NSString *)sourceClass{
//为中间类加上自定义前缀,方便自己识别
NSString *kvoClassName = [kLEEKVOClassPrefix stringByAppendingString:sourceClass];
//生成类
Class class = NSClassFromString(kvoClassName);
if (class) {
return class;
}
// 貌似在这里已经被子类化了。。操。
Class originalClass = object_getClass(self);
//新建类,KVO类,父类是originalClass 添加类 superclass 类是父类 name 类的名字 size_t 类占的空间
Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
//为KVO类替换掉class方法 —— 添加了之后之前的方法应该是和类目一样被放在后面去了 无法调用??
Method clazzMethod = class_getInstanceMethod(originalClass, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClass, @selector(class), (IMP)kvo_class, types);
//注册KVO类
objc_registerClassPair(kvoClass);
//返回KVO类
return kvoClass;
}
//检查类是否包含这个方法
- (BOOL)hasSelector:(SEL)selector{
Class clazz = object_getClass(self);
unsigned int methodCount = 0;
Method* methodList = class_copyMethodList(clazz, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if (thisSelector == selector) {
free(methodList);
return YES;
}
}
free(methodList);
return NO;
}
@end
设计思路是:
- 给一个类的对象属性添加观察者时候,首先判断这个属性有没有setter方法如果没有抛出异常
- 给当前类的对象创建一个继承自当前对象的类的子类,然后把当前操作的对象指定到创建的这个子类
- 此处添加了一个class方法到子类中,相当于调用时候,给用户造成的假象是仍然是原来的类
- 给创建的子类添加对应于要观察的属性的setter方法,并把要观察的属性的相关信息添加到信息列表中
- 此处添加的setter方法是在子类中重写后的setter方法
- 当外界触发了settter方法的时候,就会进入这个子类的setter方法中来,同时通过kvc获取前一次的值
- 构建一个给父类发消息的结构体对象,然后将消息发送出去,其中包括了发送的目的类,要执行的方法,以及参数值。
- 接着就是对外回调出结果
2.KVC
1.KVC 是 Key-Value-Coding 的简称。
2.KVC 是一种可以直接通过字符串的名字 key 来访问类属性的机制,而不是通过调用 setter、getter 方法去访问。
3.我们可以通过在运行时动态的访问和修改对象的属性。而不是在编译时确定,KVC 是 iOS 开发中的黑魔法之一。
KVC 的主要方法:
- 设置值:
// value的值为OC对象,如果是基本数据类型要包装成NSNumber或NSValue
- (void)setValue:(id)value forKey:(NSString *)key;
// keyPath键路径,类型为xx.xx
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
// 它的默认实现是抛出异常,可以重写这个函数做错误处理。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
- 获取值:
- (id)valueForKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
// 如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (id)valueForUndefinedKey:(NSString *)key;
NSKeyValueCoding 类别中还有其他的一些方法:
// 允许直接访问实例变量,默认返回YES。如果某个类重写了这个方法,且返回NO,则KVC不可以访问该类。
+ (BOOL)accessInstanceVariablesDirectly;
// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
// 如果你在setValue方法时面给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;
// 输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。
- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
// KVC提供属性值确认的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。
- (BOOL)validateValue:(id)ioValue forKey:(NSString *)inKey error:(NSError)outError;
小试牛刀:
//
// People.h
// LeeSDWebImageLearn
//
// Created by LiYang on 2017/7/19.
// Copyright © 2017年 LiYang. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface People : NSObject{
@private
NSString * _girlFriend;
}
@property(nonatomic,copy,readonly)NSString * name;
@end
这里给只读河私有的属性和变量赋值操作
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.peo setValue:@"beautifulgirl" forKey:@"_girlFriend"];
[self.peo setValue:@"oliverlee" forKey:@"name"];
NSLog(@"%@,%@",self.peo.name,[self.peo valueForKey:@"_girlFriend"]);
}
KVC 实现细节
-(void)setValue:(id)value forKey:(NSString *)key;
- 首先搜索setter 方法,有就赋值
- 如果没有setter方法,再检查类方法+(BOOL)accessInstanceVariablesDirectly
- 返回YES 继续查找相相关变量 _key ,_isKey,key,isKey 顺序检索,未找到调用
setValue:forUndefinedKey: 如果实现了这个方法不会跑出异常,为实现会奔溃 - 返回NO 执行 setValue:forUndefinedKey: 如果实现了这个方法不会跑出异常,为实现会奔溃
- 返回YES 继续查找相相关变量 _key ,_isKey,key,isKey 顺序检索,未找到调用
- 未找到 的话 调用setValue:forUndefinedKey: 如果实现了这个方法不会跑出异常,为实现会奔溃
-(id)valueForKey:(NSString *)key;
- 首先查找getter 方法,找到直接调用,如果是 bool、int、float 等基本数据类型,会做 NSNumber 的转换。
- 如果没查到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly
- 返回 NO,则执行valueForUNdefinedKey:若未实现,则奔溃
- 返回 YES,按 _key,_isKey,key,isKey的顺序检索成员名,
- 还没有找到的话,调用valueForUndefinedKey:若未实现,则奔溃
KVC 与点语法比较
用 KVC 访问属性和用点语法访问属性的区别:
- 用点语法编译器会做预编译检查,访问不存在的属性编译器会报错,但是用 KVC 方式编译器无法做检查,如果有错误只能运行的时候才能发现(crash)。
- 相比点语法用 KVC 方式 KVC 的效率会稍低一点,但是灵活,可以在程序运行时决定访问哪些属性。
- 用 KVC 可以访问对象的私有成员变量。
KVC 运用
- 字典转模型
-(void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
#import <Foundation/Foundation.h>
@interface timeModel :NSObject
@property(nonatomic,copy)NSString * timer;
@property(nonatomic,copy)NSString * name;
@end
@interface People : NSObject{
@private
NSString * _girlFriend;
}
@property(nonatomic,copy,readonly)NSString * name;
@property(nonatomic,assign)int age;
@property(nonatomic,strong)id arr;
// People.m
// LeeSDWebImageLearn
// Created by LiYang on 2017/7/19.
// Copyright © 2017年 LiYang. All rights reserved.
//
#import "People.h"
@implementation timeModel
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
@end
@implementation People
-(void)setValue:(id)value forKey:(NSString *)key{
if ([key isEqualToString:@"arr"]) {
NSMutableArray *array = (id)value;
NSMutableArray * modelArr = [NSMutableArray array];
for (int i =0; i < array.count; i++) {
NSDictionary * dic = array[i];
timeModel * model = [[timeModel alloc] init];
[model setValuesForKeysWithDictionary:dic];
[modelArr addObject:model];
}
value = modelArr;
}
[super setValue:value forKey:key];
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
@end
[self.peo setValue:@"beautifulgirl" forKey:@"_girlFriend"];
[self.peo setValue:@"oliverlee" forKey:@"name"];
NSLog(@"%@,%@",self.peo.name,[self.peo valueForKey:@"_girlFriend"]);
NSDictionary * peoDic = @{@"name":@"liyang",@"age":[NSNumber numberWithInt:100],@"key":@"hello",@"arr":@[@{@"name":@"xxxx",@"timer":@"yyyyy"},@{@"name":@"xxxx",@"timer":@"yyyyy"},@{@"name":@"xxxx",@"timer":@"yyyyy"},@{@"name":@"xxxx",@"timer":@"yyyyy"}]};
[self.peo setValuesForKeysWithDictionary:peoDic];
NSLog(@"%@,%d,%@",self.peo.name,self.peo.age,self.peo.arr);
2017-07-19 18:07:29.759 LeeSDWebImageLearn[3835:437377] oliverlee,beautifulgirl
2017-07-19 18:07:29.760 LeeSDWebImageLearn[3835:437377] liyang,100,(
"<timeModel: 0x608000022860>",
"<timeModel: 0x608000022740>",
"<timeModel: 0x6080000225e0>",
"<timeModel: 0x6080002245e0>"
)
上面是用KVC 转换的嵌套类型,用着很活,自己去定义重写她的几个方法就好了,比较轻量级别
- 修改私有属性
1.修改 TextField 的 placeholder:
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[_textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@“_placeholderLabel.font"];
2.修改 UIPageControl 的图片:
[_pageControl setValue:[UIImage imageNamed:@"selected"] forKeyPath:@"_currentPageImage"];
[_pageControl setValue:[UIImage imageNamed:@"unselected"] forKeyPath:@"_pageImage"];
- 集合类操作符(用不多)
获取集合类的 count,max,min,avg,svm。隐藏函数,访问时候要加@,确保操作的属性为数字类型,否则会报错。
- @count 返回一个值为集合中对象总数的NSNumber对象;
- @avg 首先把集合中的每个对象都转换为double类型,然后计算其平均值,并返回这个平均值的NSNumber对象;
- @max 使用compare:方法来确定最大值,并返回最大值的NSNumber对象.所以为了保证其正常比较,集合中所有的对象都必须支持和另一个对象的比较,保证其可比性;
- @min 原理和@max一样,其返回的是集合中的最小值的NSNumber对象;
- @sum 首先把集合中的每个对象都转换为double类型,然后计算其总和,并返回总和的NSNumber对象;
- Notice: 所有的集合操作,除了@count外,其他都需要有右边的keyPath(一般为属性名),@count右边的keyPath可写可不写.
1.Notice:若操作对象(数组/集合)内的元素本身就是 NSNumber 对象,那么可以这样写.
NSMutableArray * numArray = @[].mutableCopy;
for (int i =0 ; i< 15; i++) {
[numArray addObject:@(i)];
}
NSLog(@"%@",[numArray valueForKeyPath:@"@count"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@min.self"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@max.self"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@sum.self"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@avg.self"]);
控制台:
2017-07-19 18:56:37.071 LeeSDWebImageLearn[4247:488147] 15
2017-07-19 18:56:37.072 LeeSDWebImageLearn[4247:488147] 0
2017-07-19 18:56:37.072 LeeSDWebImageLearn[4247:488147] 14
2017-07-19 18:56:37.072 LeeSDWebImageLearn[4247:488147] 105
2017-07-19 18:56:37.073 LeeSDWebImageLearn[4247:488147] 7
2.若数组中的元素为模型类对象可以这样写
NSMutableArray * numArray = @[].mutableCopy;
//模拟数据
ProductModel *productA = [[ProductModel alloc] init];
productA.price = 99.0;
productA.name = @"iPod";
ProductModel *productB = [[ProductModel alloc] init];
productB.price = 199.0;
productB.name = @"iMac";
ProductModel *productC = [[ProductModel alloc] init];
productC.price = 299.0;
productC.name = @"iPhone";
ProductModel *productD = [[ProductModel alloc] init];
productD.price = 199.0;
productD.name = @"iPhone";
[numArray addObjectsFromArray:@[productA,productB,productC,productD]];
NSLog(@"%@",[numArray valueForKeyPath:@"@count"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@min.price"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@max.price"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@sum.price"]);
NSLog(@"%@",[numArray valueForKeyPath:@"@avg.price"]);
KVC 总结
键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。
优点:
- 不需要通过 setter、getter 方法去访问对象的属性,可以访问对象的私有属性。
- 可以轻松处理集合类(NSArray)。
缺点:
- 一旦使用 KVC 你的编译器无法检查出错误,即不会对设置的键、键值路径进行错误检查。
- 执行效率要低于 setter 和 getter 方法。因为使用 KVC 键值编码,它必须先解析字符串,然后在设置或者访问对象的实例变量。
- 使用 KVC 会破坏类的封装性。
友情链接:
http://www.jianshu.com/p/d702286b0b49
https://opensource.apple.com/
http://www.jianshu.com/p/4748ef75126a
http://blog.csdn.net/u010123208/article/details/4042514
http://www.jianshu.com/p/2c2af5695904