好好看看 KVC && KVO

栏目: IOS · 发布时间: 5年前

内容简介:KVC 可以将字典里面和 model 同名的 property 进行快速赋值但是这里会有2种特殊情况。运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null

KVC 可以将字典里面和 model 同名的 property 进行快速赋值 setValuesForKeysWithDictionary

//前提:model 中的各个 property 必须和 NSDictionary 中的属性一致
- (instancetype)initWithDic:(NSDictionary *)dic{
    BannerModel *model = [BannerModel new];
    [model setValuesForKeysWithDictionary:dic];
    return model;  
}
复制代码

但是这里会有2种特殊情况。

  • 情况一:在 model 里面有 property 但是在 NSDictionary 里面没有这个值

运行上面的代码,代码不崩溃,只不过在输出值的时候输出了 null

  • 情况二:在 NSDictionary 中存在某个值,但是在 model 里面没有值

运行后编译成功,但是代码奔溃掉。原因是 KVC 。所以我们只需要实现这么一个方法。甚至不需要写函数体部分

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
    
}
复制代码
  • 情况三:如果 Dictionary 和 Model 中的 property 不同名

我们照样可以利用 setValue:forUndefinedKey: 去处理

//model
@property (nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *sex;
@property (nonatomic,copy) NSString* age;
//NSDictionary
NSDictionary *dic = @{@"username":@"张三",@"sex":@"男",@"id":@"22"};

-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
    if([key isEqualToString:@"id"]){
        self.age=value;
    }
    if([key isEqualToString:@"username"]){
        self.name=value;
    }
}    
复制代码
  • 情况四:如果我们观察对象的属性是数组,我们经常会观察不到变化,因为 KVO 是观察 setter 方法。我们可以用 mutableArrayValueForKeyPath 进行属性的操作
NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
[hobbies addObject:@"Web"];
复制代码
  • 情况五: 注册依赖键.

KVO 可以观察属性的二级属性对象的所有属性变化。说人话就是“假如 Person 类有个 Dog 类,Dog 类有 name、fur、weight 等属性,我们给 Person 的 Dog 属性观察,假如 Dog 的任何属性变化是,Person 的观察者对象都可以拿到当前的变化值。我们只需要在 Person 中写下面的方法即可”

[self.person  addObserver:self
        forKeyPath:NSStringFromSelector(@selector(dog))
           options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
           context:ContextMark];

self.person.dog.name = @"啸天犬";
self.person.dog.weight = 50;


// Person.m
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ([key isEqualToString:@"dog"]) {
        NSArray *affectingKeys = @[@"name", @"fur", @"weight"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}
复制代码

KVO 的本质

kVO 是Objective-C 对观察者模式的实现。也是 Cocoa Binding 的基础。

几个基本的知识点

  1. KVO 观察者和属性被观察的对象之间不是强引用的关系

  2. KVO 的触发分为 自动触发模式手动触发模式 2种。通常我们使用的都是自动通知,注册观察者之后,当条件触发的时候会自动调用 -(void)observeValueForKeyPath... 如果需要实现手动通知,我们需要使用下面的方法

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}
复制代码
  1. 若类有实例变量 NSString *_foo, 调用 setValue:forKey: 是以 foo 还是 _foo 作为 key ?

都可以

  1. KVC 的 keyPath 中的集合运算符如何使用
  • 必须用在 集合对象 或者 普通对象的集合属性

-简单的集合运算符有 @avg、@count、@max、@min、@sum

  1. KVO 和 KVC 的 keyPath 一定是属性吗? 可以是成员变量

实现机制

Automatic key-value observing is implemented using a technique called isa-swizzling... 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 ...

Apple 文档告诉我们:被观察对象的 isa指针 会指向一个中间类,而不是原来真正的类,

通过对被观察的对象断点调试发现 Person 类在执行过 addObserveValueForKeyPath... 方法后 isa 改变了。NSKVONotifying_Person。

  • KVO 是基于 Runtime 机制实现的

  • 当某个类的属性第一次被观察的时候,系统会在运行期动态的创建该类的一个派生类。在派生类中重写任何被观察属性的 setter 方法。派生类在真正实现 通知机制

  • 如果当前类为 Person,则生成的派生类名称为 NSKVONotifying_Person

  • 每个类对象中都有一个 isa指针 指向当前类,当一个类对象第一次被观察的时候,系统会偷偷将 isa 指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是当前派生类的 setter 方法

  • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey:、didChangeValueForKey: 。在一个被观察属性改变之前,调用 willChangeValueForKey: 记录旧的值。在属性值改变之后调用 didChangeValueForKey: ,从而 observeValueForKey:ofObject:change:context: 也会被调用。

好好看看 KVC && KVO

为什么要选择是继承的子类而不是分类呢? 子类在继承父类对象,子类对象调用调方法的时候先看看当前子类中是否有方法实现,如果不存在方法则通过 isa 指针顺着继承链向上找到父类中是否有方法实现,如果父类种也不存在方法实现,则继续向上找...直到找到 NSObject 类为止,系统会抛出几次机会给 程序员 补救,如果未做处理则奔溃

关于分类与子类的关系可以看看我之前的文章.

模拟实现系统的 KVO

willChangeValueForKey、didChangeValueForKey

我们用自己的类模拟系统的 KVO。

//NSObject+LBPKVO.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (LBPKVO)

- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
 
@end

NS_ASSUME_NONNULL_END

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

@implementation NSObject (LBPKVO)


- (void)lbpKVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context {
    //生成自定义的名称
    NSString *className = NSStringFromClass(self.class);
    NSString *currentClassName = [@"LBPKVONotifying_" stringByAppendingString:className];
    //1. runtime 生成类
    Class myclass = objc_allocateClassPair(self.class, [currentClassName UTF8String], 0);
    // 生成后不能马上使用,必须先注册
    objc_registerClassPair(myclass);
    
    //2. 重写 setter 方法
    class_addMethod(myclass,@selector(setName:) , (IMP)setName, "v@:@");
    //3. 修改 isa
    object_setClass(self, myclass);
    
    //4. 将观察者保存到当前对象里面
    objc_setAssociatedObject(self, "observer", observer, OBJC_ASSOCIATION_ASSIGN);
    
    //5. 将传递的上下文绑定到当前对象里面
    objc_setAssociatedObject(self, "context", (__bridge id _Nullable)(context), OBJC_ASSOCIATION_RETAIN);
}


//
void setName (id self, SEL _cmd, NSString *name) {
    NSLog(@"come here");
    //先切换到当前类的父类,然后发送消息 setName,然后切换当前子类
    //1. 切换到父类
    Class class = [self class];
    object_setClass(self, class_getSuperclass(class));
    //2. 调用父类的 setName 方法
    objc_msgSend(self, @selector(setName:), name);
    
    //3. 调用观察
    id observer = objc_getAssociatedObject(self, "observer");
    id context = objc_getAssociatedObject(self, "context");
    if (observer) {
        objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:), @"name", self, @{@"new": name, @"kind": @1 } , context);
    }
    //4. 改回子类
    object_setClass(self, class);
}

@end


//ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [[Person alloc] init];
    _person.name = @"杭城小刘";
    _person.age = 23;
    _person.hobbies = [@[@"iOS"] mutableCopy];
    NSDictionary *context = @{@"name": @"成吉思汗", @"hobby" :  @"弯弓射大雕"};
    [_person lbpKVO_addObserver:self forKeyPath:@"hobbies" options:(NSKeyValueObservingOptionNew) context:(__bridge void * _Nullable)(context)];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    _person.name = @"刘斌鹏";
    NSMutableArray *hobbies = [_person mutableArrayValueForKeyPath:@"hobbies"];
    [hobbies addObject:@"Web"];
}

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

复制代码

KVO 的缺陷

KVO 虽然很强大,你只能重写 -observeValueForKeyPath:ofObject:change:context: 来获得通知,想要提供自定义的 selector ,不行;想要传入一个 block,没门儿。感觉如果加入 block 就更棒了。

KVO 的改装

看到官方的做法并不是很方便使用,我们看到无数的优秀框架都支持 block 特性,比如 AFNetworking ,所以我们可以将系统的 KVO 改装成支持 block。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

无处不在的算法

无处不在的算法

[德]贝特霍尔德·弗金、赫尔穆特·阿尔特 / 机械工业出版社 / 2018-1-1

本书以简单易懂的写作风格,通过解决现实世界常见的问题来介绍各种算法技术,揭示了算法的设计与分析思想。全书共有41章,分为四大部分,图文并茂,把各种算法的核心思想讲得浅显易懂。本书可作为高等院校算法相关课程的本科生教材,也可作为研究人员、专业技术人员的常备参考书。一起来看看 《无处不在的算法》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具