KVO分析

上节研究完KVC后,随之关联的还有一个KVO,本篇就让我们来分析一下KVO的使用以及原理

一、KVO使用

  • KVO通常的使用方法是addObserver forKeyPath
    image.png

    再使用回调函数处理结果
    image.png

    最后再dealloc:移除掉观察者

对于添加观察对象方法:addObserver,可以根据官方文档KVO官方查询相关用法,其中

  • addObserver:观察对象:观察对象首先通过发送广告向被观察对象注册
  • keypath:需要观察的值,只能是被观察对象的属性值
  • options:参数指定为按位或选项常量:即是被观察属性的变化,来影响生成通知的方式。有下面几种方式

NSKeyValueObservingOptionNew:观察属性的新值
NSKeyValueObservingOptionOld:选择从更改之前接收观察到的属性的值
NSKeyValueObservingOptionInitial:可以使用此附加的一次性通知在观察者中建立属性的初始值
NSKeyValueObservingOptionPrior:可以指示观察到的对象在属性更改之前发送通知(除了更改之后的常规通知)。变更字典表示变更前通知,方法是将键NSKeyValueChangeNotificationIsPriorKey与包装为YES的NSNumber的值包含在一起

  • context:上下文,是标记不同对象或者不同属性的作用。因为同一个文件里可能有多个被观察对象,或者一个观察对象有多个属性值被观察,使用 静态变量的地址(ex:static void *PersonNickContext = &PersonNickContext;形式来分辨不同的对象或不同的属性。方便在回调函数中确定对象或者属性来进行后续操作。增加了代码的可读性,可扩展性,安全性
  • addObserver: forKeyPath :options:context:方法不维护对观察对象、观察对象或上下文的强引用,因为是存在弱引用表中
  • dealloc:每次调用addObserver后,都要调用dealloc方法,在其中实现
    移除键值观察器消息,指定观察对象路径上下文:(ex: [self.person removeObserver:self forKeyPath:@"nick" context:NULL]);

注:
1、如果没有注册观察员,则请求将其删除为观察员会导致NSRangeException
2、释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已发布对象的更改通知会触发内存访问异常。因此,你要确保观察者在从内存中消失之前将自己移除。
3、没有提供询问对象是观察者还是被观察者的方法。构造代码以避免相关错误。一种典型的模式是在观察者初始化期间(例如在init或viewdiload中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。

-是否可以 不移除:不可以。否则会崩溃,观察对象没被移除,但是观察者已经被释放了,再次注册时,添加观察器,消息发送后,系统不知道应该由哪个观察器接受。造成指针混乱(
由于第一次注册KVO观察者后没有移除,再次进入界面,会导致第二次注册KVO观察者,导致KVO观察的重复注册,而且第一次的通知对象还在内存中,没有进行释放,此时接收到属性值变化的通知,会出现找不到原有的通知对象,只能找到现有的通知对象,即第二次KVO注册的观察者,所以导致了类似野指针的崩溃,即一直保持着一个野通知,且一直在监听)

image.png
  • 自动/手动接受消息
    默认观察器都是自动接受到属性值变化的消息的。如果想要手动调用,则要关闭自动开关automaticallyNotifiesObserversForKey

    image.png

    并且在属性的set方法中实现willChangeValueForKeydidChangeValueForKey,并在其中间赋值
    image.png

    打开自动开关( 默认)时,猜测系统检测属性值变化,也是调用了willChangeValueForKeydidChangeValueForKey
    测试:
    1、首先在addObserve处打断点,使用watchpoint set variable self->_person->_nick来观察属性值nick(注:watchpoint set variable:观察变量值改变命令)
    image.png

    2、然后点击页面,捕捉到nick的变化
    d

    3、最终调入如下,堆栈显示
    image.png

    结论:属性值变化时,确实是willChangeValueForKeydidChangeValueForKey之间捕捉了nickset方法

  • 覆写keypath:通过覆写keypath来定一个新的观察路径。
    使用案例:
    检测进度:定义一个进度属性:downloadProgress以及相关属性:

    image.png

    覆写keypath
    image.png

    设置初始值
    image.png

    注册观察器
    image.png

    检测keypath变化
    image.png

  • 检测数组变化
    定义一个可变数组:dateArray,点击页面时赋值,发现,在回调方法不走。这是为什么呢?

    image.png

找到文档里关于观察数组时的要求:观察数组要按照kcv的形式赋值,才能发送更改消息
那么将数组按照kvc形式赋值,更改


image.png

结果收到了更改的消息,且类型kind为2,是NSKeyValueChange的值,查到NSKeyValueChange定义,有如下四种值的改变方式

image.png

二、KVO底层
都知道,KVO只能观察属性,不能观察成员变量,这个也有在代码里验证过,只能是属性可以被观察,这是为什么呢,属性和成员变量的区别就在于,多了setget方法。说明,是kvo
只能观察set方法,捕捉到了值的变化,下面让我们来验证

  • 根据文档,被观察对象的isa会指向一个中间类
    这个中间类是什么,又是在什么时节生成的?
    观察生成中间类时机,测试一下:
    image.png

    果然,调用完addObserver后,生成了一个中间类,也可以叫做派生类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);
}

顺便添加一下打印类名的方法

#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

发现派生类实际上是当前类的子类,且重新生成set方法

注:为什么方法继承:因为继承的话,不会在子类中显示,只在父类中,这一点也可以添加LGPerson类的一个子类再打印一次方法列表试试,对比结果,也可以印证这一点。

  • 何时isa再指回原类,猜测是在dealloc里,移步到dealloc
    测试:
    image.png

确实调用完removeObserver后,isa再指回原类了。

  • 🤔️:派生类已经移除了么?
    我们到dealloc里处理打断点,移除观察者后,再调用获取类和子类的方法,发现派生类还存在。ps:测试页面销毁后,再次获取person类及其子类,还是同样的结果。

image.png

派生类根本就不移除了:因为KVO派生类只要生成,就会一直存在,这样可以减少频繁的添加操作

至此,整个KVO原理大致流程明白了:创建派生类实现了键值观察。

添加:addObserver时,创建了派生类,派生类是当前类的子类重写了被监听属性的setter方法,并将当前类的isa指向了派生类。(此时开始,所有调用本类的方法,都是调用的派生类。派生类中没有的方法,就会沿着继承链查询到本类)

改变属性值: 派生类重写了被监听属性的setter方法,在派生类的setter方法触发时:在willChange之后didChange之前,调用父类属性setter方法,完成父类属性的赋值。

移除: 在removeObserver后,isa派生类指回本类。 但创建过的派生类会被本类从子类列表中移除,会一直存在。

假象: 外部打印class永远看不到派生类,是因为派生类将class方法重写了,故意不让外界看到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354