KVC & KVO 原理剖析

2017-07-19  本文已影响51人  d20cdad48d2e

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

设计思路是:

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;

-(id)valueForKey:(NSString *)key;

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对象;

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 总结

键值编码是一种间接访问对象的属性使用字符串来标识属性,而不是通过调用存取方法直接或通过实例变量访问的机制,非对象类型的变量将被自动封装或者解封成对象,很多情况下会简化程序代码。

优点:

缺点:

友情链接:
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

上一篇下一篇

猜你喜欢

热点阅读