iOS-KVO

作者: xiaofu666 | 来源:发表于2022-03-20 22:54 被阅读0次

iOS-KVC中我们讲述了KVC的官方文档,从KVC的文档中引出KVO文档
因为有了KVO,才让OC有了响应式编程方式

定义KVO

定义3个ViewController

  • 第一个页面只负责跳转;
  • 第二个页面进行对象的监听;
  • 第三个页面中也不做处理

其中第二个ViewController中的代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.person = [LGPerson new];
    self.student = [LGStudent shareInstance];
    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.name = [NSString stringWithFormat:@"%@+", self.person.name];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"name" context:NULL];
}

这样,我们就做了一个对LGPersonname做了个监听。查看打印内容

image

到这里,相信没有人不懂的,那么下面的方法的参数具体是什么呢

// observer: 观察谁,弱引用添加
// keyPath:观察什么
// options:观察什么的变化,枚举值 - 新值、旧值
// context:上下文 为了进行观察这区分,解决问题,可以在监听回调方法中减少判断,代码可读性会降低
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context

根据自己的理解再查看官方文档。


image

发现我们的猜测是相同的

remove的作用

我们都知道,在做完监听时,需要将监听移除,不移除会出现什么问题呢?
(有人说在iOS 10之后系统做了优化,无需移除监听,通过实测可知,系统并没有做优化,但是当监听的是当前对象的属性的某个属性时,不会出现崩溃问题,而监听的是某个单例的属性时就会出现)
查看官方文档可知


image

我们也可以从中知晓,当没有添加观察者,但却进行了移除观察者时,会导致崩溃,崩溃信息为:


image

查看文档后,为了保证安全,我们可以将remove代码写在try中,如下:

    @try {
        [self.person removeObserver:self forKeyPath:@"name" context:NULL];
    } @catch (NSException *exception) {
        NSLog(@"exception = %@", exception);
    } @finally {
        
    }

这样可以保证不会崩溃。

手动添加观察以及自动添加观察

使用automaticallyNotifiesObserversForKey:方法可以实现对当前对象的某个属性的自动观察做开关处理。
我们在LGPerson.m文件中做如下处理:

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

运行代码后,查看可知,现在已经无法进行观察了。说明已经取消了自动观察。
那么在取消自动观察的时候,应该怎么做才能让他继续观察呢?
我们将上方对name的观察修改为对nick的观察,并在LGPerson中重写setNick的方法,代码如下:

- (void)setNick:(NSString *)nick{
    [self willChangeValueForKey:@"nick"];
    _nick = nick;
    [self didChangeValueForKey:@"nick"];
}

运行可知,又可以对nick进行观察了,但对name的观察仍然是取消状态。

路径处理

比如我们监听下载的进度时,可知下载的进度 = 已下载量 / 总下载量,可我们又不想监听两个量,这个时候可以使用keyPathsForValuesAffectingValueForKey方法进行观察处理。代码如下:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        // 数组中的两个值为真正需要监听的两个属性名。
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
// 重写 downloadProgress的getter方法用于取进度值
- (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];
}

将手动监听打开,运行后结果:


image

数组观察

数组的观察可能用到的不多,但是确实是可用,我们先做如下操作

    // 在 viewDidLoad中 添加 dataArray 数组的监听
    self.person.dateArray = [NSMutableArray array];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
    
    // 在点击事件内 对数组进行修改
    [self.person.dateArray addObject:@"1"];
    
    // 在 dealloc中 对数组监听进行移除
    [self.person removeObserver:self forKeyPath:@"dateArray"];

观察上述代码是否有问题,思考一会儿。
我们来运行代码,查看结果,可知并没有进入监听的回调方法中,但打印dateArray数组发现该数组确实变化了。


image

上节课在KVC中我们可知,数组中的键值与普通属性不同,那么KVO应该也不同,查看官方文档。


image

根据官方文档做如下处理,将修改数组的位置修改为

[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

运行代码可知可以进行监听数组了


image

从打印信息可知,数组的监听与普通的属性的打印信息不同,即打印中的kind的值。
查看kind的值,如下:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1, // 设置
    NSKeyValueChangeInsertion = 2, // 插入
    NSKeyValueChangeRemoval = 3, // 移除
    NSKeyValueChangeReplacement = 4, // 替换
};

上方的2对应数组的addObject等操作,3对应数组的removeObject等操作,4对应数组的replaceObjectAtIndex:withObject:的操作

KVO底层原理探索

观察的是什么?

重新定义LGPerson类,自定义成员变量name与属性nickName,源码如下:

@interface LGPerson : NSObject{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end

ViewController中做namenickName的监听等操作,查看打印。

image

通过打印结果可知,KVO只对属性进行监听,对成员变量不监听. 属性与成员变量的区别在于属性存在 setter方法,而成员变量没有setter

中间类

self.personisa的指向发生了变化

image

通过断点打印可知,在添加监听之后,self.personisa重新指向了NSKVONotifying_LGPerson类,对比之前的类多了NSKVONotifying_前缀。
派生类:即某个类的子类
自定义方法,查看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);
}

并在self.person添加监听的前后进行调用该方法,查看打印结果。

image

其中LGStudentLGPerson的一个子类,在添加监听方法后发现LGPerson多了一个子类NSKVONotifying_LGPerson,说明添加监听方法后,self.personisa指向了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);
}

调用该方法

[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];

查看打印结果。


image

从打印结果可知,在NSKVONotifying_LGPerson类中添加了四个方法,分别为:setNickNameclassdealloc_isKVOA这四个方法。

  • _isKVOA 判断当前是否为KVO类
  • dealloc 释放
  • setNickName 需要查看是继承还是重写的,答案重写的方法
  • class

判断setNickName是继承的还是重写的

通过打印另一个继承类LGStudent类进行。


image

发现LGStudent类中什么都没有,那么我们在LGStudent中重写一下setNickName方法,再次打印查看结果。

image

这个时候发现LGStudent中存在setNickName方法,由此可知NSKVONotifying_LGPerson中的setNickName为重写的方法

isa是否会还原

NSKVONotifying_LGPerson类移除后,self.personisa是否会指回来。
ViewController中的dealloc方法中添加断点,查看移除之后的self.personisa的指向。

image

通过打印可知,在移除监听后,self.personisa会重新指向LGPerson
popViewController也做LGPerson的子类的打印,结果如下:

image

发现当监听被移除后,NSKVONotifying_LGPerson类并没有被移除,而是仍然存在。
结论

  • 对象的isa会重新指向
  • 动态生成的类会一直存在

重写的setter方法内做了什么处理

猜测一下,首先,在调用NSKVONotifying_LGPerson重写setter方法的时候,改变的是其父类LGPersonnickName的值,那么在重写的setter方法中一定有对父类nickName进行传值的操作。
设置观察self->_pserson->_nickName,具体命令为:

watchpoint set variable self->_person->_nickName
image

执行命令后的结果。进入断点。


image

查看左侧堆栈,可知2、3、4步是隐藏逻辑为汇编语言


image

堆栈编号1为LGPersonsetter方法

image

堆栈编号2在断点前后分别执行NSKeyValueWillChange方法以及NSKeyValueDidChange方法。

image

willChangedidChange之间执行了父类的赋值方法。

结论

willChangedidChange之间调用父类的赋值方法,因此,父类的值得以改变。

优秀三方框架
FBKVOController

相关文章

  • iOS-KVO(二) 使用注意点

    iOS-KVO(一) 基本操作iOS-KVO(二) 使用注意点iOS-KVO(三) 窥探底层实现iOS-KVO(四...

  • iOS-KVO(三) 窥探底层实现

    iOS-KVO(一) 基本操作iOS-KVO(二) 使用注意点iOS-KVO(三) 窥探底层实现iOS-KVO(四...

  • iOS-KVO(四) 自定义KVO+Block

    iOS-KVO(一) 基本操作iOS-KVO(二) 使用注意点iOS-KVO(三) 窥探底层实现iOS-KVO(四...

  • iOS-KVO(一) 基本操作

    iOS-KVO(一) 基本操作iOS-KVO(二) 使用注意点iOS-KVO(三) 窥探底层实现iOS-KVO(四...

  • iOS-KVO

    一、VKO 简述 KVO 全称 Key Value Observing,俗称“键值监听”;可以监听对象某个属性值的...

  • iOS-KVO

    简介 先来看看官方的定义: Key-value coding is a mechanism enabled by ...

  • iOS-KVO

    一.kvo使用 kvo可以监听一个对象属性的变化,下面为简单使用. 二.使用runtime分析kvo 我写了个简单...

  • iOS-KVO

    KVO(Key-Value observing):是苹果提供的一套事件通知机制.允许对象监听另一个对象特定属性的改...

  • iOS-KVO

    iOS-KVC[https://www.jianshu.com/p/138e332b0656]中我们讲述了KVC的...

  • iOS-自定义KVO

    前言 iOS-KVO原理分析[https://www.jianshu.com/p/f94a972f6187]这篇文...

网友评论

      本文标题:iOS-KVO

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