iOS底层探索(十六) KVO

iOS底层探索(十六) KVO

上一篇iOS底层探索(十五) KVC我们讲述了KVC的官方文档,从KVC的文档中引出KVO文档
因为有了KVO,才让OC有了响应式编程方式

定义KVO

定义3个ViewController,第一个页面只负责跳转,第二个页面进行对象的监听,第三个页面中也不做处理
第二个ViewController中的代码如下:
x

- (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做了个监听。查看打印内容
在这里插入图片描述

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

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

根据自己的理解再查看官方文档。
在这里插入图片描述

发现我们的猜测是相同的

remove的作用

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

我们也可以从中知晓,当没有添加观察者,但却进行了移除观察者时,会导致崩溃,崩溃信息为:
在这里插入图片描述

查看文档后,为了保证安全,我们可以将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 false;
}

运行代码后,查看可知,现在已经无法进行观察了。说明已经取消了自动观察。
那么在取消自动观察的时候,应该怎么做才能让他继续观察呢?
我们将上方对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];
}

将手动监听打开,运行后结果:
在这里插入图片描述

数组观察

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

    // 在 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数组发现该数组确实变化了。
在这里插入图片描述

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

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

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

运行代码可知可以进行监听数组了
在这里插入图片描述

从打印信息可知,数组的监听与普通的属性的打印信息不同,即打印中的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的监听等操作,查看打印。
在这里插入图片描述

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

中间类

self.personisa的指向发生了变化
在这里插入图片描述

通过断点打印可知,在添加监听之后,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添加监听的前后进行调用该方法,查看打印结果。
在这里插入图片描述

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

查看打印结果。
在这里插入图片描述

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

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

通过打印另一个继承类LGStudent类进行。
在这里插入图片描述

发现LGStudent类中什么都没有,那么我们在LGStudent中重写一下setNickName方法,再次打印查看结果。
在这里插入图片描述

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

isa是否会还原

NSKVONotifying_LGPerson类是否会移除,self.person的isa是否会指回来。
ViewController中的dealloc方法中添加断点,查看移除之后的self.personisa的指向。
在这里插入图片描述

通过打印可知,在移除监听后,self.person的isa会重新指向LGPerson
popViewController也做LGPerson的子类的打印,结果如下:
在这里插入图片描述

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

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

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

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

watchpoint set variable self->_person->_nickName

在这里插入图片描述

执行命令后的结果。进入断点。
在这里插入图片描述

查看左侧堆栈,可知2、3、4步是隐藏逻辑为汇编语言
在这里插入图片描述

  • 堆栈编号1为LGPerson的setter方法
    在这里插入图片描述

  • 堆栈编号2在断点前后分别执行NSKeyValueWillChange方法以及NSKeyValueDidChange方法。
    在这里插入图片描述

在这里插入图片描述

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

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

自定义KVO

在弄懂KVO的原理后,使用自定义的方式进行验证。
Demo

优秀三方框架

FBKVOController

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值