iOS KVO

KVO

今天写一下KVO的实现原理。

一、应用

1、API

(1)给对象添加KVO监听

- (void)addObserver:(NSObject *)observer  
         forKeyPath:(NSString *)keyPath  
            options:(NSKeyValueObservingOptions)options  
            context:(void *)context  

observer:观察对象,需要实现observeValueForKeyPath: ofObject: change: context:
keyPath:被观察的属性。
options:发送通知的时机(属性值改变前or改变后通知)
context:(目前未用到过该参数)上下文信息,通常为nil

  • NSKeyValueObservingOptions:
    NSKeyValueObservingOptionNew 返回新值
    NSKeyValueObservingOptionOld 返回旧值
    NSKeyValueObservingOptionInitial 注册的时候发一次通知,改变后也发送一次通知
    NSKeyValueObservingOptionPrior 改变之前发一次,改变之后再发一次

(2)接收通知

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context;

keyPath:被观察的属性
object:被观察对象
context:添加监听时传过来的上下文信息
change:字典,keys有以下五种:

NSKeyValueChangeNewKey 新值
NSKeyValueChangeOldKey 旧值
NSKeyValueChangeIndexesKey 观察容器属性时会返回的索引值
NSKeyValueChangeNotificationIsPriorKey
NSKeyValueChangeKindKey 四种修改类型,如下:

NSKeyValueChangeSetting = 1 赋值 SET
NSKeyValueChangeInsertion = 2 插入 insert
NSKeyValueChangeRemoval = 3 移除 remove
NSKeyValueChangeReplacement = 4 替换 replace

kind的四种不同取值

(3)移除通知

- (void)removeObserver:(NSObject *)observer
            forKeyPath:(NSString *)keyPath;

observer:观察对象
keyPath:观察属性

被观察对象在销毁前必须要移除监听。且被重复添加N次的被观察对象,也需要销毁N次

2、代码示例

先定义两个类KYDogKYUser,且在KYUser中有KYDog类型的属性。控制器中添加KYUser属性。

  • 公用代码:
@interface KYDog : NSObject
/** 狗的名字 */
@property (nonatomic, strong) NSString *name;
/** 狗的年龄 */
@property (nonatomic, assign) int age;
@end
@interface KYUser : NSObject 
/** ID */
@property (nonatomic, strong) NSString *userId;
/** 狗�� */
@property (nonatomic, strong) KYDog *dog;
/** 数组 */
@property (nonatomic, strong) NSMutableArray *arr;
@end
@interface ViewController ()
@property (nonatomic, strong) KYUser *user;
@end

(1)观察普通类型属性

观察user中的userId属性:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.user = [[KYUser alloc] init];
    // 1、添加KVO监听
    [self.user addObserver:self forKeyPath:@"userId" options:NSKeyValueObservingOptionNew context:nil];
}
// 2、接收监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@", keyPath);
    NSLog(@"%@", object);
    /*
    NSKeyValueChangeKindKey;
    NSKeyValueChangeNewKey;
    NSKeyValueChangeOldKey;
    NSKeyValueChangeIndexesKey;
     */
    NSKeyValueChangeNotificationIsPriorKey
    NSLog(@"%@", change[NSKeyValueChangeNewKey]);
    NSLog(@"%@", (__bridge id)(context));
}
// 3、触发修改属性值
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.user.userId = @"123456789";
}
// 4、移除监听
- (void)dealloc {
    [self.user removeObserver:self forKeyPath:@"userId"];
}

(2)手动触发KVO

上面的方法在userId值发生变化时会自动触发通知。但是在某些情况下,对于有些情况的值变动并不想被观察到,该情况下可使用手动触发的KVO的方法。
在被观察对象KYUser中重写下方类方法:

扫描二维码关注公众号,回复: 1852706 查看本文章
//默认自动模式YES,若返回NO,不发送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}

此时修改userId的值,通知将不被触发。在修改值的前后写上手动通知的方法:

// willChange 和 didChange 一般成对儿出现
[self.user willChangeValueForKey:@"userId"];
self.user.userId = @"123456";
[self.user didChangeValueForKey:@"userId"];

(3)观察被观察者中自定义类型中的属性

  • 观察KYUserKYDog属性,可以直接点语法到需要观察的属性。例如在控制器中观察 user.dog.name
[self.user addObserver:self forKeyPath:@"dog.name" options:NSKeyValueObservingOptionNew context:nil];
  • 观察user.dog属性中的多个属性。例如在控制器中观察user.dog.nameuser.dog.age等。很笨的方法就是添加多个addObserver,但是若user.dog中有很多很多的属性需要观察,这样的需求可以在被观察者KYUser中重写类方法:
// 返回一个容器,里面放字符串类型,监听容器中的属性
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"dog"]) {
        NSArray *arr = @[@"_dog.name", @"_dog.age"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:arr];
    }
    return keyPaths;
}

接下来在控制器中就可以直接观察dog,上面返回的集合中的属性就都会被观察到:

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

通知中打印change:

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

change字典值
change字典中new值是KYDog类型。

(4)观察容器类属性

观察user.arr数组属性,使用[self.user.arr addObject:@"value_1"],并不能被观察到,因为观察者实现原理是对set方法的监听。但是可以结合KVC实现对容器属性的监听:

// 添加观察数组属性
[self.user addObserver:self forKeyPath:@"arr" options:NSKeyValueObservingOptionNew context:nil];
// 通过KVC获取到属性值,这样才能观察到arr属性的修改
NSMutableArray *tempArr = [self.user mutableArrayValueForKey:@"arr"];
[tempArr addObject:@"value_1"];

观察tempArr类型:
tempArr类型
其实在添加观察user.arr属性时,就动态生成了NSMutableArray的子类NSKeyValueNotifyingMutableArr,并重写了它的addObject:等方法,加入了willChangedidChange方法。

二、实现原理

KVO的实现原理就是通过运行时,替换被观察对象KYUser的isa指针,创建其子类对象NSKVONotifying_KYUser,重写被观察属性set方法,将被观察属性值的变法发送给指定的通知方法。

1、KVO动态创建的子类

添加观察的时候,动态创建了子类:
KVO动态创建子类对象

2、自定义KVO实现

  • 根据1的提示,创建NSObject的分类category。定义并实现下面的方法:
- (void)KY_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {

    // 1、自定义NSKVONotifying子类
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [NSString stringWithFormat:@"KYKVO_%@", oldClassName];

    // 创建KVO子类
    Class KYKVO_Class = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
    // 注册KVO子类
    objc_registerClassPair(KYKVO_Class);

    // 2、动态修改类型,指向新创建的子类(改变isa指针)
    object_setClass(self, KYKVO_Class);

    // 3、动态添加set方法(重写父类方法就是添加)
    // v@:@   v表示customMethod返回值void,@表示第一个参数是OC对象,: 表示SEL类型,@表示第三个参数为OC对象
    class_addMethod(KYKVO_Class, @selector(setUserId:), customMethod, "");
}
// 前两个隐藏参数不可省略
void customMethod(id self, SEL _cmd, NSString *newValue) {
    id class = [self class];
    // 让自己指向父类
    object_setClass(self, class_getSuperclass([self class]));
    objc_msgSend(self, @selector(setUserId:), newValue);
    // 赋值完了之后isa再指向自己
    object_setClass(self, class);
    // 属性值发生变化的前后,可以使用通知、代理或block的方式将值的变法发送出去。后面的步骤比较简单直接略过了。。。
}

代码中使用运行时动态创建了自定义的KVO子类,并添加了setUserId:方法。真实的逻辑肯定比这个要复杂的多,上面的代码只是很简单的模拟了一下 user.userId 被观察时的情况。然后再自己定义通知方法,将观察到的情况发送给观察者。后面的发送观察结果给观察者实现方法比较简单,直接略过了。。。。

猜你喜欢

转载自blog.csdn.net/kangpengpeng1/article/details/80075236