iOS--KVO

2020-08-06  本文已影响0人  mayuee

Introduction to Key-Value Observing Programming Guide

Key-value observing 键-值观察是一种机制,可以用于监听某个对象的指定属性值在发生更改时得到通知,是Objective-C对观察者设计模式的一种实现。经常用于程序中Controller对象观察Model对象的属性,View对象通过Controller观察Model对象的属性。此外,Model对象可以观察其他Model对象(通常用于确定从属值何时更改),甚至可以观察自身(再次确定从属值何时更改)。

可以观察属性,包括简单属性、to-one关系和to-many关系。to-many关系的观察者会被告知所做更改的类型以及更改涉及的对象。

KVO的实现基础之一是被监控对象必须拥有相应的setter方法,换句话说只有ivar(成员变量)的类是无法进行监控的。
成员变量直接修改需要手动触发KVO

   [self willChangeValueForKey:@"keyPath"];
    ivar = newivar; 
    [self didChangeValueForKey:@"keyPath"];

举个例子说明KVO如何发挥作用。假设Account表示Person在银行的储蓄帐户。Person实例可能需要知道Account实例的某些方面何时发生更改,例如余额或利率。

Art/kvo_objects_properties.png

如果这些属性是Account的公共属性,Person可以定期轮询Account以发现变化,但这显然是低效的,且常常是不切实际的。更好的方法是使用KVO,它类似于在发生更改时接收中断。

要使用KVO,首先必须确保被观察的对象(本例中的Account )与KVO兼容。通常,如果对象继承自“NSObject”,并且以常规方式创建属性,那么对象及其属性将自动兼容KVO。也可以手动实现。KVO compliance描述了自动和手动实现KVO的区别,以及如何实现两者。

接下来,必须将观察者实例Person注册到被观察的实例Account。对于每个被观察到的key path,Person发送一条addObserver:forKeyPath:options:context:消息给Account,并将自己命名为观察者。

Art/kvo_objects_add.png

为了从Account接收更改通知,Person实现了observeValueForKeyPath:ofObject:change:context:方法,这个方法所有观察者都需要实现。Account 在每次注册的key paths发生更改时将此消息发送给Person。然后 Person可以根据变更通知进行适当的处理。

Art/kvo_objects_observe.png

最后,当它不再需要通知,至少在它被deallocated之前, Person实例必须通过发送removeObserver:forKeyPath:消息给Account取消注册。

Art/kvo_objects_remove.png

Registering for Key-Value Observing 描述了注册、接收和取消注册KVO通知的整个生命周期。
Registering Dependent Keys 解释了如何指定一个键的值依赖于另一个键的值。

与使用NSNotificationCenter的通知不同,KVO没有为所有观察者提供更改通知的中心对象。相反,当发生更改时,通知会直接发送到观察对象。`NSObject'提供了键值观察的基本实现,我们很少需要重写这些方法。

重要提示:

移除观察者时,记住以下几点:

KVO实现细节

1、 KVO 的实现依赖于 Objective-C 的 Runtime 。自动KVO是使用 isa-swizzling的技术实现的。isa 指针指向维护调度表的对象类。这个调度表本质上包含了指向类实现的方法和其他数据的指针。

2、 当观察者为对象K的属性注册时,runtime 动态创建一个继承自K对象的中间类NSKVONotifying_K,并将K的isa指针指向这个中间类。因此,isa指针的值不一定反映实例的实际类。不能依赖isa指针来确定类成员身份。

3、重写了被观察属性的 setter 方法。setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改。当修改实例对象的属性时秒回调用Foundation的 _NSSetXXXCalueAndNotify函数

示例验证(这里需要引入一个分类NSObject+DLIntrospection打印instanceMethods)

//KVOModel类
@interface KVOModel : NSObject
@property(nonatomic, copy)NSString *name;
@end

//controller

_kvo1 = [KVOModel new];
    NSLog(@"1、-------监听之前");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

       
    [_kvo1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
    NSLog(@"2、-------监听之后");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass_:%@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

    [_kvo1 removeObserver:self forKeyPath:@"name"];
    NSLog(@"3、-------去掉监听之后");
    NSLog(@"setter_地址_:%p", [_kvo1 methodForSelector:@selector(setName:)]);
    NSLog(@"class_:%@", [_kvo1 class]);
    NSLog(@"object_getClass: %@", object_getClass(_kvo1));
    NSLog(@"object_getClass_instanceMethods_:%@", [object_getClass(_kvo1) instanceMethods]);

运行项目

2020-09-16 16:58:46.955488+0800 TT[14496:252466] 1、-------监听之前
2020-09-16 16:58:46.955717+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.955856+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.955962+0800 TT[14496:252466] object_getClass_:KVOModel
2020-09-16 16:58:46.956310+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void).cxx_destruct",
   "- (id)name",
   "- (void)setName:(id)arg0 "
)
2020-09-16 16:58:46.956743+0800 TT[14496:252466] 2、-------监听之后
2020-09-16 16:58:46.956898+0800 TT[14496:252466] setter_地址_:0x105e0798b
2020-09-16 16:58:46.957024+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957154+0800 TT[14496:252466] object_getClass_:NSKVONotifying_KVOModel
2020-09-16 16:58:46.957337+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void)setName:(id)arg0 ",
   "- (class)class",
   "- (void)dealloc",
   "- (BOOL)_isKVOA"
)
2020-09-16 16:58:46.957484+0800 TT[14496:252466] 3、-------去掉监听之后
2020-09-16 16:58:46.957606+0800 TT[14496:252466] setter_地址_:0x105b2c9a0
2020-09-16 16:58:46.957710+0800 TT[14496:252466] class_:KVOModel
2020-09-16 16:58:46.957840+0800 TT[14496:252466] object_getClass: KVOModel
2020-09-16 16:58:46.958318+0800 TT[14496:252466] object_getClass_instanceMethods_:(
   "- (void).cxx_destruct",
   "- (id)name",
   "- (void)setName:(id)arg0 "
)

上面的结果说明,在KVOModel对象的实例 _kvo1 被观察时,runtime动态创建了一个KVOModel类的子类NSKVONotifying_KVOModel,而且为了隐藏这个行为,NSKVONotifying_KVOModel重写了- (Class)class方法返回之前的KVOModel类。但是使用object_getClass()就暴露了,因为这个方法返回的是这个对象的isa指针,isa指针指向的一定是个这个对象的类对象。

NSObject+DLIntrospection 的instanceMethods是在arc下所有dealloc调用完成后负责释放所有的变量。
从上面2、-------监听之后的打印可以看出,动态类重写了4个方法:

KVO 的缺点:

自己代码实现KVO

系统是自动实现的中间类NSKVONotifying_KVOModel,我们自己手动创建一个中间类CustomKVO_KVOModel。给NSObject创建一个分类,让每一个对象都拥有我们自定义的KVO特性。这里只做简单实现以帮助增加对KVO原理的理解。

//NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)
- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end

//NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>
#import <objc/message.h>

@implementation NSObject (KVO)


- (void)m_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
//这里自定义kvo
{
    //注册一个类
    
    //生成中间类的类名 CustomKVO_XXX
    NSString *oldName = NSStringFromClass([self class]);
    NSString *newName = [NSString stringWithFormat:@"CustomKVO_%@", oldName];
    
    //动态创建类
    Class customClass = objc_allocateClassPair([self class], newName.UTF8String, 0);
    //设置对象的isa指针,修改 isa 指向
    object_setClass(self, customClass);
    //重写 setter 方法
    NSString *methodName = [NSString stringWithFormat:@"set%@:", keyPath.capitalizedString];
    SEL sel = NSSelectorFromString(methodName);
    //将方法添加到动态类
    class_addMethod(customClass, sel, (IMP)kvo_setter, "v@:@");
    
    //关联 观察者 属性
    objc_setAssociatedObject(self, (__bridge const void *)@"objc", observer, OBJC_ASSOCIATION_ASSIGN);

}

//IMP ----setter:
void kvo_setter(id self, SEL _cmd, NSString *name){
    //改变父类的属性值
    struct objc_super superClass = {
        self,
        class_getSuperclass([self class])
    };
    
    //调用父类
    objc_msgSendSuper(&superClass, _cmd, name);
    
    //获取观察者
    id observer = objc_getAssociatedObject(self, (__bridge const void *)@"objc");
    
    //获取setter方法名
    NSString *methodName = NSStringFromSelector(_cmd);
    //settName:   获取 name
    NSString *key = getValueKey(methodName);
    
    //通知观察者变化 调用observeValueForKeyPath
    objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), key, self, @{key:name}, nil);
}

NSString *getValueKey(NSString *setter)
{
    //在setter方法中截取属性key 如 setName: 中截取 name,没有做容错
    NSRange range = NSMakeRange(3, setter.length-4);
    NSString *key = [setter substringWithRange:range];
    key = [key lowercaseString];
    
    return key;
}


此时我们调用自己定义的监听方法, 效果和系统的也是一样的

[_kvo1 m_addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

补充

  1. 直接修改成员变量不会触发 KVO,因为没有通过 setter 方法。
  2. 通过 KVC 修改属性会触发 KVO。
上一篇 下一篇

猜你喜欢

热点阅读