iOS底层探索-KVO

1、KVC

键值编码,通过Key名直接访问对象属性,由NSKeyValueCoding非正式协议启用的机制

@interface LZPerson : NSObject {
    @public
    NSString *name;
}
#import "ViewController.h"
#import "LZPerson.h"

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    LZPerson *p = [LZPerson alloc];
    p->name = @"lz";
    [p setValue:@"kvcValue" forKey:@"name"];
    NSLog(@"%@",[p valueForKey:@"name"]);
}
@end

// 打印结果
kvcValue
  • KVC 本质上是对 NSObjectNSArrayNSDictionaryNSMutableDictionaryNSOrderedSetNSSet 等对象,实现NSKeyValueCoding分类,赋予它们Key-Value Coding的能力;详情参考 KVC文档

1.1、赋值流程

  • 首先会去找类的 set方法,如果找不到会去找 带下划线的set方法
    @implementation LZPerson
    - (void)setName:(NSString *)name {
        self->name = @"setValue";
    }
    
    - (void)_setName:(NSString *)name {
        self->name = @"_setValue";
    }
    @end
    
  • 如果都找不到,则会看 +(BOOL)accessInstanceVariablesDirectly方法中的返回(默认为YES
    // 按照 _key、_isKey、key、isKey 的顺序找属性赋值
    @interface LZPerson : NSObject {
        @public
        NSString *_name;
        NSString *_isName;
        NSString *name;
        NSString *isName;
    }
    @end
    
    • 返回YES时,会按照 _key_isKeykeyisKey 的顺序找属性赋值,如果 类中没有上面的这些属性 则会调用-(void)setValue:(id)value forUndefinedKey:(NSString *)key方法(自己实现一下,否则报错)
    • 返回NO时,会直接调用 -(void)setValue:(id)value forUndefinedKey 方法
      @implementation LZPerson
      + (BOOL)accessInstanceVariablesDirectly {
          return YES;
      }
      // 简单实现一下防止崩溃
      - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
          NSLog(@"%s",__func__);
      }
      @end
      

image.png

1.2、取值流程

  • 首先取值会按 getKeykeyisKey_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时,会按照 _key_isKeykeyisKey 的顺序找属性取值,如果 类中没有这些属性 则会调用-(id)valueForUndefinedKey:(NSString *)key方法(自己实现一下,否则报错)
    • 返回NO时,直接调用 -(id)valueForUndefinedKey image.png

image-2.png

1.3、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.4、自定义KVC

相关方法

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

@implementation NSObject (LZKVC)
- (BOOL)lz_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;
}

KVC存储

// 自定义KVC-存储
- (void)lz_setValue:(nullable id)value forKey:(NSString *)key{
   
    // 1: 判断key
    if (key == nil || key.length == 0) {
        return;
    }
    
    // 2: setter:set<Key>:→_set<Key>→setIs<Key>
    // key要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
    NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
    NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
    
    if ([self lz_performSelectorWithMethodName:setKey value:value]) {
        NSLog(@"*********%@**********",setKey);
        return;
    }else if ([self lz_performSelectorWithMethodName:_setKey value:value]) {
        NSLog(@"*********%@**********",_setKey);
        return;
    }else if ([self lz_performSelectorWithMethodName:setIsKey value:value]) {
        NSLog(@"*********%@**********",setIsKey);
        return;
    }
    
    // 3: 判断能否直接赋值实例变量,如果accessInstanceVariablesDirectly返回NO,奔溃
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LZUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 间接变量
    // 获取 ivar -> 遍历 containsObjct
    // 4.1: 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key>→_is<Key>→<key>→is<Key>
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    if ([mArray containsObject:_key]) {
        // 4.2: 获取相应的 ivar
       Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
        // 4.3: 对相应的 ivar 设置值
       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;
    }

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

KVC读取

- (nullable id)lz_valueForKey:(NSString *)key{
    
    // 1: 判断key
    if (key == nil  || key.length == 0) {
        return nil;
    }

    // 2: 找到相关方法get<Key>→<key> countOf<Key>  objectIn<Key>AtIndex
    // key要大写
    NSString *Key = key.capitalizedString;
    // 拼接方法
    NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
    NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
    NSString *_key = [NSString stringWithFormat:@"_%@",key];
    NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",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)];
    }
    else if ([self respondsToSelector:NSSelectorFromString(countOfKey)]){
        
        NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
        if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
            int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
            for (int i = 0; i<num-1; i++) {
                num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
            }
            for (int j = 0; j<num; j++) {
                id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
                [mArray addObject:objc];
            }
            return mArray;
        }
    }
#pragma clang diagnostic pop
    
    // 3: 判断是否能够直接赋值实例变量
    if (![self.class accessInstanceVariablesDirectly] ) {
        @throw [NSException exceptionWithName:@"LUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.****",self] userInfo:nil];
    }
    
    // 4: 找相关实例变量进行赋值
    // 4.1: 定义一个收集实例变量的可变数组
    NSMutableArray *mArray = [self getIvarListName];
    // _<key>→_is<Key>→<key>→is<Key>
    // _name→_isName→name→isName
    NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
    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 @"";
}

2、KVO

  • KVO 全称Key-Value Observing(键值观察),是一种机制,允许对象在 其他对象的指定属性发生更改时 收到通知

2.1、KVO 和 NSNotificatioCenter 的差异

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

2.2、监听过程

  • 注册观察者

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

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

    [self.person removeObserver:self forKeyPath:@"name" context:NULL];
    
    • 如果被观察者是单例,那么如果被观察者所在界面销毁时不移除观察者会崩溃(被观察者未释放,值改变方法还要调用,但界面被释放,这个方法找不到了所以崩溃)
  • 设置 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.3、手动关闭KVO、手动触发KVO

  • +(BOOL)automaticallyNotifiesObserversForKey手动关闭KVO
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        return NO;
    }
    
  • willChangeValueForKeydidChangeValueForKey手动触发KVO
    [LZPerson willChangeValueForKey:@"name"];
    _name = name;
    [LZPerson didChangeValueForKey:@"name"];
    

2.4、监听可变数组

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.5、技术细节

  • 自动键值观察是使用称为isa-swizzling的技术实现
  • isa 指针指向对象的类,它保持一个调度表,该调度表主要包含指向类实现的方法的指针,以及其他数据
  • 当观察者 注册观察对象的某属性 时,被观察对象的 isa 指针被修改指向中间类而不是真正的类;因此,isa 指针的值不一定反映实例的实际类
  • 永远不要依赖 isa 指针来确定类成员身份,应该使用该 class 方法来确定对象实例的类

2.6、底层原理

isa改变
- (void)viewDidLoad {
    [super viewDidLoad];

    self.person = [[LZPerson 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观察者之前:LZPerson
添加KVO观察者之后:NSKVONotifying_LZPerson
  1. 当调用addObserve方法时,系统动态生成当前类的子类NSKVONotifying_类名当前类的子类
  2. 对象会将 isa指针指向这个子类,这个子类会生成对应的 set方法(setName)构造方法dealloc_isKVOA(标记是否为KVO生成的中间类)
  3. set方法中会调用willChangeValueForKeydidChangeValueForKey两个方法
  4. 当注册被移除时,对象将isa指针指回正常
  • NSKVONotifying_类名中的方法

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        self.person = [[LZPerson 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
    
  • 因为类的属性有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"];
    
  • 可以参考FBKVOController

参考文章:www.yuque.com/u12101430/a…

猜你喜欢

转载自juejin.im/post/7115413383813267464