[iOS] KVC KVO

作者: TYM | 来源:发表于2016-04-01 14:11 被阅读211次

    KCV

    • 其实由于ObjC的语言特性,你根部不必进行任何操作就可以进行属性的动态读写,这种方式就是Key Value Coding(简称KVC)。
    • 键值对编码意思是,能够通过数据成员的名字来访问到它的值
    • KVC的操作方法由NSKeyValueCoding协议提供,而NSObject就实现了这个协议,也就是说ObjC中几乎所有的对象都支持KVC操作,常用的KVC操作方法如下:
      • 动态设置: setValue:属性值 forKey:属性名(用于简单路径)、**setValue:属性值 forKeyPath:属性路径
        **(用于复合路径,例如Person有一个Account类型的属性,那么person.account就是一个复合属性)
      • 动态读取: **valueForKey:属性名
        valueForKeyPath:属性名
        **(用于复合路径)

    KVC的使用

    KVC 动态设置

    • 单层设值(简单路径)
      • - (void)setValue:(nullable id)value forKey:(NSString *)key;
        People *bPel = [People new];
        // 单层设值
        [bPel setValue:@"谭谭谭" forKey:@"name"];
        [bPel setValue:@(20) forKey:@"age"];
    
    • 多层设置(复合路径xxx.xxx)
      • - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
        People *bPel = [People new];
        bPel.dog = [[Dog alloc] init];
        [bPel setValue:@(20) forKeyPath:@"age"];
        [bPel setValue:@"啊啊啊" forKeyPath:@"dog.name"];
        [bPel setValue:@(300) forKeyPath:@"dog.price"];
    
    • 私有成员设值
        @implementation People
        {
            double _hight;
            @private int _score;
        }
        // 创建people对象
        People *bbPel = [People new];
        // 单层设值
        [bbPel setValue:@"我我我" forKey:@"_name"];
        // 私有成员设置
        [bbPel setValue:@"170.0" forKey:@"_hight"];
        [bbPel setValue:@"99" forKey:@"_score"];
        // 类的.h文件中写的一个方法用来看是否设置了私有成员,打印操作
        [bbPel print];
    
    • 字典转模型
      • 使用- (void)setValuesForKeysWithDictionary:方法进行字典转模型
      • 前提:字典中的key必须和模型中的属性一模一样(个数 + 名称)
      • 注意:只能对当前调用KVC方法的对象进行转换,不能对它的属性对象进行转换
      • 原理:其实就是根据字典中的key和对应的value,一个个地,通过setValeu:forKeyPath方法进行赋值,从而转换成模型
    #import <Foundation/Foundation.h>
    @class Dog;
    @interface PeopleModel : NSObject
    /** 名字*/
    @property (nonatomic, copy) NSString *name;
    /** 年龄*/
    @property (nonatomic, assign) float height;
    /** 狗*/
    @property (nonatomic, strong) Dog *dog;
    @end
     
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 创建字典
        NSDictionary *dict = @{
                               @"name":@"Sam",
                               @"height":@(1.75),
                               @"dog": @{
                                       @"name":@"WangCai"
                                       }
                               };
        PeopleModel *model = [PeopleModel new];
        // 字典转模型
        [model setValuesForKeysWithDictionary:dict];
        NSLog(@"%@", model);
        // 需要注意的是,model里面的dog应该是字典类型而不是dog类型
        // 因为setValuesForKeysWithDictionary:方法不能对对象属性进行转换
    }
    

    KVC 动态读取

    • 单层读取
      • - (id)valueForKey:(NSString *)key;
    NSString *aName = [aPel valueForKey:@"name"];
    NSLog(@"%@",aName);
    
    • 多层读取
      • - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
    NSString *dogName = [aPel valueForKeyPath:@"dog.name"];
    NSLog(@"%@",dogName);
    
    • 私有变量获取
    int age = [aPel valueForKey:@"_age"];
    
    • 模型转字典
      • - (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
    NSDictionary *dicModel = [aPel dictionaryWithValuesForKeys:@[@"name",@"age"]];
    NSLog(@"%@",dicModel);
    
    • 获取数组中对象的值
      • 单层的用valueForKey:,多层的用valueForKeyPath:
        People *ppA = [People new];
        ppA.name = @"aaa";
        ppA.age = 1;
        ppA.dog = [Dog new];
        [ppA setValue:@"wangcaiA" forKeyPath:@"dog.name"];
        
        People *ppB = [People new];
        ppB.name = @"bbb";
        ppB.age = 2;
        ppB.dog = [Dog new];
        [ppB setValue:@"wangcaiB" forKeyPath:@"dog.name"];
        
        People *ppC = [People new];
        ppC.name = @"ccc";
        ppC.age = 3;
        ppC.dog = [Dog new];
        [ppC setValue:@"wangcaiC" forKeyPath:@"dog.name"];
        
        People *ppD = [People new];
        ppD.name = @"ddd";
        ppD.age = 4;
        ppD.dog = [Dog new];
        [ppD setValue:@"wangcaiD" forKeyPath:@"dog.name"];
        
        NSArray *arr = @[ppA,ppB,ppC,ppD];
        
        NSArray *nameArr = [arr valueForKeyPath:@"dog.name"];
        NSLog(@"%@", nameArr);
    
    • 运算符功能
      • @max,@min,@avg等运算符功能
        People *ppA = [People new];
        ppA.name = @"aaa";
        ppA.age = 1;
        ppA.dog = [Dog new];
        [ppA setValue:@"wangcaiA" forKeyPath:@"dog.name"];
        
        People *ppB = [People new];
        ppB.name = @"bbb";
        ppB.age = 2;
        ppB.dog = [Dog new];
        [ppB setValue:@"wangcaiB" forKeyPath:@"dog.name"];
        
        People *ppC = [People new];
        ppC.name = @"ccc";
        ppC.age = 3;
        ppC.dog = [Dog new];
        [ppC setValue:@"wangcaiC" forKeyPath:@"dog.name"];
        
        People *ppD = [People new];
        ppD.name = @"ddd";
        ppD.age = 4;
        ppD.dog = [Dog new];
        [ppD setValue:@"wangcaiD" forKeyPath:@"dog.name"];
        
        NSArray *arr = @[ppA,ppB,ppC,ppD];
        
        NSString *maxName = [arr valueForKeyPath:@"@max.dog.name"];
        NSLog(@"%@", maxName);
        
        int maxAge = [[arr valueForKeyPath:@"@max.age"] intValue];
        NSLog(@"%d", maxAge);
    
        // 输出结果
        wangcaiD
        4
    

    KVC的原理

    KVC使用起来比较简单,但是它如何查找一个属性进行读取呢?具体查找规则(假设现在要利用KVC对a进行读取):

    • 如果是动态设置属性,
      • 1.则优先考虑调用setA方法
      • 2.如果没有该方法则优先考虑搜索成员变量_a
      • 3.如果仍然不存在则搜索成员变量a
      • 4.如果最后仍然没搜索到,则会调用这个类的setValue:forUndefinedKey:方法(注意搜索过程中不管这些方法、成员变量是私有的还是公共的都能正确设置);
    • 如果是动态读取属性,
      • 1.则优先考虑调用a方法(属性a的getter方法),
      • 2.如果没有搜索到则会优先搜索成员变量_a,
      • 3.如果仍然不存在则搜索成员变量a,
      • 4.如果最后仍然没搜索到,则会调用这个类的valueforUndefinedKey:方法(注意搜索过程中不管这些方法、成员变量是私有的还是公共的都能正确读取);

    KVC的使用场景

    • 场景 1
      • 有些控件的属性是私有的(比如readonly),但是你又想设置赋值的话
      • 这时候就可以用KVC一般用来设置一些objtevie-c内部的一些不能调用(setOnly)和设置(readOnly)的属性
    • 场景 2
    • 字典转模型

    KVO

    • 在ObjC中原生支持一种双向绑定机制,如果数据模型修改了之后会立即反映到UI视图上,它叫做Key Value Observing(简称KVO)。
    • KVO其实是一种观察者模式,利用它可以很容易实现视图组件和数据模型的分离,当数据模型的属性值改变之后作为监听器的视图组件就会被激发,激发时就会回调监听器自身
    • 在ObjC中要实现KVO则必须实现NSKeyValueObServing协议,不过幸运的是NSObject已经实现了该协议,因此几乎所有的ObjC对象都可以使用KVO。

    在ObjC中使用KVO操作常用的方法

    • 注册指定Key路径监听器
      - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
    
    • observer :哪个对象(一般是view)监听
    • keyPath :监听对象(一般是model)的哪个属性
    • options :监听到对象发送改变后,需要传递什么值
    • context :注册监听时需要传递的数据(一般用于监听多个控制器的时候进行区分)
    • 删除指定Key路径的监听器
        - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
    
        - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
    
    • observer:哪个对象监听
    • keyPath:监听对象的属性
    • context:注册监听时需要传递的数据(一般用于监听多个控制器的时候进行区分)
    • 回调监听:
        -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
    
    • keyPath:被监听的对象的属性
    • object:被监听的对象
    • change:
    • context:注册监听时需要传递的数据(一般用于监听多个控制器的时候进行区分)

    KVO的使用步骤

    1. 通过addObserver: forKeyPath: options: context:为被监听对象(它通常是数据模型)注册监听器
    2. 重写监听器的observeValueForKeyPath: ofObject: change: context:方法
    3. 删除监听器

    KVO的注意事项

    • 1.如果使用KVO监听某个对象的属性,当对象释放之前一定要移除监听
      • 不移除监听的话,会报错:
        • 比如 reason: 'An instance 0x7f9483516610 of class Person was deallocated while key value observers were still registered with it.
    • 2.KVO只能监听通过set方法修改的值
    // KVO只能监听到setter方法
    [p setAge:998];
    p.age = 998;
    //这不是setter方法,KVO监听不到
    p->_age = 998;
    

    KVO实例

    假设当商品(模型)的price价格发送改变的时候,我们商品的ShopView展示页面(视图)可以及时作出响应。那么此时ShopItemModel就作为我们的被监听对象,需要ShopVIew为它注册监听,而商品展示页面ShopVIew作为监听器需要重写它的observeValueForKeyPath: ofObject: change: context:方法,当监听的余额发生改变后会回调监听器ShopVIew监听方法(observeValueForKeyPath: ofObject: change: context:)。下面通过代码模拟上面的过程:

    ShopItemModel.h

    #import <Foundation/Foundation.h>
    
    @interface ShopItemModel : NSObject
    
    /** 商品名称*/
    @property (nonatomic, copy) NSString *name;
    /** 价格*/
    @property (nonatomic, assign) float price;
    
    @end
    

    ShopItemModel.h

    #import "ShopItemModel.h"
    
    @implementation ShopItemModel
    
    @end
    

    ShopView.h

    #import <UIKit/UIKit.h>
    @class ShopItemModel;
    @interface ShopView : UIView
    
    /** 数据模型*/
    @property (nonatomic, strong) ShopItemModel *item;
    
    @end
    

    ShopView.m

    #import "ShopView.h"
    #import "ShopItemModel.h"
    
    @interface ShopView ()
    
    /** 标签*/
    @property (nonatomic, weak) UILabel *tLabel;
    
    @end
    
    @implementation ShopView
    
    -(instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {
            self.backgroundColor = [UIColor lightGrayColor];
            // 添加一个标签
            UILabel *label = [[UILabel alloc] init];
            label.textAlignment = NSTextAlignmentCenter;
            label.text = @"价格";
            [self addSubview:label];
            self.tLabel = label;
        }
        return self;
    }
    
    -(void)layoutSubviews {
        [super layoutSubviews];
        
        CGSize size = self.frame.size;
        CGFloat labHeight = size.height * 0.5;
        CGFloat labY = (size.height - labHeight) * 0.5;
        self.tLabel.frame = CGRectMake(0, labY, size.width, labHeight);
    }
    
    #pragma mark 设置商品模型
    -(void)setItem:(ShopItemModel *)item {
        _item = item;
        
        self.tLabel.text = [NSString stringWithFormat:@"%@价格为:%f", self.item.name, self.item.price];
        
        // 注册监听
        [self.item addObserver:self forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"商品界面"];
    }
    
    #pragma mark 重写observeValueForKeyPath方法,当商品价格变化后此处获得通知
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
        if ([keyPath isEqualToString:@"price"]) {
            NSLog(@"keyPath = %@, object = %@, change = %@, context = %@", keyPath, object, change, context);
            self.tLabel.backgroundColor = [UIColor redColor];
            self.tLabel.text = [NSString stringWithFormat:@"%@价格为:%f", self.item.name, self.item.price];
            NSLog(@"%@知道价格发生变化了,价格为%f", self, self.item.price);
        }
    }
    #pragma mark 重写销毁方法
    -(void)dealloc {
        // 移除监听
        [self.item removeObserver:self forKeyPath:@"price"];
    }
    

    ViewController.m

    #import "ViewController.h"
    
    #import "ShopView.h"
    #import "ShopItemModel.h"
    
    @interface ViewController ()
    
    /** 商品view*/
    @property (nonatomic, weak) ShopView *shopView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 创建商品模型
        ShopItemModel *item = [[ShopItemModel alloc] init];
        item.name = @"包包";
        item.price = 20000;
    
        // 创建商品视图
        ShopView *sV = [[ShopView alloc] init];
        sV.frame = CGRectMake(0, 200, self.view.frame.size.width, 200);
        // 设置商品视图的模型
        sV.item = item;
        // 添加到界面
        [self.view addSubview:sV];
        self.shopView = sV;
    }
    
    #pragma mark 重写触摸事件方法
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        self.shopView.item.price = 500;  //注意!执行到这一步会触发监听器的回调函数observeValueForKeyPath: ofObject: change: context:
    }
    
    // 结果:
    keyPath = price, object = <ShopItemModel: 0x7f9119d2fca0>, change = {
        kind = 1;
        new = 500;
        old = 20000;
    }, context = 商品界面
    <ShopView: 0x7f84b1e141f0; frame = (0 200; 414 200); layer = <CALayer: 0x7f84b1e16850>>知道价格发生变化了,价格为500.000000
    
    触摸前 触摸后

    KVO原理

    只要给一个对象注册一个监听,那么在运行时:

    1. 系统给这个对象生成一个子类对象 NSKVONotifying_XXX(XXX类名)
    2. 对子类对象中的被监听属性重写setter方法,
    3. 在setter方法中通知监听者

    修改上面例子的setItem方法

    -(void)setItem:(ShopItemModel *)item {
        _item = item;
        
        self.tLabel.text = [NSString stringWithFormat:@"%@价格为:%f", self.item.name, self.item.price];
        
        // 打印被监听对象的isa
        NSLog(@"%@", [self.item valueForKey:@"isa"]);
    
        // 注册监听
        NSKeyValueObservingOptions option =  NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
        [self.item addObserver:self forKeyPath:@"price" options:option context:@"商品界面"];
    
        // 在这里打断点
    
        // 打印被监听对象的isa
        NSLog(@"%@", [self.item valueForKey:@"isa"]);
    }
    
    // 输出结果
    ShopItemModel
    NSKVONotifying_ShopItemModel
    
    • 这段代码说明,当一个对象被监听之后,会生成一个叫做NSKVONotifying_xxxx的子类对象,并将地址赋给原对象。
      • 简单地说,当一个对象被监听之后,会变成一个子类对象
      • 在子类对象中重写setter方法,里面添加通知,使得当外界调用setter方法后,会发送通知

    运行时创建子类,相当于以下代码操作

    #import <Foundation/Foundation.h>
    
    @interface NSKVONotifying_ShopItemModel : ShopItemModel
    
    @end
    
    #import "NSKVONotifying_ShopItemModel.h"
    
    @implementation NSKVONotifying_ShopItemModel
    
    // 重写了price的setter方法
    -(void)setPrice:(float)price {
        // Invoked to inform the receiver that the value of a given property is about to change.
        [self willChangeValueForKey:@"price"];
        _price = price;
        // Invoked to inform the receiver that the value of a given property has changed.
        [self didChangeValueForKey:@"price"];
    }
    
    @end
    

    注意:如果你自己定义了这样一个NSKVONotifing_xxx子类的话,会报错,因为你自己搞了一个子类,系统又需要搞一个同名子类,就搞不了,所以出错

    相关文章

      网友评论

        本文标题:[iOS] KVC KVO

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