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);


-
将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));
}

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


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

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


正确的写法为.super_class = [self class]LGKVONotifying_LGPerson的父类LGPerson中查找setNickName:方法

- 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);

-
通知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.移除观察者

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





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

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

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


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


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

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


2.监听原理

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

重写setter方法[r overrideSetterFor: aPath]
添加观察者
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];

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

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

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

GNU源码有助于理解Foudation框架
问题一:LGPerson中实现-(void)dealloc方法后,和自定义添加的-(void)lg_setter方法,在LGPerson销毁的时候会走哪一个方法呢???
前文运行发现会走-(void)lg_setter方法,为什么呢?
self.person对象的地址不会发生变化,添加观察者后只是产生了一个动态子类,只是isa改变的一个类型,外部的实例对象地址是不变的,对原来的类LGPerson的-(void)dealloc方法进行了重写和覆盖,并不会再进入LGPerson的-(void)dealloc方法了

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


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


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



_FBKVOInfo中有重写hash和isEqual:方法,新建的临时变量的keyPath是相等的,能从_objectInfosMap的infos中找到info,从而进行移除

网友评论