KVO分析

作者: 浅墨入画 | 来源:发表于2021-09-05 00:08 被阅读0次

    KVO细节分析上

    KVO官方文档

    • 这里我们主要了解context的细节如下图
    image.png

    关于context,苹果官方文档解释

    ### Context
    
    The context pointer in the `addObserver:forKeyPath:options:context:` message contains arbitrary data that will be passed back to the observer in the corresponding change notifications. You may specify `NULL` and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.
    
    A safer and more extensible approach is to use the context to ensure notifications you receive are destined for your observer and not a superclass.
    
    The address of a uniquely named static variable within your class makes a good context. Contexts chosen in a similar manner in the super- or subclass will be unlikely to overlap. You may choose a single context for the entire class and rely on the key path string in the notification message to determine what changed. Alternatively, you may create a distinct context for each observed key path, which bypasses the need for string comparisons entirely, resulting in more efficient notification parsing. Listing 1 shows example contexts for the `balance` and `interestRate` properties chosen this way.
    
    • 监听回调


      image.png
    • 移除观察者


      image.png

    现在编写代码如下

    // 可以定义context标识符来区分监听的属性
    static void *PersonNameContext = &PersonNameContext;
    
    @interface LGViewController ()
    @property (nonatomic, strong) LGPerson  *person;
    @end
    
    @implementation LGViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
         self.person  = [LGPerson new];
        [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    }
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{    
        self.person.name = @"test";
    }
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
       // 这里使用context来区分监听的属性
       if(context == PersonNameContext){
            NSLog(@"%@",change);
        }
    }
    
    - (void)dealloc{
     [self.person removeObserver:self forKeyPath:@"name" context:PersonNameContext];
    }
    
    // 运行工程,点击页面打印如下
    2021-09-04 12:07:04.887849+0800 001---KVO初探[10193:13690743] {
        kind = 1;
        new = test;
    }
    

    现在创建单例[LGStudent shareInstance];,页面销毁时候不移除属性监听,代码如下

    @implementation LGDetailViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor orangeColor];
        // 单例
        self.student = [LGStudent shareInstance];
        [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        self.student.name = @"hello word";
    }
    #pragma mark - KVO回调
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"LGDetailViewController :%@",change);
    }
    
    - (void)dealloc{
    }
    

    运行单例工程,点击页面发生崩溃

    image.png

    进入一个VC里面有一个单例持有观察VC的属性,哪怕VC被释放 单例没释放继续观察,再次进入VC的时候观察属性发生变化,之前已经被释放的VC会继续接收到消息,因为已经被释放所以野指针崩溃

    小结: 使用KVC,一定要注意添加观察者移除观察者一一对应

    KVO细节分析下

    • 手动观察属性
    @implementation LGPerson
    // 自动开关
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
        return NO;
    }
    - (void)setName:(NSString *)name
    {
        [self willChangeValueForKey:@"name"];
        _name = name;
        [self didChangeValueForKey:@"name"];
    }
    
    //监听name这个属性
    [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    //改变属性值
    self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
    
    • 监听一个属性通过另外两个变量去控制
    @implementation LGPerson
    + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if ([key isEqualToString:@"downloadProgress"]) {
            NSArray *affectingKeys = @[@"totalData", @"writtenData"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    - (NSString *)downloadProgress{
        if (self.writtenData == 0) {
            self.writtenData = 10;
        }
        if (self.totalData == 0) {
            self.totalData = 100;
        }
        return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
    }
    
    //监听downloadProgress这个属性
    [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
    //改变这两个属性
    self.person.writtenData += 10;
    self.person.totalData  += 1;
    
    • 可变数组的观察
    image.png
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    self.person.dateArray = [NSMutableArray array];
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"nb"];
    
    2021-09-04 12:53:15.899991+0800 001---KVO初探[10500:13724184] {
        kind = 1;
        new =     (
        );
    }
    2021-09-04 12:53:15.900726+0800 001---KVO初探[10500:13724184] {
        indexes = "<_NSCachedIndexSet: 0x6000022c0c00>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 2;
        new =     (
            nb
        );
    }
    
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    kind=2的类型是insert
    

    KVO原理上

    KVO的原理描述

    • KVO的自动键值观察是使用isa-swizzling技术实现的;
    • isa指针,顾名思义指向维护调度表的对象的类。这个调度表本质上包含指向类实现的方法的指针,以及其他数据;
    • 当对象的属性注册为观察者时,将会修改被观察对象的isa指针,指向一个中间类而不是真正的类。因此isa指针的值不一定反映实例的实际类;
    • 不应该依赖isa指针来决定类的成员。相反应该使用类方法来确定对象实例的类

    KVO原理分析

    image.png
    • 只对属性观察setter,不能对成员变量进行监听。而属性和成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法
    • 中间类 - self.person -> LGPerson isa 发生了变化 NSKVONotifying_LGPerson (LGPerson 子类)
    • 有什么东西 - 方法 - 属性 setNickName - class - dealloc - _isKVOA
      继承 - 重写 - 实实在在的实现
    • setter 子类 - 父类改变nickName传值
      willchange父类的setter didChange
    • NSKVONotifying_LGPerson是否移除+isa是否会回来 在移除观察的时候

    添加KVO监听的时候动态生成了NSKVONotifying_LGPerson

    image.png image.png

    遍历类以及子类,查看LGPersonNSKVONotifying_LGPerson是什么关系?

    #pragma mark - 遍历类以及子类
    - (void)printClasses:(Class)cls{
        
        // 注册类的总数
        int count = objc_getClassList(NULL, 0);
        // 创建一个数组, 其中包含给定对象
        NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
        // 获取所有已注册的类
        Class* classes = (Class*)malloc(sizeof(Class)*count);
        objc_getClassList(classes, count);
        for (int i = 0; i<count; i++) {
            if (cls == class_getSuperclass(classes[i])) {
                [mArray addObject:classes[i]];
            }
        }
        free(classes);
        NSLog(@"classes = %@", mArray);
    }
    
    image.png

    得出结论 LGPersonNSKVONotifying_LGPerson的父类

    KVO原理下

    • 探索NSKVONotifying_LGPerson类中有哪些方法?
    #pragma mark - 遍历方法-ivar-property
    - (void)printClassAllMethod:(Class)cls{
        unsigned int count = 0;
        Method *methodList = class_copyMethodList(cls, &count);
        for (int i = 0; i<count; i++) {
            Method method = methodList[i];
            SEL sel = method_getName(method);
            IMP imp = class_getMethodImplementation(cls, sel);
            NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
        }
        free(methodList);
    }
    
    // LGViewController.m文件
    @implementation LGViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.person = [[LGPerson alloc] init];
        [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
        [self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
    }
    
    // 运行工程打印如下
    // 重写setNickName方法
    2021-09-04 22:13:31.476760+0800 002---KVO原理探讨[11980:13924110] setNickName:-0x7fff207bab57
    2021-09-04 22:13:31.477058+0800 002---KVO原理探讨[11980:13924110] class-0x7fff207b9662
    2021-09-04 22:13:31.477224+0800 002---KVO原理探讨[11980:13924110] dealloc-0x7fff207b940b
    2021-09-04 22:13:31.477352+0800 002---KVO原理探讨[11980:13924110] _isKVOA-0x7fff207b9403
    

    可以看到自动生成的类NSKVONotifying_Person中,有四个方法,分别是setNickName,class,dealloc,_isKVOA:

    1. setNickName 观察对象的setter方法
    2. class 类型
    3. dealloc 是否释放(该dealloc执行时,将isa重新指向Person)
    4. _isKVOA判断是否是KVO生成的一个辨识码

    NSKVONotifying_Person中的方法是重写了父类的方法

    • 判断当前isa是否指回来?
      image.png

    在注册成为观察者之前,实例对象person的isa指向LGPerson,在注册成为观察者之后,实例对象person的isa指向NSKVONotifying_Person;,移除观察者之后实例对象person的isa又指向LGPerson

    • NSKVONotifying_LGPerson用完是否销毁?
      image.png

    KVO移除之后,生成的NSKVONotifying_LGPerson并不会销毁。

    • 我们测试了属性nickName的修改可以被KVO监听到,那么成员变量是否也能监听到呢?
      LGPerson类添加名为name的成员变量
    @interface LGPerson : NSObject{
        @public
        NSString *name;
    }
    @property (nonatomic, copy) NSString *nickName;
    @end
    
    // 分别为name和nickName都添加KVO监听
    [self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
    [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    image.png

    KVO只能监听属性,不能对成员变量进行监听;而属性和成员变量的区别在于属性比成员变量多一个setter方法,而KVO监听的就是setter方法

    • KVO监听的是setter方法,中间类NSKVONotifying_Person也重写了setter方法,那么我们最终修改的setter方法究竟是NSKVONotifying_Person的还是LGPerson的呢?
    image.png

    可以看到在移除观察者时,isa已经指向了LGPerson,而且nickName的值也改变了,那么此时的setter方法是LGPerson的

    下面我们通过观察变量值改变来验证一下

    image.png

    继续运行项目,触发监听

    image.png

    bt打印堆栈信息


    image.png

    所以最终调用的setterLGPersonsetNickName方法

    小结

    1.isa->LGPerson->NSKVONotifying_LGPerson-消失
    1.1 动态生成 NSKVONotifying_LGPerson
    1.2 LGPerson VS NSKVONotifying_LGPerson 父子关系
    1.3 NSKVONotifying_LGPerson有哪些方法
    setNickName //重写set方法
    class
    dealloc
    _isKVOA
    1.4 isa指回来 //通过这个方法_isKVOA
    1.5 NSKVONotifying_LGPerson并没有销毁
    2.setter方KVO实例法(setter)/class
    2.1 set方法的意义是能够区分成员变量属性变量
    2.2 修改LGPerson属性setNickName方法

    相关文章

      网友评论

          本文标题:KVO分析

          本文链接:https://www.haomeiwen.com/subject/zlplwltx.html