美文网首页
iOS-KVO浅谈

iOS-KVO浅谈

作者: 梦蕊dream | 来源:发表于2018-05-16 14:42 被阅读52次

    上一篇:iOS-KVC浅谈

    前言:KVO 作为 KVC 的同袍兄弟,功能更强大,聊聊 KVO。

    一、KVO 简介

    1.1 KVO 概述

    1.KVO 是键值观察者(key-value-observing)。
    2.KVO提供了一种观察者的机制,通过对某个对象的某个属性添加观察者,当该属性改变,就会调用"observeValueForKeyPath:"方法,为我们提供一个“对象值改变了!”的时机进行一些操作。
    3.KVO 是一个观察者模式。观察一个对象的属性,注册一个指定的路径,若这个对象的的属性被修改,则 KVO 会自动通知观察者。
    4.基本思想:一个目标对象管理所有依赖于它的观察者对象,并在它自身的状态改变时主动通知观察者对象。这个主动通知通常是通过调用各观察者对象所提供的接口方法来实现的。观察者模式较完美地将目标对象与观察者对象解耦。
    5.任何对象都允许观察其他对象的属性,并且可以接收其他对象状态变化的通知。
    6.Objective-C 中有两种使用键值观察的方式:手动或自动,此外还支持注册依赖键(即一个键依赖于其他键,其他键的变化也会作用到该键)。

    二、KVO 应用

    2.1 基本使用

      1. 注册观察者,实施监听;
    [self.person addObserver:self
                  forKeyPath:@"age"
                     options:NSKeyValueObservingOptionNew
                     context:nil];
    
      1. 回调方法,在这里处理属性发生的变化;
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary<NSString *,id> *)change
                           context:(void *)context
    
      1. 移除观察者;
    [self removeObserver:self forKeyPath:@"age"];
    

    代码示例

    _person = [[Person alloc] init];
        
    /**
     *  添加观察者
     *
     *  @param observer 观察者
     *  @param keyPath  被观察的属性名称
     *  @param options  观察属性的新值、旧值等的一些配置(枚举值,可以根据需要设置,例如这里可以使用两项)
     *  注: options: NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld 返回未改变之前的值和改变之后的值    context可以为空
     *  @param context  上下文,可以为nil。
     */
    [_person addObserver:self
              forKeyPath:@"age"
                 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                 context:nil];
    /**
     *  KVO回调方法
     *
     *  @param keyPath 被修改的属性
     *  @param object  被修改的属性所属对象
     *  @param change  属性改变情况(新旧值)
     *  @param context context传过来的值
     */
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary<NSString *,id> *)change
                           context:(void *)context
    {
        NSLog(@"%@对象的%@属性改变了:%@",object,keyPath,change);
     }
    /**
     *  移除观察者
     */
    - (void)dealloc
    {
        [self.person removeObserver:self forKeyPath:@"age"];
    }
    

    2. KVO的使用场景

    KVO用于监听对象属性的改变。
      (1)下拉刷新、下拉加载监听UIScrollView的contentoffsize;
      (2)webview混排监听contentsize;
      (3)监听模型属性实时更新UI;
      (4)监听控制器frame改变,实现抽屉效果。

    监听 ScrollView 的 contentOffSet 属性:

    [scrollview addObserver:self
                 forKeyPath:@"contentOffset"                   
                    options:NSKeyValueObservingOptionNew
                    context:nil];
    

    三、键值观察

    3.1 运用键值观察

    1.注册与解除注册

    如果我们已经有了包含可供键值观察属性的类,那么就可以通过在该类的对象(被观察对象)上调用名为 NSKeyValueObserverRegistration 的 category 方法将观察者对象与被观察者对象注册与解除注册:

    • Foundation/NSKeyValueObserving.h 中,NSObject,NSArray,NSSet均实现了以下方法,因此我们不仅可以观察普通对象,还可以观察数组或结合类对象。
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    
    • NSObject 还实现了 NSKeyValueObserverNotification 的 category 方法:
      这两个方法在手动实现键值观察时会用到
    - (void)willChangeValueForKey:(NSString *)key;
    - (void)didChangeValueForKey:(NSString *)key;
    
    • 注:不要忘记解除注册,否则会导致资源泄露。
    2.设置属性

    将观察者与被观察者注册好之后,就可以对观察者对象的属性进行操作,这些变更操作就会被通知给观察者对象。注意,只有遵循 KVO 方式来设置属性,观察者对象才会获取通知,也就是说遵循使用属性的 setter 方法,或通过key-path来设置:

    [target setAge:30];
    [target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
    
    3.处理变更通知

    观察者需要实现名为 NSKeyValueObserving 的 category 方法来处理收到的变更通知:

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
    

    在这里,change 这个字典保存了变更信息,具体是哪些信息取决于注册时的 NSKeyValueObservingOptions。

    5.代码示例

    在实现处理变更通知方法observeValueForKeyPath时,要将不能处理的 key 转发给 super 的 observeValueForKeyPath 来处理。

    // Observer.h
    @interface Observer : NSObject
    @end
    
    // Observer.m
    #import "Observer.h"
    #import <objc/runtime.h>
    #import "Target.h"
    
    @implementation Observer
    
    - (void) observeValueForKeyPath:(NSString *)keyPath
                           ofObject:(id)object 
                             change:(NSDictionary *)change
                            context:(void *)context
    {
        if ([keyPath isEqualToString:@"age"])
        {
            Class classInfo = (Class)context;
            NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                      encoding:NSUTF8StringEncoding];
            NSLog(@" >> class: %@, Age changed", className);
            NSLog(@" old age is %@", [change objectForKey:@"old"]);
            NSLog(@" new age is %@", [change objectForKey:@"new"]);
        }
        else
        {
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    
    @end
    

    调用示例

    Observer * observer = [[[Observer alloc] init] autorelease];
    Target * target = [[[Target alloc] init] autorelease];
    [target addObserver:observer
             forKeyPath:@"age"
                options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                context:[Target class]];
    [target setAge:30];
    //[target setValue:[NSNumber numberWithInt:30] forKey:@"age"];
    [target removeObserver:observer forKeyPath:@"age"];
    

    输出结果:

    class: Target, Age changed
    old age is 10
    new age is 30

    3.2 手动实现键值观察

    首先,需要手动实现属性的 setter 方法,并在设置操作的前后分别调用 willChangeValueForKey: 和 didChangeValueForKey方法,这两个方法用于通知系统该 key 的属性值即将和已经变更了;
    其次,要实现类方法 automaticallyNotifiesObserversForKey,并在其中设置对该 key 不自动发送通知(返回 NO 即可)。这里要注意,对其它非手动实现的 key,要转交给 super 来处理。

    @interface Target : NSObject
    {
        int age;
    }
    // for manual KVO - age
    - (int) age;
    - (void) setAge:(int)theAge;
    @end
    
    @implementation Target
    
    - (id) init
    {
        self = [super init];
        if (nil != self)
        {
            age = 10;
        }
        return self;
    }
    
    // for manual KVO - age
    - (int) age
    {
        return age;
    }
    
    - (void) setAge:(int)theAge
    {
        [self willChangeValueForKey:@"age"];
        age = theAge;
        [self didChangeValueForKey:@"age"];
    }
    
    + (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"age"]) {
            return NO;
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    @end
    

    3.3 自动实现键值观察

    自动实现键值观察就非常简单了,只要使用了自动属性即可。

    @interface Target : NSObject
    // for automatic KVO - age
    @property (nonatomic, readwrite) int age;
    @end
    
    @implementation Target
    @synthesize age; // for automatic KVO - age
    
    - (id) init
    {
        self = [super init];
        if (nil != self)
        {
            age = 10;
        }
        
        return self;
    }
    @end
    

    3.4 键值观察依赖键

    有时候一个属性的值依赖于另一对象中的一个或多个属性,如果这些属性中任一属性的值发生变更,被依赖的属性值也应当为其变更进行标记。因此,object 引入了依赖键。

    1. 观察依赖键

    观察依赖键的方式与前面描述的一样,下面先在 Observer 的 observeValueForKeyPath:ofObject:change:context: 中添加处理变更通知的代码:

    #import "TargetWrapper.h"
    
    - (void) observeValueForKeyPath:(NSString *)keyPath
                           ofObject:(id)object 
                             change:(NSDictionary *)change
                            context:(void *)context
    {
        if ([keyPath isEqualToString:@"age"])
        {
            Class classInfo = (Class)context;
            NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                      encoding:NSUTF8StringEncoding];
            NSLog(@" >> class: %@, Age changed", className);
    
            NSLog(@" old age is %@", [change objectForKey:@"old"]);
            NSLog(@" new age is %@", [change objectForKey:@"new"]);
        }
        else if ([keyPath isEqualToString:@"information"])
        {
            Class classInfo = (Class)context;
            NSString * className = [NSString stringWithCString:object_getClassName(classInfo)
                                                      encoding:NSUTF8StringEncoding];
            NSLog(@" >> class: %@, Information changed", className);
            NSLog(@" old information is %@", [change objectForKey:@"old"]);
            NSLog(@" new information is %@", [change objectForKey:@"new"]);
        }
        else
        {
            [super observeValueForKeyPath:keyPath
                                 ofObject:object
                                   change:change
                                  context:context];
        }
    }
    
    2.实现依赖键

    在这里,观察的是 TargetWrapper 类的 information 属性,该属性是依赖于 Target 类的 age 和 grade 属性。为此,我在 Target 中添加了 grade 属性:

    @interface Target : NSObject
    @property (nonatomic, readwrite) int grade;
    @property (nonatomic, readwrite) int age;
    @end
    
    @implementation Target
    @synthesize age; // for automatic KVO - age
    @synthesize grade;
    @end
    

    TragetWrapper 中的依赖键属性是实现:

    @class Target;
    
    @interface TargetWrapper : NSObject
    {
    @private
        Target * _target;
    }
    
    @property(nonatomic, assign) NSString * information;
    @property(nonatomic, retain) Target * target;
    
    -(id) init:(Target *)aTarget;
    
    @end
    
    #import "TargetWrapper.h"
    #import "Target.h"
    
    @implementation TargetWrapper
    
    @synthesize target = _target;
    
    -(id) init:(Target *)aTarget
    {
        self = [super init];
        if (nil != self) {
            _target = [aTarget retain];
        }
        
        return self;
    }
    
    -(void) dealloc
    {
        self.target = nil;
        [super dealloc];
    }
    
    - (NSString *)information
    {
        return [[[NSString alloc] initWithFormat:@"%d#%d", [_target grade], [_target age]] autorelease];
    }
    
    - (void)setInformation:(NSString *)theInformation
    {
        NSArray * array = [theInformation componentsSeparatedByString:@"#"];
        [_target setGrade:[[array objectAtIndex:0] intValue]];
        [_target setAge:[[array objectAtIndex:1] intValue]];
    }
    
    + (NSSet *)keyPathsForValuesAffectingInformation
    {
        NSSet * keyPaths = [NSSet setWithObjects:@"target.age", @"target.grade", nil];
        return keyPaths;
    }
    
    //+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
    //{
    //    NSSet * keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    //    NSArray * moreKeyPaths = nil;
    //    
    //    if ([key isEqualToString:@"information"])
    //    {
    //        moreKeyPaths = [NSArray arrayWithObjects:@"target.age", @"target.grade", nil];
    //    }
    //    
    //    if (moreKeyPaths)
    //    {
    //        keyPaths = [keyPaths setByAddingObjectsFromArray:moreKeyPaths];
    //    }
    //    
    //    return keyPaths;
    //}
    
    @end
    
    • 首先,要手动实现属性 information 的 setter/getter 方法,在其中使用 Target 的属性来完成其 setter 和 getter。
    • 其次,要实现 keyPathsForValuesAffectingInformation 或 keyPathsForValuesAffectingValueForKey: 方法来告诉系统 information 属性依赖于哪些其他属性,这两个方法都返回一个key-path 的集合。如果选择实现 keyPathsForValuesAffectingValueForKey,要先获取 super 返回的结果 set,然后判断 key 是不是目标 key,如果是就将依赖属性的 key-path 结合追加到 super 返回的结果 set 中,否则直接返回 super的结果。
    • information 属性依赖于 target 的 age 和 grade 属性,target 的 age/grade 属性任一发生变化,information 的观察者都会得到通知。
    3.使用示例
    Observer * observer = [[[Observer alloc] init] autorelease];
    Target * target = [[[Target alloc] init] autorelease];
    
    TargetWrapper * wrapper = [[[TargetWrapper alloc] init:target] autorelease];
    [wrapper addObserver:observer
              forKeyPath:@"information"
                 options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                 context:[TargetWrapper class]];
    
    [target setAge:30];
    [target setGrade:1];
    [wrapper removeObserver:observer forKeyPath:@"information"];
    

    输出结果:
    class: TargetWrapper, Information changed
    old information is 0#10
    new information is 0#30
    class: TargetWrapper, Information changed
    old information is 0#30
    new information is 1#30

    四、KVO 原理

    Key-Value Observing Implementation Details
    Automatic key-value observing is implemented using a technique called isa-swizzling.
    The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
    When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
    You should never rely on the isa pointer to determine class membership. Instead, you should use the [class](http://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/Reference/NSObject.html#//apple_ref/occ/intfm/NSObject/class) method to determine the class of an object instance.

    • 当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类
    • 在这个派生类中重写基类中任何被观察属性的 setter 方法。
    • 派生类在被重写的 setter 方法实现真正的通知机制,就如前面手动实现键值观察那样。
    • 基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。
    • 前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。
    • 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。
    • 系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。
    • 派生类还重写了 dealloc 方法来释放资源。
    • 当一个观察者注册对象的一个属性 isa 观察对象的指针被修改,指着一个中间类而不是在真正的类。
    • isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 runtime 系统这个对象的类是什么。

    4.1 派生类 NSKVONotifying_Person 剖析:

    在这个过程,被观察对象的 isa 指针从指向原来的 Person 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_Person 类,来实现当前类属性值改变的监听。

    所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为 NSKVONotifying_Person 的类(),就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_Person 的中间类,并指向这个中间类了。

    因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。

    4.2 子类 setter 方法剖析:

    1.KVO 在调用存取方法之前总是调用 willChangeValueForKey:,通知系统该 keyPath 的属性值即将变更。
    2.当改变发生后,didChangeValueForKey:被调用,通知系统该 keyPath 的属性值已经变更。
    3.之后,observeValueForKey:ofObject:change:context:也会被调用。

    重写观察属性的 setter 方法这种方式是在运行时而不是编译时实现的。 KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:

    - (void)setName:(NSString *)newName
    {
        [self willChangeValueForKey:@"name"];    // KVO在调用存取方法之前总调用
        [super setValue:newName forKey:@"name"]; // 调用父类的存取方法
        [self didChangeValueForKey:@"name"];     // KVO在调用存取方法之后总调用
    }
    
    KVO原理图

    总结:
    KVO 的本质就是监听对象的属性进行赋值的时候有没有调用 setter 方法

    • 系统会动态创建一个继承于 Person 的 NSKVONotifying_Person
    • person 的 isa 指针指向的类 Person 变成 NSKVONotifying_Person,所以接下来的 person.age = newAge 的时候,他调用的不是 Person 的 setter 方法,而是 NSKVONotifying_Person(子类)的 setter 方法
    • 重写NSKVONotifying_Person的setter方法:[super setName:newName]
    • 通知观察者告诉属性改变。
    补充
    • Apple 使用了 isa 混写(isa-swizzling)来实现KVO
    • 使用setter方法改变值 KVO生效
    • 使用setValue:forKey: 改变值 KVO生效
    • 成员变量直接修改值 KVO失效,必须手动添加方法才生效
    • 系统利用运行时动态创建一个 NSKVONotifying_Person的子类,改写isa指针的指向,并重写子类的setter方法

    相关文章

      网友评论

          本文标题:iOS-KVO浅谈

          本文链接:https://www.haomeiwen.com/subject/plvpdftx.html