[iOS] KVO底层原理

1. KVO

KVO(Key-Value Observing),即键值观察,是一种机制,允许注册成为其他对象的观察者,当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。

在日常开发中,我们使用 KVO 来监听对象属性的变化,并做出响应,现在我们来看下KVO的底层实现。

2. KVO 的基本使用

基本使用可以分为以下 3 步:

  • 1.注册观察者 addObserver:forKeyPath:options:context
    一般最后的context 属性,一般用来区分回调来源,当然也可以使用keyPath

  • 2.实现 KVO 回调 observeValueForKeyPath:ofObject:change:context

  • 3.移除观察者 removeObserver:forKeyPath:context
    移除观察者调用方法是必须的,和注册观察者方法成对出现。

2.1 案例一:基本使用

首先有一个Person 类,只有一个name属性,代码如下:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Person


@end

ViewController 中使用:

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person = [Person alloc];
    // 注册 self 也就是 controller 为自己的观察者
    [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    _person.name = @"哈哈";
}

// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"%@ - %@ - %@",keyPath,object,change);
}

// 移除观察者
- (void)dealloc{
    [_person removeObserver:self forKeyPath:@"name"];
}

@end

这样点击屏幕,控制台就会有以下输出:

2021-01-18 15:59:15.571749+0800 KVO[3322:245856] name -  - {
    kind = 1;
    new = "\U54c8\U54c8";
    old = "";
}

其中的 kind 表示键值变化的类型,是一个枚举:

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,//设值
    NSKeyValueChangeInsertion = 2,//插入
    NSKeyValueChangeRemoval = 3,//移除
    NSKeyValueChangeReplacement = 4,//替换
};
2.2 案例二 手动触发 observer 回调

像案例一中那样,我们改变了name 的值,就自动触发了observer回调,但是有时候我们并不一定每一次都通知,比如满足某一个条件的时候,才想着通知一下。

  • 我们先在 Person 中添加一个方法,关闭自动触发:
// 对所有的都关闭
//+ (BOOL)automaticallyNotifiesObserversOfName{
//    return NO;
//}

// 可以对某个指定key 关闭
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if([key isEqualToString:@"name"]){
        NSLog(@"关闭了自动触发");
        return NO;
    }
    return YES;
}
  • person.name 赋值的地方添加上手动触发的代码:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // name 的值即将改变
    [_person willChangeValueForKey:@"name"];
    _person.name = @"哈哈";
    // name的值改变完成
    [_person didChangeValueForKey:@"name"];
}
2.3 案例三 观察属性.属性的变化

我们再创建一个Dog类,并且Person 类中也添加一个Dog 对象:

@interface Dog : NSObject
@property(nonatomic,assign)int age;
@end

#import "Dog.h"
@implementation Dog
@end

我们想要观察 person.dogage 的属性变化,此时需要修改添加观察者的代码,keyPath 改成dog.age即可:

[_person addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];

2.4 案例四 注册一个KVO观察者,可以监听多个属性的变化

我们在 Person 类中再添加两个属性:firstNamelastName,它们两个是name的组成,当给name 添加通知的时候,当 firstNamelastName 任意一个变化的时候,都要收到通知:

  • Person 中加入类方法+keyPathsForValuesAffectingValueForKey,返回一个容器:
+ (NSSet *)keyPathsForValuesAffectingName{
    NSSet *keyPaths = [NSSet setWithArray:@[@"firstName",@"lastName"]];
    return keyPaths;
}

注册观察personname 属性,这样就可以在 firstNamelastName 变化时,也收到相应的回调了。

2.5 案例五 可变数组

我们给Person 添加一个可变数组属性,并观察这个可变数组,当向这个可变数组添加数据时,是不会调用setter 方法的,也不会触发 KVO的回调,代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person = [Person alloc];
    // sons是一个可变数组
    _person.sons = [@[] mutableCopy];
    // 观察 sons 的变化
    [_person addObserver:self forKeyPath:@"sons" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // sons添加元素
    [_person.sons addObject:@"lili"];
}

直接通过[_person.sons addObject:@"lili"];这样是无法触发KVO 回调的,针对于可变数组的集合类型,需要通过mutableArrayValueForKey方法将元素添加到可变数组中,才能触发 KVO 回调,将添加元素的代码改为如下代码即可:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [[_person mutableArrayValueForKey:@"sons"] addObject:@"lili"];
}

输出如下,可以看到收到相应的回调了:

2021-01-18 16:35:05.850405+0800 001-alloc&init探索[3865:311198] sons -  - {
    indexes = "<_NSCachedIndexSet: 0x600000041020>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
    kind = 2;
    new =     (
        lili
    );
}

3. KVO 底层探索

KVO 的官方文档中,有如下说明:

image.png

  • KVO 是通过 isa-swizzling的技术实现的
  • 当为对象的属性添加观察者时,会修改观察对象的isa 指针,指向一个中间类,此时 isa 指针的值不一定反映对象的实际类
  • 不应依靠isa指针来确定类成员身份,应该使用class方法来确定对象实例的类
3.1 验证 KVO 只对属性观察

我们给 Person 添加一个成员变量name,一个属性 nickName,分别注册KVO观察,当值发生变化时,是否都能收到回调?

////// Person类:
@interface Person : NSObject
{
    @public
    NSString *name;
}
@property (nonatomic, copy) NSString *nickName;

@end

@implementation Person
@end

////// ViewController中:
@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _person = [Person alloc];
    [_person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [_person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    _person.nickName = @"嘻嘻";
    _person->name = @"哈哈";
}

// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
    NSLog(@"%@ - %@ - %@",keyPath,object,change);
}

// 移除观察者
- (void)dealloc{
    [_person removeObserver:self forKeyPath:@"nickName"];
    [_person removeObserver:self forKeyPath:@"name"];
}

@end

运行结果:

2021-01-18 16:45:19.627398+0800 KVO[4055:350986] nickName -  - {
    kind = 1;
    new = "\U563b\U563b";
    old = "";
}

可以看到,只收到了 nickName 变化的回调,所以KVO 只对属性进行观察,观察的是 setter方法。

3.2 验证中间类

根据官方文档描述,在注册KVO 观察者后,观察对象的isa 指针会发生改变,指向了一个中间类

  • 注册观察者之前,实例对象personisa 指针指向Person:

    截屏2021-01-18 下午4.50.35.png

  • 注册观察者之后,personisa 指针指向了NSKVONotifying_Person

    截屏2021-01-18 下午4.51.15.png

在注册观察者后,实例对象的isa指针指向由Person类变为了NSKVONotifying_Person中间类,即实例对象的isa指针指向确实发生了变化。

那么这个NSKVONotifying_Person和 Person 类有什么关系?

截屏2021-01-18 下午8.43.24.png

我们获取NSKVONotifying_Person类的父类,可以看到是PersonNSKVONotifying_PersonPerson 的子类。

生成这个中间类肯定是有他的作用,我们去看下这个类有什么内容,可以通过下面的方法获取NSKVONotifying_Person类中所有的方法:

- (void)printClassAllMethod:(Class)cls{
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i

输出结果如下:

2021-01-18 20:47:41.791402+0800 KVO[4396:407755] setNickName:-0x10918c54b
2021-01-18 20:47:41.791516+0800 KVO[4396:407755] class-0x10918afd5
2021-01-18 20:47:41.791612+0800 KVO[4396:407755] dealloc-0x10918ad3a
2021-01-18 20:47:41.791706+0800 KVO[4396:407755] _isKVOA-0x10918ad32

从结果中可以看出有 4 个方法:setNickNameclassdealloc_isKVOA,那么这些方法是继承的还是重写的呢?
我们也输出一下 Person 类的方法列表,和NSKVONotifying_Person的方法列表进行对比:

截屏2021-01-18 下午8.51.49.png

可以看到方法地址都不同,说明NSKVONotifying_Person重写了父类 PersonsetNickName 方法,同时重写了基类NSObjectclassdealloc_isKVOA方法。

那么当移除观察者后,被观察对象的 isa 又指向了谁?NSKVONotifying_Person这个中间类还会存在吗?

  • 直接断点查看


    截屏2021-01-18 下午8.56.19.png

可以看到,在移除 KVO 观察者之后,isa 的指向又从NSKVONotifying_Person变成了 Person,那么现在中间类是否还存在么?

我们通过打印 Person 的子类情况,判断中间类是否销毁:

截屏2021-01-18 下午8.59.16.png

通过打印结果可以看出,中间类没有被销毁,还在内存中,主要是为了重用

3.3 总结
  • 实例对象的 isa 指向在注册 KVO 观察者之后,由原有类更改为指向中间类,类名为NSKVONotifying_原有类名
  • 中间类重写了被观察属性的setter 方法classdealloc_isKVO方法
  • dealloc 方法中,移除 KVO 观察者之后,实例对象 isa指向由中间类改为原有类
  • 中间类在移除观察者后也并不会被销毁

4. 自定义 KVO

既然知道了系统KVO 的大概实现,我们可以模仿一下,并进行一些优化处理:

  • 将注册和响应通过函数式编程,即block的方法结合在一起
  • 去掉系统繁琐的三部曲,实现KVO自动销毁机制

在系统中,注册观察者和 KVO 相应属于响应式编程,就是分开写的,为了使代码更集中,将注册和回调的逻辑组合在一起,即采用函数式编程方式,分为三部分:

  • 注册观察者
//*********定义block*********
typedef void(^KVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);

//*********注册观察者*********
- (void)tt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block;
  • KVO响应
    这部分主要是重写setter方法,在中间类的 setter 方法中,通过 block 的方式传递给外部进行响应

  • 移除观察者

//*********移除观察者*********
- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

之后我们创建一个NSObject的分类,实现上面三个方法。

4.1 注册观察者

注册观察者方法中,主要有以下几部分操作:

  • 1.判断当前观察keyPathsetter 方法是否存在
// 验证是否有 setter 方法
- (void)judgeSetterMethodFromKeyPath:(NSString *)keyPath{
    Class class = object_getClass(self);
    SEL setterSEL = NSSelectorFromString([self setterForGetter:keyPath]);
    Method setterMethod = class_getInstanceMethod(class, setterSEL);
    if (!setterMethod) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"KVO - 没有当前%@的setter方法", keyPath] userInfo:nil];
    }
}

// getter 转 setter
- (NSString*)setterForGetter:(NSString*)key{
    if (key.length <= 0) {
        return nil;
    }
    key = [key capitalizedStringWithLocale:[NSLocale currentLocale]];
    NSString *setter = [NSString stringWithFormat:@"set%@:",key];
    return setter;
}
  • 2.动态生成子类,将需要重写的class方法添加到中间类中
// 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
    // 原有类名
    NSString *oriClassName = NSStringFromClass([self class]);
    // 新的子类名
    NSString *childClassName = [NSString stringWithFormat:@"NSKVONotifying_%@",oriClassName];
    // 获取子类
    Class childCls = NSClassFromString(childClassName);
    if(childCls){
        return childCls;
    }
    
    // 申请类
    childCls = objc_allocateClassPair([self class], childClassName.UTF8String, 0);
    // 注册类
    objc_registerClassPair(childCls);
    // 添加方法
    SEL classSel = @selector(class);
    Method classMethod = class_getInstanceMethod([self class], classSel);
    const char *classType = method_getTypeEncoding(classMethod);
    class_addMethod(childCls, classSel, (IMP)tt_class, classType);
    
    return childCls;
}

// 重写class方法,为了与系统类对外保持一致
Class tt_class(id self, SEL _cmd){
    //在外界调用class返回Person类
    return class_getSuperclass(object_getClass(self));
}
  • 3.将被观察实例对象的isa 指向由原有类改为中间类
objc_setClass(self,childCls);
  • 4.保存信息,这里用的是数组,需要创建保存信息的模型类
@interface KVOInfo : NSObject

@property (nonatomic, weak) id observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) KVOBlock handleBlock;

- (instancetype)initWithObserver:(id)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block;

@end
@implementation KVOInfo

- (instancetype)initWithObserver:(id)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block{
    self = [super init];
    if (self) {
        _observer = observer;
        _keyPath = keyPath;
        _handleBlock = block;
    }
    return self;
}

@end

///////// 保存信息
//- 保存多个信息
KVOInfo *info = [[KVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
//使用数组存储 -- 也可以使用map
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
if (!mArray) {//如果mArray不存在,则重新创建
    mArray = [NSMutableArray arrayWithCapacity:1];
    objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];

完整的注册观察者代码如下:

- (void)tt_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(KVOBlock)block{
    // 1.验证 setter 方法是否存在
    [self judgeSetterMethodFromKeyPath:keyPath];
    
    // 2.保存信息
    KVOInfo *info = [[KVOInfo alloc] initWithObserver:observer forKeyPath:keyPath handleBlock:block];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
    if (!mArray) {//如果mArray不存在,则重新创建
        mArray = [NSMutableArray arrayWithCapacity:1];
        objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [mArray addObject:info];
    
    // 3.动态生成子类
    Class childCls = [self createChildClassWithKeyPath:keyPath];
    
    // 4.更改 isa 指向
    object_setClass(self, childCls);
    
    // 5.添加一个 setter 方法
    SEL setterSel = NSSelectorFromString([self setterForGetter:keyPath]);
    //获取setter实例方法
    Method method = class_getInstanceMethod([self class], setterSel);
    //方法签名
    const char *type = method_getTypeEncoding(method);
    //添加一个setter方法
    class_addMethod(childCls, setterSel, (IMP)tt_setter, type);
    
}

生成的中间类的 class 方法必须重写,其目的是为了和系统一样,对外获取的类保持一致
tt_setter方法实现,会在下面进行讲述。

4.2 KVO 响应

主要是给子类添加 setter 方法,其目的是为了在setter 方法中向父类发送消息,告知其属性值的变化:

static void tt_setter(id self, SEL _cmd, id newValue){
    NSLog(@"来了:%@",newValue);
    
    //此时应该有willChange的代码
    
    //往父类Person发消息 - 通过objc_msgSendSuper
    //通过系统强制类型转换自定义objc_msgSendSuper
    void (*tt_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
    //定义一个结构体
    struct objc_super superStruct = {
        .receiver = self, //消息接收者 为 当前的self
        .super_class = class_getSuperclass(object_getClass(self)), //第一次快捷查找的类 为 父类
    };
    //调用自定义的发送消息函数
    tt_msgSendSuper(&superStruct, _cmd, newValue);
    
    //此时应该有didChange的代码
    
    //让vc去响应
    /*---函数式编程*/
    NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
    id oldValue = [self valueForKey:keyPath];
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
    for (KVOInfo *info in mArray) {
        NSMutableDictionary *change = [NSMutableDictionary dictionaryWithCapacity:1];
        if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
            
           info.handleBlock(info.observer, keyPath, oldValue, newValue);
        }
    }
}
4.3 移除观察者

为了避免在外界不断的调用removeObserver 方法,在自定义KVO中实现自动移除观察者。
实现tt_removeObserver:forKeyPath:方法,主要是清空数组,以及isa指向更改

- (void)tt_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
    
    NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kKVOAssociateKey));
    if (mArray.count <= 0) {
        return;
    }
    
    for (KVOInfo *info in mArray) {
        if ([info.keyPath isEqualToString:keyPath]) {
            [mArray removeObject:info];
            objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kCJLKVOAssociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
    
    if (mArray.count <= 0) {
        //isa指回父类
        Class superClass = [self class];
        object_setClass(self, superClass);
    }
}

在子类中重写dealloc方法,当子类销毁时,会自动调用dealloc方法(在动态生成子类的方法中添加)

#pragma mark - 动态生成子类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
    //...
    
    //添加dealloc 方法
    SEL deallocSel = NSSelectorFromString(@"dealloc");
    Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
    const char *deallocType = method_getTypeEncoding(deallocMethod);
    class_addMethod(newClass, deallocSel, (IMP)tt_dealloc, deallocType);
    
    return newClass;
}

//************重写dealloc方法*************
void tt_dealloc(id self, SEL _cmd){
    NSLog(@"来了");
    Class superClass = [self class];
    object_setClass(self, superClass);
}

其原理主要是Person 释放即调用 dealloc 了,就会自动走到重写的 tt_dealloc方法中(原因是person 对象的isa目前还在指向中间类,但是实例对象的地址是不变的,所以 person释放时,会调用子类重写 tt_dealloc 方法),达到自动移除观察者的目的。

这个例子是为了能够更方便的理解KVO 底层原理,仅做参考。

你可能感兴趣的:([iOS] KVO底层原理)