阅读 429

好好看看 KVC && KVO

基本用法

字典快速赋值

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

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。

关注下面的标签,发现更多相似文章
评论