KVO 底层原理

845 阅读9分钟

iOS 底层原理 文章汇总

  • 观察者中的context上下文参数可以防止重名(多个对象观察的同名属性区分),性能,代码可读性,安全
  • 观察者在dealloc方法中要移除,若不移除,程序将会奔溃。
[self.student addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
- (void)dealloc{
    [self.student removeObserver:self forKeyPath:@"name" context:NULL];
}
  • 单例对象的属性观察者,在两个Controller中都对同一个属性name进行观察,若没有remove掉,会引起野指针,无法判定是哪一个观察者而崩溃 [self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];中并不会产生循环引用,在底层添加的属性observer是weak保存的self

单例对象内存中一直存在,name属性为一份,当name值发生变化时,两个观察者都会收到通知,并不清楚需要哪个对象处理,属于野通知。

KVO的初体验

KVO的步骤:

  • 添加观察
  • observe回调
  • 在合适位置更改观察属性的值
  • dealloc里移除观察
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person  = [LGPerson new];
    [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.nick = [NSString stringWithFormat:@"%@+",self.person.nick];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"%@",change);
}
- (void)dealloc{
    [self.person removeObserver:self forKeyPath:@"dateArray"];
}

KVO的其他用法

1、切换手动与自动

  • 自动开关 (默认)
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return YES;
}
  • 手动开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

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

2、路径的处理

@implementation LGPerson
// 下载进度 -- writtenData/totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"downloadProgress"]) {
        NSArray *affectingKeys = @[@"totalData", @"writtenData"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
- (NSString *)downloadProgress{
    if (self.writtenData == 0) {
        self.writtenData = 1.0;
    }
    if (self.totalData == 0) {
        self.totalData = 100;
    }
    return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
@end

@implementation LGViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person  = [LGPerson new];
    // 4: 路径处理
    // 下载的进度 = 已下载 / 总下载
   [self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
}
@end

3、数组属性的监听

  • 对于集合类型,属于键值观察,基于KVC,不能直接添加元素,需要将数组mutableArrayValueForKey保存
@interface LGPerson : NSObject
@property (nonatomic, strong) NSMutableArray *dateArray;
@end

@implementation LGViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.person  = [LGPerson new];
    // 1: 数组观察
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // KVC 集合 array
    [[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];
}
/* Possible values in the NSKeyValueChangeKindKey entry in change dictionaries. See the comments for -observeValueForKeyPath:ofObject:change:context: for more information.
*/
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1, 属性
    NSKeyValueChangeInsertion = 2, 集合类型
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};

KVO的原理分析

KVO只能观察属性,属性有setter方法,不能观察成员变量

  • 如下点击屏幕只输出属性变化,成员变量没有观察到变化

image.png

KVO形成中间类

  • 原来的person对象的isa指向了LGPerson类,生成中间类后,isa不再指向LGPerson类,isa指向发生了变化,添加观察者后,person对象的isa指向派生中间类NSKVONotifying_LGPerson
  • 通过object_getClassName(self.person)可以获取当前isa的情况,打印如下

image.png

  • nickName添加addObserver前后isa指向类发生变化

查看添加观察者前后的子类情况 先遍历类及子类查看情况

image.png

打印添加addObserver前后子类变化

image.png

中间类NSKVONotifying_LGPerson中有什么东西呢?我们通过遍历方法-ivar-property

image.png

打印结果:

image.png NSKVONotifying_LGPerson中的所有方法:setNickName:,class,dealloc(释放监听),_isKVOA(是否是KVO)

setNickName:方法属于继承自LGPerson方法还是重写呢?

  • 新建LGPerson的子类LGStudent,查看LGStudent中的方法,若LGStudent也有上述NSKVONotifying_LGPerson中的所有方法(setNickName:,class,dealloc(释放监听),_isKVOA(是否是KVO)),则表明NSKVONotifying_LGPerson中的所有方法来自于继承

image.png

打印发现LGStudent中没有任何方法,故NSKVONotifying_LGPerson中的所有方法都来自重写

这几个方法都来自于重写,在LGStudent中重写setName:可以验证,打印出LGStudent中的setName:方法

image.png

添加观察者后isa指向中间类NSKVONotifying_LGPerson,什么时候isa指回来LGPerson呢?是在移除观察者的时候吗?查看dealloc析构函数中移除观察者前后指针指向。

image.png

  • 移除观察者后,继续遍历LGPerson类以及子类

image.png

打印结果:

image.png

移除观察者后发现NSKVONotifying_LGPerson注册到内存中后会一直存在,防止之后程序继续添加观察者,再重复开辟NSKVONotifying_LGPerson内存空间,浪费性能

  • 研究下是子类 setter ,还是 父类setter改变 nickName 值 设置观察点watchpoint set variable self->_person->_nickName

image.png

通过设置观察点发现setNickName:在willChange和didChange之间

image.png

进入了父类的setNickName:方法,从中间类NSKVONotifying_LGPerson中调用了[super setNickName:]方法,所以是父类改变了nickName的值

image.png

自定义KVO

实现 addObserver

image.png

验证是否存在setter方法

  • 不让实例成员变量进来,若观察成员变量name,则会报错

image.png

动态生成子类

image.png

  • 添加方法时不能添加动态成员变量,成员变量存在于ro中,ivar在read_images中就初始化和分配好了ivar空间,存在于ro,clean memory,不能再进行添加了。
  • 方法和属性添加在rwe中,dirty memory,可以进行添加。

指向isa

image.png

属性值变化后通知到自定义方法中

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

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方法

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

image.png image.png

这个注意一点: 下面两段代码中object_getClass(self)替换成[self class]会报错

struct objc_super superStruct = {
   .receiver = self,
   .super_class = class_getSuperclass(object_getClass(self)),
};
Class lg_class(id self,SEL _cmd){
    return class_getSuperclass(object_getClass(self));
}
  • objc_msgSendSuper消息发送的好处:通用,封装,解决了依赖,并不依赖于类LGPerson或NSObject,都不用关注这个类是NSObject还是LGPerson

image.png

  • 通知VC中的方法,属性值发生了变化,先要保存VC,在分类中保存VC,用到runtime关联对象,保存观察者VC控制器,setNickName:中收到nickName的值变化后,发送消息通知观察者控制器VC

image.png

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

4193251-e10c49bb39f4ad17.png

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

  • 创建一个LGKVOInfo来保存观察信息类,保存观察者信息

image.pngimage.png

  • 在分类中修改保存观察者信息逻辑

image.png image.png

移除观察者

image.png

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

自定义函数式KVO

image.png image.png

添加属性观察的位置进行回调 image.png image.png

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方法
  • 可以在添加观察者的时候进行方法交换从而改变LGPersonisa指向,此刻会循环递归调用,程序崩溃

image.png

  • 循环递归调用的原因: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的销毁,从而移除自定义观察者,但是又会有别的问题,方法交换的方式就不怎么好,有太多问题了,容易造成系统的混乱

image.png image.png

FBKVOController源码探索

监听原理

4193251-726324aaaa407f54.png

结构图

image.png

  • 不需要手动移除观察者;框架自动帮我们移除观察者
  • 实现 KVO 与事件发生处的代码上下文相同,不需要跨方法传参数;
  • 使用 block 来替代方法能够减少使用的复杂度,提升使用 KVO 的体验;block或者selector的方式,方便使用
  • 每一个 keyPath 会对应一个属性,不需要在 block 中使用 if 判断 keyPath;一个- keyPath对应一个SEL或者block,不需要统一的observeValueForKeyPath方法里写if判断
  • 实现了观察者主动去添加被观察者,实现了角色上的反转,其实就是用的比较方便。

NSObject分类和KVOController的初始化

给NSObject添加一个FBKVOController分类,用关联对象动态添加属性。 image.png

KVOController介绍

在KVOController.h文件,发现三个类:FBKVOInfoFBKVOSharedControllerFBKVOController

  • 其中FBKVOInfo主要是对需要观测的信息的包装,包含了action、block、options等等,改类中重写了hash,isEqual等方法。_FBKVOInfo覆写了-isEqual:方法用于对象之间的判等以及方便 NSMapTable 的存储。
@implementation _FBKVOInfo
{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}
  • FBKVOController是核心类,包含MapTablepthread_mutex_lock,其中_objectInfosMap是存储一个对象对应的KVOInfo的映射关系,也就是说这里<id, NSMutableSet<_FBKVOInfo *> *> 中的id就是对象,MutableSet就是KVOInfos,各种键值观测的包装。
@implementation FBKVOController
{
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  pthread_mutex_t _lock;
}

每个被观测者对象和KVOInfo的关系

image.png

  • FBKVOSharedController是一个实际操作类,负责将FBKVOController发送过来的信息转发给系统的KVO处理。这里实现了KVO的观测和remove。
@implementation _FBKVOSharedController
{
  NSHashTable<_FBKVOInfo *> *_infos;
  pthread_mutex_t _mutex;
}

循环引用的分析

VC1持有_kvoCtrl,_kvoCtrl持有一个_objectInfosMap,这是一个可以存放弱指针的NSDictionary,这个函数[_objectInfosMap setObject:infos forKey:object];就是将object和其需要监听的info加入map中。故VC1持有KVOCtrl,KVOCtrl持有map,map持有VC2,*也就是VC1持有VC2。这是要如果我们VC2里观测VC1,就会VC2持有VC1,造成循环引用。所以_FBKVOInfo中用weak修饰下FBKVOController为了防止强引用

image.png