好好看看 KVC && KVO

基本用法

字典快速赋值

KVC 能够将字典里面和 model 同名的 property 进行快速赋值 setValuesForKeysWithDictionary程序员

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

可是这里会有2种特殊状况。objective-c

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

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

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

运行后编译成功,可是代码奔溃掉。缘由是 KVC 。因此咱们只须要实现这么一个方法。甚至不须要写函数体部分bash

- (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 的基础。ui

几个基本的知识点

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

  2. KVO 的触发分为自动触发模式手动触发模式2种。一般咱们使用的都是自动通知,注册观察者以后,当条件触发的时候会自动调用-(void)observeValueForKeyPath...  若是须要实现手动通知,咱们须要使用下面的方法spa

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

均可以3d

  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: 也会被调用。

KVO原理图

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

关于分类与子类的关系能够看看我以前的 文章.

模拟实现系统的 KVO

  1. 建立被观察对象的子类
  2. 重写观察对象属性的 set 方法,同时调用 willChangeValueForKey、didChangeValueForKey
  3. 外界改变 isa 指针(class方法重写)

咱们用本身的类模拟系统的 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。

相关文章
相关标签/搜索