15:KVO (上) —— 使用篇

170 阅读7分钟

KVO 3个步骤 (自动调用)

对对象自动调用

@interface LGViewController ()
@property (nonatomic, strong) LGPerson  *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 注册观察者
        [person addObserver:self
	             forKeyPath:@"name"
	                options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
	                context:NULL];
    
    // 对象中被观察的属性发生变化
    person.name = @"Mark";    // 点语法
    [person setName:@"Dash"];    // setter
    [person setValue:nil forKey:@"name"];  // KVC
}

// 2. 监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    MDNSLog(@"keyPath: %@", keyPath);
    MDNSLog(@"object: %@", object);
    MDNSLog(@"change: %@", change);
}

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

@end

控制台打印↓

keyPath: name
object: <MDPerson: 0x600001561340>
change: {
    kind = 1;
    new = Mark;
    old = "<null>";
}
keyPath: name
object: <MDPerson: 0x600001561340>
change: {
    kind = 1;
    new = Dash;
    old = Mark;
}
keyPath: name
object: <MDPerson: 0x600001561340>
change: {
    kind = 1;
    new = "<null>";
    old = Dash;
}
  • 三个步骤:

    1. 注册观察者。 person对象说:self作为观察者,观察我里面的name属性,一旦有变化,我需要了解new新值和old旧值,context是一个 tag(待会详细说)

    2. 监听回调。 参数有:发生变化的属性名name,这个属性所属的对象<MDPerson: 0x600001561340>,发生了什么变化change: {kind = 1;new = Mark;old = "<null>";}

    3. 移除观察者。 在观察者被销毁之前,要移除监听,否则会出问题。假设观察者被销毁,person仍然存在(譬如单例对象),如果name发生改变则会触发监听回调,系统会因为找不到观察者而崩溃(相当于野指针)。但也要注意,移除不存在的观察者,系统会崩溃

对数组/集合自动调用

  • 三个步骤 同上

  • set值的时候有区别

    [[person mutableArrayValueForKey:@"hobbies"] addObject:@"sleep"];
    [[person mutableArrayValueForKey:@"hobbies"] replaceObjectAtIndex:0 withObject:@"run"];
    [[person mutableArrayValueForKey:@"hobbies"] removeObjectAtIndex:0];
    
    // 监听回调 MDNSLog(@"%@", change);
    

    控制台打印↓

    {
        indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 2;
        new =     (
            sleep
        );
    }
    {
        indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 4;
        new =     (
            run
        );
        old =     (
            sleep
        );
    }
    {
        indexes = "<_NSCachedIndexSet: 0x600001c1e860>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 3;
        old =     (
            run
        );
    }
    

打印 change 时,里面的 kind 是什么?

  • kind属于NSKeyValueChangeKindKey

    • 1 对象的所有变化
    • 2 数组/集合的新增元素
    • 3 数组/集合的移除元素
    • 4 数组/集合的替换元素
    typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
        NSKeyValueChangeSetting = 1,
        NSKeyValueChangeInsertion = 2,
        NSKeyValueChangeRemoval = 3,
        NSKeyValueChangeReplacement = 4,
    };
    

context 有什么用?

  • 官方文档描述了这样一个问题:

    You may specify NULL and rely entirely on the key path string to determine the origin of a change notification, but this approach may cause problems for an object whose superclass is also observing the same key path for different reasons.

    你有可能给contextNULL,然后根据keyPath来定位想要的监听。但这种方法有可能出现问题:如果object父类也在观察这个keyPath

父类和子类同时观察同一个 keyPath 引起问题

  • 父类.m

    @implementation MDPerson
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            // 注册观察者
            [self addObserver:self
    	           forKeyPath:@"name"
    	              options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
    	              context:NULL];
        }
        return self;
    }
    
    // 父类的监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    {
        // 识别 KeyPath
        if ([keyPath isEqualToString:@"name"]) {
            MDNSLog(@"(Person) new name is: %@", change[@"new"]);
        }
    }
    
    @end
    
  • 子类.m

    @implementation MDStudent
    
    - (instancetype)init
    {
        self = [super init];
        if (self) {
            // 注册观察者
            [self addObserver:self
    	           forKeyPath:@"name"
    	              options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
    	              context:NULL];
        }
        return self;
    }
    
    // 子类的监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    {
        // 识别 KeyPath
        if ([keyPath isEqualToString:@"name"]) {
            MDNSLog(@"(Student) new name is: %@", change[@"new"]);
        }
    }
    
    @end
    
  • 被观察的属性发生变化

    // 创建对象
    MDStudent *student = [[MDStudent alloc] init];
    // 属性发生变化
    student.name = @"Mark";
    
  • 控制台打印↓

    (Student) new name is: Mark 
    (Student) new name is: Mark
    
  • 问题:不走父类MDPerson的监听回调,走了2次子类MDStudent的监听回调

  • 原因:首先,父类的注册观察者子类的注册观察者,这2行代码(在这个情况)本质做的是同一件事情,等价于== 写2次父类的注册观察者 或者 写2次子类的注册观察者;其次,子类重写了监听回调,自然就不走父类的了。

解决问题

  • 注册时,传context;监听时,识别context

  • 效果:父类的注册观察者子类的注册观察者,这2行代码不一样了,子类的传有context;子类监听回调识别context,前一次识别成功,后一次识别失败(contextNULL) 抛给父类处理。

    // 定义一个 context
    static void *StudentContext = &StudentContext;
    
    // 注册观察者,传 context
    [self addObserver:self
           forKeyPath:@"name"
              options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
              context:StudentContext];
    
    // 监听回调里面,要识别 context
    if (context == StudentContext) {
        MDNSLog(@"(Student) new name is: %@", change[@"new"]);
    }
    else {
        // 任何未识别的 context 原则上要抛给父类处理
        [super observeValueForKeyPath:keyPath
    	                     ofObject:object
    	                       change:change
    	                      context:context];
    }
    

    控制台打印↓

    (Student) new name is: Mark
    (Person) new name is: Mark
    

context 通常用法

  • 除了用来解决以上问题,context最简单的用法就是作为TAG将监听定位到某个对象

PS:以上问题只能用context解决,而下面的代码却有其他一些替代方案。意味着有些被广泛应用的特性其实是为了解决某个罕见问题,而我们却毫无察觉╮(╯▽╰)╭。

  • 简单区分person对象p2对象

    // 在外部定义 context
    static void *personNameContext = &personNameContext;
    static void *p2NameContext = &p2NameContext;
    
    // 注册观察者时使用 context
    [person addObserver:self
             forKeyPath:@"name"
                options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
                context:personNameContext];
    [p2 addObserver:self
             forKeyPath:@"name"
                options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
                context:p2NameContext];
    
    // 监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    {
        if (context == personNameContext) {
            MDNSLog(@"new name of person is: %@", change[@"new"]);
        }
        else if (context == p2NameContext) {
            MDNSLog(@"new name of p2 is: %@", change[@"new"]);
        }
    }
    

手动调用 KVO

对对象手动调用

  • 以上,在属性发生变化时,KVO的监听回调是自动触发的;我们也可以通过一个开关来手动触发

  • Person.m添加下面代码,则person.name = @"Mark"不触发监听回调

    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        BOOL automatic = NO;
        if ([key isEqualToString:@"name"]) {
            // 仅对该key禁用系统自动通知,若要禁用该整个类的KVO则直接返回NO;
            automatic = NO;
        }
        else {
            // 识别不到的 key,抛给父类处理
            automatic = [super automaticallyNotifiesObserversForKey:key];
        }
        return automatic;
    }
    
  • 想让哪行赋值代码触发监听回调,就在那行代码的上下夹写willChangeValueForKey:didChangeValueForKey:(可能在touchesBegan:withEvent:里,可能在类的setter里..)

    [student willChangeValueForKey:@"name"];
    student.name = @"Mark";
    [student didChangeValueForKey:@"name"];
    

对数组/集合手动调用

  • 官方文档还提及了to-many类型的情况:

    In the case of an ordered to-many relationship, you must specify not only the key that changed, but also the type of change and the indexes of the objects involved. The type of change is an NSKeyValueChange that specifies NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, or NSKeyValueChangeReplacement. The indexes of the affected objects are passed as an NSIndexSet object.

    数组或集合的情况,除了指定发生变化的key,还要指定改变类型以及下标集合

    改变类型是NSKeyValueChange,有3种:NSKeyValueChangeInsertion NSKeyValueChangeRemoval NSKeyValueChangeReplacement

    下标集合是NSIndexSet类型

  • 示例:某个类的removeTransactionsAtIndexes:方法里进行移除,仍然是上下夹写

    - (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
        [self willChange:NSKeyValueChangeRemoval
            valuesAtIndexes:indexes forKey:@"transactions"];
     
        // 对指定的那些下标项,进行移除操作
     
        [self didChange:NSKeyValueChangeRemoval
            valuesAtIndexes:indexes forKey:@"transactions"];
    }
    

手动调用和自动调用的本质区别

  • 自动调用,设置watchpoint,在setter被调用时,在汇编里看到系统已经自动给class_getMethodImplementation夹写了willChangedidChange

C = A + B,如何在 A或B 发生变化时,能监听到C的值

复合路径的使用

  • KVO建立在KVC上,本质是监听setter

    • 注册观察A - setA - 监听回调得到A,成功
    • 注册观察C - setA - 监听回调得到C,失败
    • 注册观察C - 告诉系统C会被A影响 - setA - 监听回调得到C,成功,看下面实现
  • Person.h

    @interface MDPerson : NSObject
    // 属性
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, copy) NSString *firstName;
    @property (nonatomic, copy) NSString *lastName;
    
    @end
    
  • Person.m

    • 重写被影响者(name) 的getter方法 (和KVC流程不完全对标,尝试过只能是getNamename)

    • 重写keyPathsForValuesAffectingValueForKey:设置影响者-被影响者Person类有3个属性,运行时 该方法会进来3次,记录每个属性的影响者 (keyfirstNamelastName时,返回的keyPaths为空)

    @implementation MDPerson
    
    - (NSString *)name {
        return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
    }
    
    + (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
     
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
     
        if ([key isEqualToString:@"name"]) {
            NSArray *affectingKeys = @[@"lastName", @"firstName"];
            keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
        }
        return keyPaths;
    }
    
    @end
    
  • ViewController.m,这之前差不多,只不过发生改变的是影响者

    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 创建对象
        MDPerson *person = [[MDPerson alloc] init];
        // 注册观察者 (被影响者)
        [person addObserver:self
                 forKeyPath:@"name"
                    options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld
                    context:NULL];
        // 影响者发生改变
        person.firstName = @"Mark";
        person.lastName = @"Dash";
    }
    
    // 监听回调
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                           context:(void *)context
    {
        MDNSLog(@"new name of person is: %@", change[@"new"]);
    }
    
    @end
    

    控制台打印↓

    new name of person is: Mark (null)
    new name of person is: Mark Dash
    

意外情况

  • namegetter方法被重写,导致外界的person.name无法获取到_name,如果想获取就要另外写一个方法return _name

  • 即使这样,调用person.name = @"Mark"时,回调方法的change依然是firstName lastName而不是_name,所以在回调方法里也要做处理