iOS-KVC+KVO

一、KVC

1.1 什么是KVC

  • KVC指的是键值编码;通过key来直接访问对象的属性,然后由NSKeyValueCoding非正式协议启用的机制。
  • KVC本质上是对NSObject、NSArray、NSMutableDictionary、NSOrderedSet、NSSet等对象;实现NSKeyValueCoding的分类,赋予他们键值编码的能力。

1.2 KVC的底层流程

   主要分为set赋值和get取值两个部分。

1.2.1 set赋值流程

  • 首先找类的set方法实现,如果找不到就会找_set方法
@implementation LGPerson
- (void)setName:(NSString *)name {
    self->name = @"setValue";
}

- (void)_setName:(NSString *)name {
    self->name = @"_setValue";
}
@end
  • 如果找不到,就会看+(BOOL)accessInstanceVariablesDirectly(如果返回yes);默认是yes
@interface LGPerson : NSObject {
    @public
    NSString *_name;
    NSString *_isName;
    NSString *name;
    NSString *isName;
}
@end
  • 按照_<key>、_is<Key>、<key>、is<Key>的顺序查找成员变量赋值
  • 如果类中没有这些成员变量就会调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key方法;抛出

    NSUnknownKeyException异常(实现了setValue也不会抛弃异常)

  • 如果+(BOOL)accessInstanceVariablesDirectly返回No,就会直接调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key;抛出NSUnknownKeyException异常
@implementation LGPerson
+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}
// 简单实现一下防止崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    NSLog(@"%s",__func__);
}
@end

1.2.2 流程图(参考自https://juejin.cn/post/7115413383813267464)

 

1.3 get取值流程

  • 按照get<Key>、<key>、is<Key>、_<key>的顺序查找方法

- (id)getName {
    return @"getGetNameValue";
}
- (id)name {
    return @"getNameValue";
}
- (id)isName {
    return @"getIsNameValue";
}
- (id)_name {
    return @"get_NameValue";
}

+ (BOOL)accessInstanceVariablesDirectly {
    return YES;
}

- (id)valueForUndefinedKey:(NSString *)key {
    return @"valueForUndefineKey";
}
  • 如果找不到,就会看+(BOOL)accessInstanceVariablesDirectly(如果返回yes);默认是yes
  • 如果返回yes,会按照按照_<key>、_is<Key>、<key>、is<Key> 的顺序查找成员变量,找到则取值找不到则调用valueForUndefinedKey:并抛出NSUnknownKeyException异常;(实现了也不会抛出异常)
  • 如果返回No,直接调用 -(id)valueForUndefinedKey

1.3.2 流程图(参考自https://juejin.cn/post/7115413383813267464)

1.4 常用的一些API

// 通过 Key 读取和存储
- (nullable id)valueForKey:(NSString *)key;
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通过 keyPath 读取和存储
- (nullable id)valueForKeyPath:(NSString *)keyPath;           
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  

// 默认返回YES,若没有找到Set<Key>方法,按照_key、_iskey、key、iskey顺序搜索成员
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确,为不正确的值做一个替换值或者拒绝设置新值并返回错误原因
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 如果Key不存在,且KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常
- (nullable id)valueForUndefinedKey:(NSString *)key;

// 和上一个方法一样,但这个方法是设值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;

//如果你在SetValue方法时给Value传nil,则会调用这个方法
- (void)setNilValueForKey:(NSString *)key;

//输入一组Key,返回该组Key对应的Value,再转成字典返回,用于将Model转到字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

1.5 自定义KVC

1.5.1 API实现

#import "NSObject+LGKVC.h"
#import <objc/runtime.h>

@implementation NSObject (LGKVC)
- (BOOL)lg_performSelectorWithMethodName:(NSString *)methodName value:(id)value{
 
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
        
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:NSSelectorFromString(methodName) withObject:value];
#pragma clang diagnostic pop
        return YES;
    }
    return NO;
}

- (id)performSelectorWithMethodName:(NSString *)methodName{
    if ([self respondsToSelector:NSSelectorFromString(methodName)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        return [self performSelector:NSSelectorFromString(methodName) ];
#pragma clang diagnostic pop
    }
    return nil;
}

- (NSMutableArray *)getIvarListName{
    
    NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList([self class], &count);
    for (int i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char *ivarNameChar = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar];
        NSLog(@"ivarName == %@",ivarName);
        [mArray addObject:ivarName];
    }
    free(ivars);
    return mArray;
}

1.5.2 KVC存储(set)

- (void)lg_setValue:(nullable id)value forKey:(NSString *)key{
   
    if (key == nil || key.length == 0) {return;}

    NSString *Key = key.capitalizedString;

    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(setKey)]) {
         [self performSelector:NSSelectorFromString(setKey) withObject:value];
        return;
    }else if ([self respondsToSelector:NSSelectorFromString(_setKey)]) {
         [self performSelector:NSSelectorFromString(_setKey) withObject:value];
        return;
    }
#pragma clang diagnostic pop
    
    if (![self.class accessInstanceVariablesDirectly]) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    
    NSMutableArray *mArray = [self getIvarListName];
    if ([mArray containsObject:_key]) {
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:_isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:key]) {
       Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }else if ([mArray containsObject:isKey]) {
       Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
       object_setIvar(self , ivar, value);
       return;
    }
    

    @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key name.****",self,NSStringFromSelector(_cmd)] userInfo:nil];
}

1.5.3 KVC读取(get)

- (nullable id)lg_valueForKey:(NSString *)key{
    
    if (key == nil  || key.length == 0) {
        return nil;
    }

    NSString *Key = key.capitalizedString;
    
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
        return [self performSelector:NSSelectorFromString(getKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(key)]){
        return [self performSelector:NSSelectorFromString(key)];
    }else if ([self respondsToSelector:NSSelectorFromString(isKey)]){
        return [self performSelector:NSSelectorFromString(isKey)];
    }else if ([self respondsToSelector:NSSelectorFromString(_key)]){
        return [self performSelector:NSSelectorFromString(_key)];
    }
#pragma clang diagnostic pop
    
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LGUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    NSMutableArray *mArray = [self getIvarListName];
    
    if ([mArray containsObject:_key]) {
        Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:_isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:key]) {
        Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
        return object_getIvar(self, ivar);;
    }else if ([mArray containsObject:isKey]) {
        Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
        return object_getIvar(self, ivar);;
    }

    return @"";
}

 

二、KVO

2.1 什么是KVO

KVO是键值观察,是一种允许对象在其他对象的指定属性发生更改时收到通知的机制。

2.2 KVO和NSNotification的差异

1.KVO 只能用于监听 对象属性的变化,NSNotificatioCenter 可以监听任何你感兴趣的东西
2.KVO 发出消息由 系统控制,NSNotificatioCenter 由 开发者控制
3.KVO 自动记录新旧值变化,NSNotificatioCenter 只能记录开发者传递的参数

2.3 监听过程(注册观察者;属性变化通知;移除观察者)

2.3.1 注册观察者

[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
  • 消息中的 上下文指针context 包含任意数据,这些数据将在相应的更改通知中传回给观察者;可以指定NULL并完全依赖keyPath字符串来确定更改通知的来源,但这样可能会导致 父类由于不同原因也在观察相同键路径的对象时 出现问题

2.3.2 属性变化通知

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"%@",change);
    }
}

2.3.3 移除观察者

[self.person removeObserver:self forKeyPath:@"name" context:NULL];
  • 如果被观察者是单例模式创建的,那么如果被观察者所在界面销毁时不移除观察者会崩溃(因为被观察者在全局区,不会随界面的销毁而释放,值改变方法还要被调用,但界面被释放导致这个方法找不到了,所以崩溃)

2.3.4 设置context上下文,区分通知来源

static void *PersonNickContext = &PersonNickContext;
static void *PersonNameContext = &PersonNameContext;

[self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == PersonNickContext) {
        NSLog(@"nick:%@",change);
        return;
    }

    if (context == PersonNameContext){
        NSLog(@"name:%@",change);
        return;
    }
}

2.4 手动关闭、触发KVO的API

2.4.1 手动关闭的API

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

2.4.3 手动触发的API (willChangeValueForKey;didChangeValueForKey)

[LGPerson willChangeValueForKey:@"name"];
_name = name;
[LGPerson didChangeValueForKey:@"name"];

2.5 监听可变数组(与属性的监听是不同的API)

self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 这种写法不能收到KVO通知,因为KVO基于KVC,访问 集合对象 有三种不同的代理方法
    // if(self.person.dateArray.count == 0){
    //     [self.person.dateArray addObject:@"1"];
    // }
    // else{
    //     [self.person.dateArray removeObjectAtIndex:0];
    // }
    
    if(self.person.dateArray.count == 0){
        [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
    }
    else{
        [[self.person mutableArrayValueForKey:@"dateArray"] removeObjectAtIndex:0];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}

- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];
}
  • 对集合对象访问定义了三种不同的代理方式
mutableArrayValueForKey:和 mutableArrayValueForKeyPath:
mutableSetValueForKey:和 mutableSetValueForKeyPath:
mutableOrderedSetValueForKey:和mutableOrderedSetValueForKeyPath:
  • 会打印NSKeyValueChange类型的kind,表示键值变化的类型,执行addObject时;kind打印值为2,执行removeObjectAtIndex时,kind的打印值为3
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,      //赋值
    NSKeyValueChangeInsertion = 2,    //插入
    NSKeyValueChangeRemoval = 3,      //移除
    NSKeyValueChangeReplacement = 4,  //替换
};

2.6 注意点

  • KVO是通过isa-swizzling的技术实现的
  • 该isa指针指向对象的类,它保持一个调度表,该调度表主要包含指向类实现的方法的指针,以及其他数据
  • 当观察者注册观察者的某属性时,被观察对象的isa指针被修改,指向中间类而不是真正的类;因此,isa指针的值不一定反映实例的实际类
  • 在KVC模式下可通过getclass的方式来确定对象的类,因为isa指针的指向有可能被修改。

2.7 KVO底层原理

2.7.1 isa指针指向的改变

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[LGPerson alloc] init];
    NSLog(@"添加KVO观察者之前:%s", object_getClassName(self.person));
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    NSLog(@"添加KVO观察者之后:%s", object_getClassName(self.person));
}

// 打印结果
添加KVO观察者之前:LGPerson
添加KVO观察者之后:NSKVONotifying_LGPerson
  • 当调用addObserve方法时,系统动态生成当前类的子类NSKVONotifying_类名(当前类的子类)
  • 对象会将 isa指针指向这个子类,这个子类会生成对应的 set方法(setName)、构造方法、dealloc、_isKVOA(标记是否为KVO生成的中间类)
  • 子类生成的set方法中会调用willChangeValueForKeydidChangeValueForKey两个方法
  • 当注册被移除时,对象将isa指针指回正常

2.7.2 NSKVONotifying_类名中的方法

- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[LGPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];

    unsigned int intCount;
    Method *methodList = class_copyMethodList(objc_getClass("NSKVONotifying_LZPerson"), &intCount);

    for (unsigned int intIndex=0; intIndex<intCount; intIndex++) {

        Method method = methodList[intIndex];
        NSLog(@"SEL:%@,IMP:%p",NSStringFromSelector(method_getName(method)), method_getImplementation(method));
    }
}

// 打印结果
SEL:setNickName:,IMP:0x18a5d8520
SEL:class,IMP:0x18a5d6fd4
SEL:dealloc,IMP:0x18a5d6d58
SEL:_isKVOA,IMP:0x18a5d6d50

2.7.3 KVO对成员变量的监听

因为类的属性有set方法,而 成员变量没有set方法,因此KVO不能监听成员变量;如果一定要监听成员变量,需要 使用KVC触发

[self.person addObserver:self forKeyPath:@"_sex" options:(NSKeyValueObservingOptionNew) context:NULL];
//直接赋值无法触发KVO,要用KVC 
//self.person->_sex = @"male";
[self.person setValue:@"male" forKey:@"_sex"];
posted on 2022-07-02 11:13  suanningmeng98  阅读(93)  评论(0编辑  收藏  举报