好好看看 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。

相關文章
相關標籤/搜索