iOS-底层原理21-KVO(下)

iOS-底层原理21-KVO(下)

《iOS底层原理文章汇总》
上一篇文章《iOS-底层原理20-KVO(上)》介绍到自定义KVO中观察到属性的值发生变化后,怎么通知到自定义的方法中

1.自定义KVO,属性值变化后通知到自定义方法中

来到lg_setter方法中之后,改变父类中nickName的值,进行消息发送,往父类发送消息setNickName:,重定义objc_msgSendSuper,传入三个参数objc_super父类结构体指针,_cmd(setNickName:)方法,newValue新改变的值,进入自定义子类LGKVONotifying_LGPerson的父类LGPerson的setNickName:方法中

    // 4: 消息转发 : 转发给父类
    // 改变父类的值 --- 可以强制类型转换
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self)),
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
image.png

image.png
  • 将llvm严格校验参数个数关闭,解决objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)方法报错


    image.png
  • 此时存在一个问题,系统的观察者在观察前后一直self.person.class一致指向LGPerson,而此时自定义的观察者self.person.class指向LGKVONotifying_LGPerson,此时需要重写class方法


    image.png

    image.png

    重写class方法:class_getSuperclass(object_getClass(self))


    image.png
Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
image.png

下面这种写法会死循环:class_getSuperclass([self class])中[self class]还会调用lg_class方法


image.png

image.png

此时self.person.class的指向才和系统观察者一模一样了


image.png

此时执行程序发现无法找到setNickName:方法,为什么呢?分析原因,取了两次父类.super_class = class_getSuperclass([self class])中[self class]调用lg_class方法,class_getSuperclass(object_getClass(self))又取一次父类,即LGKVONotifying_LGPerson的父类的父类,并不存在setNickName:所以报unrecognized selector
image.png

image.png

正确的写法为.super_class = [self class]LGKVONotifying_LGPerson的父类LGPerson中查找setNickName:方法
image.png
  • objc_msgSendSuper消息发送的好处:通用,封装,解决了依赖,并不依赖于类LGPerson或NSObject,都不用关注这个类是NSObject还是LGPerson
    void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
    // void /* struct objc_super *super, SEL op, ... */
    struct objc_super superStruct = {
        .receiver = self,
        .super_class = [self class],
    };
    //objc_msgSendSuper(&superStruct,_cmd,newValue)
    lg_msgSendSuper(&superStruct,_cmd,newValue);
image.png
  • 通知VC中的方法,属性值发生了变化,先要保存VC,在分类中保存VC,用到runtime关联对象,保存观察者VC控制器,setNickName:中收到nickName的值变化后,发送消息通知观察者控制器VC


    image.png

    image.png
  • 添加关联对象是否产生循环引用了?没有,因为关联对象是存放在一个哈希表中,并没有进行持有,一一对应关系的保存,控制器VCpop后还是能正常进入dealloc方法,并没有产生循环引用


    AssociationsHashMap@2x.png

    image.png

    image.png

2.若要观察多个属性呢,以上会保存多次观察者VC,这里需要优化。

  • 1.新建LGKVOInfo保存观察信息类,保存观察者信息


    image.png

    image.png
  • 2.观察一个属性的情况
    I.添加观察者,观察属性nickName变化


    image.png

    II.保存观察者信息,方便第三步进行消息发送到观察者控制器


    image.png

    III.对新旧值进行处理,发送给观察者
    image.png
  • 3.观察多个属性值的情况,同事观察nickName和kcName


    image.png

    image.png

    image.png

    观察两个属性,observerArr中存在两个info


    image.png

3.移除观察者

image.png

以上自定义的KVO是通过传值的方法,观察到值的变化后,在不同的方法中进行通知,有没有函数式的方式y=f(x)的方式,和添加观察者耦合再一起呢?

4.自定义函数式KVO

image.png
image.png
image.png
image.png
image.png

5.KVO自动销毁机制

在VC的dealloc方法中调用移除观察者方法[self.person lg_removeObserver:self forKeyPath:@"nickName"],怎么对观察者进行自动移除呢?在什么时候自动移除呢?VC进行销毁的时候,会调用dealloc方法,VC销毁,则持有的属性LGPerson必定销毁了,在VC的dealloc()方法中会给LGPerson发送release消息[self.person release],我们可以监听self.person是什么时候销毁的,即观察LGPerson是什么时候析构(走入-(void)dealloc方法)的,要想将自定义观察者自动移除,我们想到一种办法是监听LGPerson的析构函数-(void)dealloc,并在析构函数中将LGPerson的isa由LGKVONotifying_LGPerson指回LGPerson

image.png

我们不能在NSObject分类中直接重写-(void)dealloc方法进行监听,会改变所有NSObject子类的-(void)dealloc方法,可以在添加观察者的时候进行方法交换从而改变LGPerson的isa指向,此刻会循环递归调用,程序崩溃
image.png

循环递归调用的原因是LGPerson中没有实现dealloc方法,则会取LGPerson的父类NSObject也就是系统中的-(void)dealloc方法进行交换,NSObject中的dealloc方法就被替换为了NSObject+LGKVO分类中myDealloc方法的实现,-(void)myDealloc方法被替换为了NSObject中的dealloc方法的实现,但是其他NSObject的子类并不知道系统的dealloc方法已经被替换为myDealloc方法了,所以程序会在走[self myDealloc]中一直循环调用,自己调用自己。
image.png

image.png

解决办法在LGPerson中实现要被交换的-(void)dealloc方法,程序不会崩溃,LGViewController控制器pop后,里面的属性LGPerson也正常销毁,进入-(void)dealloc方法,可以通过交换方法监听-(void)dealloc方法从而监听到VC的销毁,从而移除自定义观察者,但是又会有别的问题,方法交换的方式就不怎么好,有太多问题了,容易造成系统的混乱
image.png

image.png

由前文知道KVO会生成派生子类LGKVONotifying_LGPerson,重写四个方法class ,setter,dealloc,isKVO,因此当观察者被移除的时候,可以在dealloc方法中将LGPerson对象中的isa指回LGPerson,在VC销毁时进入vc的dealloc方法,后进入LGKVONotifying_LGPerson的dealloc也就是lg_dealloc方法来将isa指回LGPerson


image.png

6.FBKVOController源码探索

1.保存信息_FBKVOInfo中用weak修饰下FBKVOController为了防止强引用,循环引用链条为self -> kvoCtrl -> _objectInfosMap -> infos - > info -/弱引用__weak-> self.kvoCtrl


image.png

image.png

2.监听原理


image.png

7.GNU源码分析:系统的KVO的底层代码就在此

image.png

重写setter方法[r overrideSetterFor: aPath]
添加观察者

[info addObserver: anObserver
             forKeyPath: aPath
                options: options
                context: aContext];
image.png

观察的值发生改变走setter方法,消息发送给父类,判断自动开关是否打开


image.png

发送响应通知在didChangeValueForKey方法中,对change进行处理,拿到info信息进行通知


image.png

change进行setObject,oldValue和newValue进行处理,将所有消息处理完,向外界发送消息,使外界都能接收到消息,oldValue和newValue分别进行release,整个过程更加符合苹果写的系统的原生集的KVO的处理。
image.png

GNU源码有助于理解Foudation框架

问题一:LGPerson中实现-(void)dealloc方法后,和自定义添加的-(void)lg_setter方法,在LGPerson销毁的时候会走哪一个方法呢???

前文运行发现会走-(void)lg_setter方法,为什么呢?
self.person对象的地址不会发生变化,添加观察者后只是产生了一个动态子类,只是isa改变的一个类型,外部的实例对象地址是不变的,对原来的类LGPerson的-(void)dealloc方法进行了重写和覆盖,并不会再进入LGPerson的-(void)dealloc方法了


image.png

问题二:FBKVOController在进行移除观察者的时候,新建的_FBKVOInfo *info是怎么准确的移除的?很明显临时变量info在infos中是找不到的,registeredInfo为nil,所以最后[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo]将移除个锤子,添加时是临时创建的info,移除时也是临时创建的info,是不同的一个

image.png

image.png

例如两个person是不同的内存地址,就不相等,info为空,查看NSSet中方法member:官方源码,对象被认为相等,如果isEqual:中的条件是相等的返回YES,则认为对象是相等的


image.png

image.png

重写hash和isEqual:方法来判断对象是否相等,hash匹配的是地址指针的值,两个对象相等哈希值也要相等


image.png

image.png

image.png

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

推荐阅读更多精彩内容