1.iOS开发之--KVO详解

作者: 木子心语 | 来源:发表于2018-03-31 21:48 被阅读472次

    如果你从事iOS开发,
    对于KVO肯定不陌生.
    今天写了这篇文章,
    让我们进一步了解KVO,
    我们从下图几个部分了解KVO,
    如果你看了这篇文章,
    会颠覆你对KVO的认知,
    原来你了解过得KVO只是一部分.


    KVO.png

    1.什么是KVO?

    KVO(Key Value Observing, 键值观察)是Objective-C对观察者模式的实现,每次当被观察对象的某个属性值发生改变时,注册的观察者便能获得通知。

    2.KVO的使用

    这里我们结合代码了解KVO,
    单纯的文字描述已经没有发言权.

    • 案例一:
      1.1我们先创建一个person类
    #import <Foundation/Foundation.h>
    @interface Person : NSObject{
    @property(nonatomic,copy)NSString *name;
    @end
    
    #import "Person.h"
    @implementation Person
    @end
    

    并为person添加属性name

    此时,我们已经创建好了person类

    1.2接着,我们在viewDidLoad方法中创建Person对象

    //方便引用
    @property(nonatomic,strong)Person *p;
    
     Person *p = [[Person alloc]init];
        //引用
     _p = p;
    

    p指针在哪个地方? p指针指向引用数据类型,对象在堆区,指针在栈区.

    1.3接着我们在ViewDidLoad方法中添加观察

    KVO-添加观察者—options.png
     [p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    
    

    1.4使用touchesBegan时间,改变属性值,以方便进行观察.

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        static int a;
        _p.name = [NSString stringWithFormat:@"%d",a++];
    }
    

    1.5此时我们运行会不会有结果呢?
    答案是我们点击屏幕的时候,程序会崩溃.因为我们这里没有去响应.
    我们要添加响应的方法

    //响应方法
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        //观察者观察name的变化,当点击屏幕,改变name的值,chang就会捕获新值.
        NSLog(@"%@",change);
    }
    

    1.6,此时你运行起来点屏幕几下.控制台就会有输出了.

    2018-03-31 18:38:54.319824+0800 001--kvo使用[2725:50940] {
        kind = 1;
        new = 0;
    }
    

    案例一,创建了基本简答的KVO.

    • 案例二

    如果我们改变了name的属性,observe回调就来了,这就是自动触发.
    还有只用,变化了不一定每一次都通知,平常开发需求也会遇到.
    如果满足了某一个条件的时候,我们来通知一下.

    2.1我们在person中添加一个方法

    +(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
        //手动触发
        return NO;
        //自动触发
       // return YES;
    }
    

    2.2这个时候,我们回到ViewController中,对,touchBegan方法进行修改,就可以完成手动触发.

        static int a;
        //手动触发
        //即将改变
        [_p willChangeValueForKey:@"name"]
        _p.name = [NSString stringWithFormat:@"%d",a++];
        // 改变完成
        [_p didChangeValueForKey:@"name"];
    

    2.3如果我们想满足一个条件后,进行属性观察.
    我们就要在 automaticallyNotifiesObserversForKey:(NSString *)key
    通过判断key来进行手动触发或者自动触发.
    比如,我们判断是name的时候,我们手动触发

        //手动触发
        if([key isEqualToString:@"name"]){
            return NO;
        }
        return YES;
    

    案例二,手动触发,自动触发

    • 案例三
      我们创建一个dog类,.我们想观察person中的dog属性的变化,我们可以这样注册.
      3.1首先,我们先创建dog类
    #import <Foundation/Foundation.h>
    @interface Dog : NSObject
    @property(nonatomic,assign)int age;
    @end
    
    #import "Dog.h"
    @implementation Dog
    @end
    

    3.2我们在person中加入

    @property(nonatomic,strong)Dog *dog;
    

    3.3对dog进行初始化

    -(instancetype)init{
        if(self=[super init]){
            _dog=[[Dog alloc]init];
        }
        return self;
    }
    

    3.4我们添加观察者

    [p addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
    

    3.5在点击事件中,改变age的值

     _p.dog.age = a++;
    

    我们观察到dog的age值变化

    案例四:
    4.1我们在dog类中加入level

    @property(nonatomic,assign)int level;
    

    4.2通过person中的dog属性,我们观察dog中的level和age属性的变化

        [p addObserver:self forKeyPath:@"dog.age" options:(NSKeyValueObservingOptionNew) context:nil];
    
        [p addObserver:self forKeyPath:@"dog.level" options:(NSKeyValueObservingOptionNew) context:nil];
    
    

    4.3在点击事件中,改变age和level

    //    _p.dog.age = a++;
    //    _p.dog.level = a++;
    

    4.4运行后,我们就可以看到属性值的变化.

    案例四,是不是有些麻烦呢,这种方式我们要写两次

    • 案例五:

    5.1.我们直接观察dog中所有属性值的变化

    [p addObserver:self forKeyPath:@"dog" options:(NSKeyValueObservingOptionNew) context:nil];
    

    5.2.这时我们在person中加入类方法keyPathsForValuesAffectingValueForKey,返回一个容器

    +(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
        
        NSLog(@"%@",key);
        
        //可不可以观察到dog属性
        NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
        if([key isEqualToString:@"dog"]){
            
            NSArray *aggectingkeys = @[@"_dog.age",@"_dog.level"];
            
          keyPaths =  [keyPaths setByAddingObjectsFromArray:aggectingkeys];
        }
        return keyPaths;
    }
    

    案例五,通过把dog属性放到集合中,观察dog的属性值变化

    • 案例六:

    6.1我们在person中加入数组属性

    @property(nonatomic,strong)NSMutableArray *array;
    

    6.2初始化数组

    -(instancetype)init{
        if(self=[super init]){
            _array = NSMutableArray.array;
        }
        return self;
    }
    

    6.3我们添加观察者

    [p addObserver:self forKeyPath:@"array" options:(NSKeyValueObservingOptionNew) context:nil];
    

    6.4点击事件中

    [_p.array addObject:@"123"];
    

    6.5,把我们的程序跑起来看看.控制台并没有输出
    当我们po这个数组的时候,发现数组中是有数据的.

    案例六,array改变,却观察不到.看来不是所有的属性都能观察到
    其实根本原因是,addobject 并没有setter方法.观察者观察到的是setter方法的改变.

    • 案例七:

    oc属性封装 _成员变量+ getter setter
    能观察属性的变化,getter有可能么?no
    属性变化,两种方法--1.直接修改成员变量;2.通过setter方法修改
    kvo观察属性变化,要么观察成员变量,要么观察setter,要么都观察
    我们来验证一下,观察者观察的是不是setter方法呢?

    7.1我们在person中,加入成员变量_name

    {
        @public
        NSString * _name;
    }
    

    7.2添加观察者

    [p addObserver:self forKeyPath:@"_name" options:(NSKeyValueObservingOptionNew) context:nil];
    

    7.3 touchbegan中加入

    _p->_name = [NSString stringWithFormat:@"%d",a++];
    

    7.3运行程序,点击没有输出.说明观察者观察的是setter方法.

    3.自定义KVO

    1.1我们首先创建person类,加入name 属性

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    @property(nonatomic,copy)NSString *name;
    @end
    
    #import "Person.h"
    @implementation Person
    @end
    

    1.2.我们NSObject +LKKVO类

    #import <Foundation/Foundation.h>
    @interface NSObject (LKKVO)
    - (void)LK_addObserver:(NSObject *_Nullable)observer forKeyPath:(NSString *_Nonnull)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    @end
    
    #import "NSObject+LKKVO.h"
    #import <objc/message.h>
    
    @implementation NSObject (LKKVO)
    
    - (void)LK_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
    {
        //1.动态创建一个类
        NSString *oldClassName= NSStringFromClass(self.class);
        NSString *newClassName = [@"LKKVO" stringByAppendingString:oldClassName];
        //1.1创建类
        Class myClass =objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
        //1.2注册类
        objc_registerClassPair(myClass);
        //2.添加setName--目的为了重写
        class_addMethod(myClass, @selector(setName:), (IMP)lk, "");
        //3.修改类型
        object_setClass(self, myClass);
    }
    
    void lk(id self,SEL _cmd ,NSString * newName){
        NSLog(@"%@\n%@\n%@",self,NSStringFromSelector(_cmd) ,newName);
    }
    @end
    

    自定义我们自己的KVO后,我们来使用

    1.3.我们在VC中

    @property(nonatomic,strong)Person *p;
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        Person * p = [[Person alloc]init];
    
        _p = p;
        
        [_p LK_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    
    }
    

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    static int a ;
    _p.name = [NSString stringWithFormat:@"%d",a++];
    

    }

    1.4我的程序跑起来,控制台输出

    2018-03-31 20:52:55.460559+0800 002-自定义kvo[3231:106523] <LKKVOPerson: 0x600000004ee0>
    setName:
    0
    

    在自定义自己的kvo的时候,1.通过运行时机制动态创建一个类2.添加setName3.修改类型.

    4.KVO观察容器

    • 1.KVO --响应式编程
      首先,我们需要添加,订阅,然后有响应的方法
      代理是么?其实代理也一样
      比如tableView,我们添加代理,就可以使用代理方法,代理方法回调.
      比如button时间,其实也是响应式编程,为button添加target,开启方法,方法调用.

    • 2.我们在案例六中提到了数组,我们初始化后并没有观察到person中array属性的变化
      我们这次就要借助KVC
      2.1我们创建person类,加入arr属性

    @property(nonatomic,strong)NSMutableArray *arr;
    

    2.2初始化数组

    - (instancetype)init
    {
        self = [super init];
        if (self) {
            _arr = NSMutableArray.array;
        }
        return self;
    }
    

    2.3在VC中

    @property(nonatomic,strong)Person *p;
    

    2.4创建person对象,添加观察者

     Person *p = [[Person alloc]init];
        
        _p = p;
    
    [_p addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:nil];
    

    2.5响应方法

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

    2.6touchesBegan中加入

    //KVO监听容器方法,需要借助KVC
    
    NSMutableArray *tempArr =    [_p mutableArrayValueForKeyPath:@"arr"];
    [tempArr addObject:@"123"];
    

    2.7程序跑起来,我们在控制台可以看到

    2018-03-31 21:15:13.551030+0800 003-KVO观察容器[3369:118039] {
        indexes = "<_NSCachedIndexSet: 0x60400003ff60>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
        kind = 2;
        new =     (
            11
        )
    

    这里的kind是什么意思?
    在案例1中输出的kind=1,这里的kind=2.这代表了什么意思?

    //set方法,kind=1
    NSKeyValueChangeSetting = 1,
    //插入
    NSKeyValueChangeInsertion = 2,
    //删除
    NSKeyValueChangeRemoval = 3,
    //替换
    NSKeyValueChangeReplacement = 4,
    

    有没有颠覆KVO调setter方法,还可以调用insert,remove,replace
    这里输出的kind2,说明了插入的数据的时候,观察者也可以观察到属性值的变化.
    如果没有深入了解一下KVO,知道了KVO不仅可以调用setter方法,还可以调用插入,删除,代替方法.

    总结

    1.动态创建Person的子类使用
    2.子类重写setName
    3.动态修改了对象的类型
    4.还了解到了,KVO不仅可以调用setter方法,还可以调用插入,删除,代替方法.通过对kvo的进一步了解,我们是很清楚KVO底层运用.
    好的,这就是KVO.
    这篇文章能帮到您,
    请您点个赞和来波鲜花.
    代码会放到github,
    会把地址放到下面.

    相关文章

      网友评论

        本文标题:1.iOS开发之--KVO详解

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