KVO相关知识以及底层实现

2018-11-20  本文已影响0人  king_jensen

一.KVO的基本使用

使用KVO分为三个步骤:

1.通过addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context方法添加观察者,观察者可以接受keyPath属性的变化事件。

2.在观察者中实现-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context方法,当keyPath属性发生变化后,KVO回调这个方法通知观察者。

3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则导致Crash

代码实现:

新建Student类,定义属性
@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end

控制器实现观察Student类的属性
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
 _student = [[Student alloc] init];
 [self.student addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@",change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.student.name = [NSString stringWithFormat:@"%ld",random()];
}
控制器dealloc中移除监听
- (void)dealloc {
    [self.student removeObserver:self forKeyPath:@"name"];
}

二.KVO出发模式

KVO在属性发生改变时调用是自动的,如果想要手动控制这个调用时机,或想自己实现KVO属性的调用,可以通过KVO提供的方法进行调用.

在Student中修改为手动模式:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

这里可以通过KEY值来修改,指定手动触发的属性:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return YES;
}

实现手动触发:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     [self.student willChangeValueForKey:@"name"];
     self.student.name = [NSString stringWithFormat:@"%ld",random()];
     [self.student didChangeValueForKey:@"name"];
}

这里有一个问题:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     [self.student willChangeValueForKey:@"name"];
    //  self.student.name = [NSString stringWithFormat:@"%ld",random()];
     [self.student didChangeValueForKey:@"name"];
}

这个代码还是会触发,这里和底层原理有关,稍后分析。

三.KVO属性依赖

新建一个Dog类:

@interface Dog : NSObject
@property (assign, nonatomic) int age;
@property (strong, nonatomic) NSString * name;
@end

@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end

@implementation Student
- (instancetype)init {
    if (self = [super init]) {
        _dog = [[Dog alloc] init];
        _things = [[NSMutableArray alloc] initWithCapacity:0];
    }
    return self;
}
@end

通过路径观察:

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@",change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.student.dog.age = random()%100;
}

实际开发中有这样的需求:监听Student的Dog的变化,不管Dog发生什么样的变化,都要通知Student.
这个就是一种依赖,有一些属性的值取决于一个或者多个其他对象的属性值,一旦某个依赖的属性值变了,依赖它的属性的变化也需要被通知。

  [self.student addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];

在Student中实现:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet * set = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        set = [NSSet setWithObjects:@"_dog.age",@"_dog.name", nil];
    }
    return set;
}

四.KVO原理探究

大家应该都听说过KVO内部是监听set方法的吧,那么我们就来看看,它是怎么一回事!
我们将属性改成成员变量:

@interface Student : NSObject
{
    @public
    NSString * name;
}

为该成员变量添加监听:
[self.student addObserver:self forKeyPath:@"_name" options:NSKeyValueObservingOptionNew context:nil];
修改值:

 self.student->name = [NSString stringWithFormat:@"%ld",random()];

测试结果:监听不到
结论:KVO是通过监听一个对象有没有调用set方法,然后set方法。

KVO底层实现:
1.自定义Sudent类的子类
2.重写setName,在内部恢复父类做法,通知观察者,
3.让外界调用Student类的子类方法,修改当前对象isa指针,指向自定义子类

DFECC7E712C0463F8FC82067F0578116.png

苹果修改了isa指针。

五.简单模拟KVO

先来看看苹果的做法:


A06D46A4DBBE6961C02C2DA9E223F803.png

创建分类:

@interface NSObject (JKVO)
- (void)JENSEN_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
@end

@implementation NSObject (JKVO)

- (void)JENSEN_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    const char * jClassName = [[@"JENSENKVO_" stringByAppendingString:NSStringFromClass([self class])]UTF8String];
    Class jensenClass = objc_allocateClassPair([self class], jClassName, 0);
    class_addMethod(jensenClass, @selector(setName:), (IMP)setName, "v@:@");
    objc_registerClassPair(jensenClass);
    object_setClass(self, jensenClass);
    objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

void setName(id self,SEL _cmd, NSString * newName){
    Class class = [self class];
    object_setClass(self ,class_getSuperclass(class));
    objc_msgSend(self,@selector(setName:),newName);
    id observer = objc_getAssociatedObject(self, @"objc");
    if (observer) {
objc_msgSend(observer,@selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new":[self valueForKey:@"name"],@"kind":@1},nil);
    }
    object_setClass(self, class);
}

@end

六.KVO对容器类的监听

Student类:

@interface Student : NSObject
@property (strong, nonatomic) NSString * name;
@property (strong, nonatomic) NSMutableArray * things;
@property (strong, nonatomic) Dog * dog;
@end

@implementation Student
- (instancetype)init {
    if (self = [super init]) {
        _dog = [[Dog alloc] init];
        _things = [[NSMutableArray alloc] initWithCapacity:0];
    }
    return self;
}
@end

控制器实现监听:

   _student = [[Student alloc] init];
    [self.student addObserver:self forKeyPath:@"things" options:NSKeyValueObservingOptionNew context:nil];

在容器中添加数据:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.student.things addObject:@"00"];
}

结果:监听不到容器变化
为什么?KVO监听的是set方法!addObject方法和things的set方法没有关系吧
所以我们改一下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableArray * tempArray = NSMutableArray.array;
    [tempArray addObject:@"00"];
    self.student.things = tempArray;
}

这样就能够监听到了吧!
但是如果我们需要观察这个容器类属性内部的变化呢?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSMutableArray * tempArray = [self.student mutableArrayValueForKey:@"things"];
    [tempArray addObject:@"00"];
}

运行结果:


F2CCB5C606ED4D519A9C53F8A943B942.png

kind为2,我们看一下kind定义的头文件:


A4D6223F8513FD8F5437DD086E3C00BC.png
这个时候,我们就观察到容器类插入方法了。

那么这种是怎么做到的呢?


9B19BDFC1AEEE2C253A04F7BF7EED4D7.png

这个NSKeyValueNotifyingMutableArray就是NSMutableArray的子类!
内部也是一个道理,重写了addObject方法,多了一个手动通知KVO的willChange和DidChange的吧。

上一篇 下一篇

猜你喜欢

热点阅读