iOS--KVO

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。通过遵循KVO Compliance中描述的步骤,可以确保自己的类符合KVO。通常情况下,苹果提供的框架中的属性只有在上面文档记录的才符合KVO。

  • addObserver:forKeyPath:options:context:方法不对观察对象、被观察对象或上下文的强引用。如有必要应该确保维护对观察对象、被观察对象和上下文的强引用。

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

  • 如果尚未注册为观察员,则请求将其作为观察员删除会导致 NSRangeExceptionaddObserver:forKeyPath:options:context:removeObserver:forKeyPath:context:要对应,或者在try/catch块内调用removeObserver:forKeyPath:context:以处理潜在异常。

  • 释放时,观察者不会自动删除自身。被观察对象继续发送通知,而不考虑观察者的状态。但是,与任何其他消息一样,发送到已释放对象的更改通知会触发内存访问异常。因此,要确保观察者在从内存中释放之前将自己移除。

  • 协议没有提供询问对象是观察者还是被观察者的方法。构造代码以避免与释放release相关的错误。一种典型的模式是在观察者初始化期间(例如在init或viewDidLoad中)注册为观察者,并在释放期间注销(通常在dealoc中),确保正确配对和有序地添加和删除消息,并且在将观察者从内存中释放之前取消注册。

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函数

  • willChangeValueForKey:
  • 父类K原来的 setter
  • didChangeValueForKey:,其内部会触发监听器(Oberser)的监听方法:- (void)observeValueForKeyPath: ofObject: change: context:

示例验证(这里需要引入一个分类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个方法:

  • - (void)setName:(id)arg0:最主要的重写方法,set值时调用通知函数;
  • - (class)class隐藏自己,返回原来类的class;
  • - (void)dealloc清理监听时的动态修改;
  • - (BOOL)_isKVOA内部使用的标示,判断这个类有没被KVO动态生成子类。
    iOS--KVO_第1张图片
    image.png

KVO 的缺点:

  • 只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知,在复杂的业务逻辑中,准确判断被观察者相对比较麻烦。
  • 需要手动移除观察者,且移除观察者的时机必须合适;
  • 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过 void * 指针。

自己代码实现KVO

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

//NSObject+KVO.h
#import 
@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 
#import 

@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。
  • KVC 通过 setValue: forKey:setValue: forKeyPath:赋值
  • KVC 在修改值得前后会分别自动调用willChangeValueForKey和didChangeValueForKey:
    • (BOOL)accessInstanceVariablesDirectly是否可以访问成员变量默认是 YES, 可以访问,所当根据 key找到的是成员变量时(没有setKey:和 _setKey:方法)也会触发 KVO


      iOS--KVO_第2张图片
      setValue: forKey原理

你可能感兴趣的:(iOS--KVO)