使用 Block 實現 KVO

在iOS開發中,咱們能夠經過KVO機制來監聽某個對象的某個屬性的變化。git

用過KVO的同窗都應該知道,KVO的回調是以代理的形式實現的:在給某個對象添加觀察之後,須要在另一個地方實現回調代理方法。這種設計給人感受比較分散,所以忽然想試試用Block來實現KVO,將添加觀察的代碼和回調處理的代碼寫在一塊兒。在學習了ImplementKVO的實現之後,本身也寫了一個:SJKVOController程序員

SJKVOController的用法

只須要引入NSObject+SJKVOController.h頭文件就可使用SJKVOController。 先看一下它的頭文件:github

#import <Foundation/Foundation.h>
#import "SJKVOHeader.h"

@interface NSObject (SJKVOController)


//============== add observer ===============//
- (void)sj_addObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys withBlock:(SJKVOBlock)block;
- (void)sj_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(SJKVOBlock)block;


//============= remove observer =============//
- (void)sj_removeObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys;
- (void)sj_removeObserver:(NSObject *)observer forKey:(NSString *)key;
- (void)sj_removeObserver:(NSObject *)observer;
- (void)sj_removeAllObservers;


//============= list observers ===============//
- (void)sj_listAllObservers;

@end
複製代碼

從上面的API能夠看出,這個小輪子:編程

  1. 支持一次觀察同一對象的多個屬性。
  2. 能夠一次只觀察一個對象的一個屬性。
  3. 能夠移除對某個對象對多個屬性的觀察。
  4. 能夠移除對某個對象對某個屬性的觀察。
  5. 能夠移除某個觀察本身的對象。
  6. 能夠移除全部觀察本身的對象。
  7. 打印出全部觀察本身的對象的信息,包括對象自己,觀察的屬性,setter方法。

下面來結合Demo講解一下如何使用這個小輪子:json

在點擊上面兩個按鈕中的任意一個,增長觀察:dom

一次性添加:async

- (IBAction)addObserversTogether:(UIButton *)sender {
    
    NSArray *keys = @[@"number",@"color"];
    
    [self.model sj_addObserver:self forKeys:keys withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
        
        if ([key isEqualToString:@"number"]) {
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.numberLabel.text = [NSString stringWithFormat:@"%@",newValue];
            });
            
        }else if ([key isEqualToString:@"color"]){
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.numberLabel.backgroundColor = newValue;
            });
        }
        
    }];
}
複製代碼

分兩次添加:學習

- (IBAction)addObserverSeparatedly:(UIButton *)sender {
    
    [self.model sj_addObserver:self forKey:@"number" withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.numberLabel.text = [NSString stringWithFormat:@"%@",newValue];
        });
        
    }];
    
    [self.model sj_addObserver:self forKey:@"color" withBlock:^(id observedObject, NSString *key, id oldValue, id newValue) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.numberLabel.backgroundColor = newValue;
        });
        
    }];
    
}
複製代碼

添加之後,點擊最下面的按鈕來顯示全部的觀察信息:ui

- (IBAction)showAllObservingItems:(UIButton *)sender {
    
    [self.model sj_listAllObservers];
}
複製代碼

輸出:this

SJKVOController[80499:4242749] SJKVOLog:==================== Start Listing All Observers: ==================== 
SJKVOController[80499:4242749] SJKVOLog:observer item:{observer: <ViewController: 0x7fa1577054f0> | key: color | setter: setColor:}
SJKVOController[80499:4242749] SJKVOLog:observer item:{observer: <ViewController: 0x7fa1577054f0> | key: number | setter: setNumber:}
複製代碼

在這裏我重寫了description方法,打印出了每一個觀察的對象和key,以及setter方法。

如今點擊更新按鈕,則會更新model的number和color屬性,從而觸發KVO:

- (IBAction)updateNumber:(UIButton *)sender {
    
    //trigger KVO : number
    NSInteger newNumber = arc4random() % 100;
    self.model.number = [NSNumber numberWithInteger:newNumber];
    
    //trigger KVO : color
    NSArray *colors = @[[UIColor redColor],[UIColor yellowColor],[UIColor blueColor],[UIColor greenColor]];
    NSInteger colorIndex = arc4random() % 3;
    self.model.color = colors[colorIndex];
}
複製代碼

咱們能夠看到中間的Label上面顯示的數字和背景色都在變化,成功實現了KVO:

同時觀察顏色和數字的變化

如今咱們移除觀察,點擊remove按鈕

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    [self.model sj_removeAllObservers];   
}
複製代碼

在移除了全部的觀察者之後,則會打印出:

SJKVOController[80499:4242749] SJKVOLog:Removed all obserbing objects of object:<Model: 0x60000003b700>
複製代碼

並且若是在這個時候打印觀察者list,則會輸出:

SJKVOController[80499:4242749] SJKVOLog:There is no observers obserbing object:<Model: 0x60000003b700>
複製代碼

須要注意的是,這裏的移除能夠有多種選擇:能夠移某個對象的某個key,也能夠移除某個對象的幾個keys,爲了驗證,咱們能夠結合list方法來驗證一下移除是否成功:

驗證1:在添加number和color的觀察後,移除nunber的觀察:

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    [self.model sj_removeObserver:self forKey:@"number"];
}
複製代碼

在移除之後,咱們調用list方法,輸出:

SJKVOController[80850:4278383] SJKVOLog:==================== Start Listing All Observers: ====================
SJKVOController[80850:4278383] SJKVOLog:observer item:{observer: <ViewController: 0x7ffeec408560> | key: color | setter: setColor:}
複製代碼

如今只有color屬性被觀察了。看一下實際的效果:

只觀察顏色的變化

咱們能夠看到,如今只有color在變,而數字沒有變化了,驗證此移除方法正確。

驗證2:在添加number和color的觀察後,移除nunber和color的觀察:

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    
    [self.model sj_removeObserver:self forKeys:@[@"number",@"color"]];
}
複製代碼

在移除之後,咱們調用list方法,輸出:

SJKVOController[80901:4283311] SJKVOLog:There is no observers obserbing object:<Model: 0x600000220fa0>
複製代碼

如今color和number屬性都不被觀察了。看一下實際的效果:

顏色和數字的變化都再也不被觀察

咱們能夠看到,如今color和number都不變了,驗證此移除方法正確。

OK,如今知道了怎麼用SJKVOController,我下面給你們看一下代碼:

SJKVOController代碼解析

先大體講解一下SJKVOController的實現思路:

  1. 爲了減小侵入性,SJKVOController被設計爲NSObject的一個分類。
  2. SJKVOController仿照了KVO的實現思路,在添加觀察之後在運行時動態生成當前類的子類,給這個子類添加被觀察的屬性的set方法並使用isa swizzle的方式將當前對象轉換爲當前類的子類的實現。
  3. 同時,這個子類還使用了關聯對象來保存一個「觀察項」的set,每個觀察項封裝了一次觀察的行爲(有去重機制):包括觀察本身的對象,本身被觀察的屬性,以及傳進來的block。
  4. 在當前類,也就是子類的set方法被調用的時候作三件事情:
    • 第一件事情是使用KVC來找出當前屬性的舊值。
    • 第二件事情是調用父類(原來的類)的set方法(設新值)。
    • 第三件事是根據當前的觀察對象和key,在觀察項set裏面找出對應的block並調用。

再來看一下這個小輪子的幾個類:

  • SJKVOController:實現KVO主要功能的類。
  • SJKVOObserverItem:封裝觀察項的類。
  • SJKVOTool:setter和getter的相互轉換和相關運行時查詢方法等。
  • SJKVOError:封裝錯誤類型。
  • SJKVOHeader:引用了運行時的頭文件。

下面開始一個一個來說解每一個類的源碼:

SJKVOController

再看一下頭文件:

#import <Foundation/Foundation.h>
#import "SJKVOHeader.h"

@interface NSObject (SJKVOController)

//============== add observer ===============//
- (void)sj_addObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys withBlock:(SJKVOBlock)block;
- (void)sj_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(SJKVOBlock)block;


//============= remove observer =============//
- (void)sj_removeObserver:(NSObject *)observer forKeys:(NSArray <NSString *>*)keys;
- (void)sj_removeObserver:(NSObject *)observer forKey:(NSString *)key;
- (void)sj_removeObserver:(NSObject *)observer;
- (void)sj_removeAllObservers;

//============= list observers ===============//
- (void)sj_listAllObservers;

@end
複製代碼

每一個方法的意思相信讀者已經能看懂了,如今講一下具體的實現。從sj_addObserver:forKey withBlock:開始:

sj_addObserver:forKey withBlock:方法:

除去一些錯誤的判斷,該方法做了下面幾件事情:

1.判斷當前被觀察的類是否存在與傳入key對應的setter方法:

SEL setterSelector = NSSelectorFromString([SJKVOTool setterFromGetter:key]);
Method setterMethod = [SJKVOTool objc_methodFromClass:[self class] selector:setterSelector];
//error: no corresponding setter mothod
if (!setterMethod) {
     SJLog(@"%@",[SJKVOError errorNoMatchingSetterForKey:key]);
     return;
}
複製代碼

2. 若是有,判斷當前被觀察到類是否已是KVO類(在KVO機制中,若是某個對象一旦被觀察,則這個對象就變成了帶有包含KVO前綴的類的實例)。若是已是KVO類,則將當前實例的isa指針指向其父類(最開始被觀察的類):

//get original class(current class,may be KVO class)
    NSString *originalClassName = NSStringFromClass(OriginalClass);
    
    //若是當前的類是帶有KVO前綴的類(也就是已經被觀察到類),則須要將KVO前綴的類刪除,並講
    if ([originalClassName hasPrefix:SJKVOClassPrefix]) {
        //now,the OriginalClass is KVO class, we should destroy it and make new one
        Class CurrentKVOClass = OriginalClass;
        object_setClass(self, class_getSuperclass(OriginalClass));
        objc_disposeClassPair(CurrentKVOClass);
        originalClassName = [originalClassName substringFromIndex:(SJKVOClassPrefix.length)];
    }
複製代碼

3. 若是不是KVO類(說明當前實例沒有被觀察),則建立一個帶有KVO前綴的類,並將當前實例的isa指針指向這個新建的類:

//create a KVO class
    Class KVOClass = [self createKVOClassFromOriginalClassName:originalClassName];
    
    //swizzle isa from self to KVO class
    object_setClass(self, KVOClass);
複製代碼

看一下如何新建一個新的類:

- (Class)createKVOClassFromOriginalClassName:(NSString *)originalClassName
{
    NSString *kvoClassName = [SJKVOClassPrefix stringByAppendingString:originalClassName];
    Class KVOClass = NSClassFromString(kvoClassName);
    
    // KVO class already exists
    if (KVOClass) {
        return KVOClass;
    }
    
    // if there is no KVO class, then create one
    KVOClass = objc_allocateClassPair(OriginalClass, kvoClassName.UTF8String, 0);//OriginalClass is super class
    
    // pretending to be the original class:return the super class in class method
    Method clazzMethod = class_getInstanceMethod(OriginalClass, @selector(class));
    class_addMethod(KVOClass, @selector(class), (IMP)return_original_class, method_getTypeEncoding(clazzMethod));
    
    // finally, register this new KVO class
    objc_registerClassPair(KVOClass);
    
    return KVOClass;
}
複製代碼

4. 查看觀察項set,若是這個set裏面有已經保存的觀察項,則須要新建一個空的觀察項set,將已經保存的觀察項放入這個新建的set裏面:

//if we already have some history observer items, we should add them into new KVO class
    NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
    if (observers.count > 0) {
        
        NSMutableSet *newObservers = [[NSMutableSet alloc] initWithCapacity:5];
        objc_setAssociatedObject(self, &SJKVOObservers, newObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        
        for (SJKVOObserverItem *item in observers) {
            [self KVOConfigurationWithObserver:item.observer key:item.key block:item.block kvoClass:KVOClass setterSelector:item.setterSelector setterMethod:setterMethod];
        }    
    }
複製代碼

看一下如何保存觀察項的:

- (void)KVOConfigurationWithObserver:(NSObject *)observer key:(NSString *)key block:(SJKVOBlock)block kvoClass:(Class)kvoClass setterSelector:(SEL)setterSelector setterMethod:(Method)setterMethod
{
    //add setter method in KVO Class
    if(![SJKVOTool detectClass:OriginalClass hasSelector:setterSelector]){
        class_addMethod(kvoClass, setterSelector, (IMP)kvo_setter_implementation, method_getTypeEncoding(setterMethod));
    }
    
    //add item of this observer&&key pair
    [self addObserverItem:observer key:key setterSelector:setterSelector setterMethod:setterMethod block:block];
}
複製代碼

這裏首先給KVO類增長了setter方法:

//implementation of KVO setter method
void kvo_setter_implementation(id self, SEL _cmd, id newValue)
{
    
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = [SJKVOTool getterFromSetter:setterName];
    

    if (!getterName) {
        SJLog(@"%@",[SJKVOError errorTransferSetterToGetterFaildedWithSetterName:setterName]);
        return;
    }
    
    // create a super class of a specific instance
    Class superclass = class_getSuperclass(OriginalClass);
    
    struct objc_super superclass_to_call = {
        .super_class = superclass,  //super class
        .receiver = self,           //insatance of this class
    };
    
    // cast method pointer
    void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
    
    // call super's setter, the supper is the original class
    objc_msgSendSuperCasted(&superclass_to_call, _cmd, newValue);
    
    // look up observers and call the blocks
    NSMutableSet *observers = objc_getAssociatedObject(self,&SJKVOObservers);
    
    if (observers.count <= 0) {
        SJLog(@"%@",[SJKVOError errorNoObserverOfObject:self]);
        return;
    }
    
    //get the old value
    id oldValue = [self valueForKey:getterName];
    
    for (SJKVOObserverItem *item in observers) {
        if ([item.key isEqualToString:getterName]) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                //call block
                item.block(self, getterName, oldValue, newValue);
            });
        }
    }
}
複製代碼

而後實例化對應的觀察項:

- (void)addObserverItem:(NSObject *)observer
                    key:(NSString *)key
         setterSelector:(SEL)setterSelector
           setterMethod:(Method)setterMethod
                  block:(SJKVOBlock)block
{
    
    NSMutableSet *observers = objc_getAssociatedObject(self, &SJKVOObservers);
    if (!observers) {
        observers = [[NSMutableSet alloc] initWithCapacity:10];
        objc_setAssociatedObject(self, &SJKVOObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    SJKVOObserverItem *item = [[SJKVOObserverItem alloc] initWithObserver:observer Key:key setterSelector:setterSelector setterMethod:setterMethod block:block];
    
    if (item) {
        [observers addObject:item];
    }
    
}
複製代碼

5. 判斷新的觀察是否會與已經保存的觀察項重複(當觀察對象和key一致的時候),若是重複,則不添加新的觀察:

/ /ignore same observer and key:if the observer and key are same with saved observerItem,we should not add them one more time
    BOOL findSameObserverAndKey = NO;
    if (observers.count>0) {
        for (SJKVOObserverItem *item in observers) {
            if ( (item.observer == observer) && [item.key isEqualToString:key]) {
                findSameObserverAndKey = YES;
            }
        }
    }
    
    if (!findSameObserverAndKey) {
        [self KVOConfigurationWithObserver:observer key:key block:block kvoClass:KVOClass setterSelector:setterSelector setterMethod:setterMethod];
    }
複製代碼

而一次性添加多個key的方法,也只是調用屢次一次性添加單個key的方法罷了:

- (void)sj_addObserver:(NSObject *)observer
               forKeys:(NSArray <NSString *>*)keys
             withBlock:(SJKVOBlock)block
{
    //error: keys array is nil or no elements
    if (keys.count == 0) {
        SJLog(@"%@",[SJKVOError errorInvalidInputObservingKeys]);
        return;
    }
    
    //one key corresponding to one specific item, not the observer
    [keys enumerateObjectsUsingBlock:^(NSString * key, NSUInteger idx, BOOL * _Nonnull stop) {
        [self sj_addObserver:observer forKey:key withBlock:block];
    }];
}
複製代碼

關於移除觀察的實現,只是在觀察項set裏面找出封裝了對應的觀察對象和key的觀察項就能夠了:

- (void)sj_removeObserver:(NSObject *)observer
                   forKey:(NSString *)key
{
    NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
    
    if (observers.count > 0) {
        
        SJKVOObserverItem *removingItem = nil;
        for (SJKVOObserverItem* item in observers) {
            if (item.observer == observer && [item.key isEqualToString:key]) {
                removingItem = item;
                break;
            }
        }
        if (removingItem) {
            [observers removeObject:removingItem];
        }
        
    }
}
複製代碼

再看一下移除全部觀察者:

- (void)sj_removeAllObservers
{
    NSMutableSet* observers = objc_getAssociatedObject(self, &SJKVOObservers);
    
    if (observers.count > 0) {
        [observers removeAllObjects];
        SJLog(@"SJKVOLog:Removed all obserbing objects of object:%@",self);
        
    }else{
        SJLog(@"SJKVOLog:There is no observers obserbing object:%@",self);
    }
}
複製代碼

SJKVOObserverItem

這個類負責封裝每個觀察項的信息,包括:

  • 觀察者對象。
  • 被觀察的key。
  • setter方法名(SEL)
  • setter方法(Method)
  • 回調的block

須要注意的是: 在這個小輪子裏,對於同一個對象能夠觀察不一樣的key的狀況,是將這兩個key區分開來的,是屬於不一樣的觀察項。因此應該用不一樣的SJKVOObserverItem實例來封裝。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

typedef void(^SJKVOBlock)(id observedObject, NSString *key, id oldValue, id newValue);

@interface SJKVOObserverItem : NSObject

@property (nonatomic, strong) NSObject *observer;
@property (nonatomic, copy)   NSString *key;
@property (nonatomic, assign) SEL setterSelector;
@property (nonatomic, assign) Method setterMethod;
@property (nonatomic, copy)   SJKVOBlock block;

- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key setterSelector:(SEL)setterSelector setterMethod:(Method)setterMethod block:(SJKVOBlock)block;

@end

複製代碼

SJKVOTool

這個類負責setter方法與getter方法相互轉換,以及和運行時相關的操做,服務於SJKVOController。看一下它的頭文件:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>

@interface SJKVOTool : NSObject

//setter <-> getter
+ (NSString *)getterFromSetter:(NSString *)setter;
+ (NSString *)setterFromGetter:(NSString *)getter;

//get method from a class by a specific selector
+ (Method)objc_methodFromClass:(Class)cls selector:(SEL)selector;

//check a class has a specific selector or not
+ (BOOL)detectClass:(Class)cls hasSelector:(SEL)selector;

@end
複製代碼

##SJKVOError

這個小輪子仿照了JSONModel的錯誤管理方式,用單獨的一個類SJKVOError來返回各類錯誤:

#import <Foundation/Foundation.h>

typedef enum : NSUInteger {
    
    SJKVOErrorTypeNoObervingObject,
    SJKVOErrorTypeNoObervingKey,
    SJKVOErrorTypeNoObserverOfObject,
    SJKVOErrorTypeNoMatchingSetterForKey,
    SJKVOErrorTypeTransferSetterToGetterFailded,
    SJKVOErrorTypeInvalidInputObservingKeys,
    
} SJKVOErrorTypes;

@interface SJKVOError : NSError

+ (id)errorNoObervingObject;
+ (id)errorNoObervingKey;
+ (id)errorNoMatchingSetterForKey:(NSString *)key;
+ (id)errorTransferSetterToGetterFaildedWithSetterName:(NSString *)setterName;
+ (id)errorNoObserverOfObject:(id)object;
+ (id)errorInvalidInputObservingKeys;

@end
複製代碼

OK,這樣就介紹完了,但願各位同窗能夠積極指正~

本篇已同步到我的博客:使用Block實現KVO

---------------------------- 2018年7月17日更新 ----------------------------

注意注意!!!

筆者在近期開通了我的公衆號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者之前發佈的精選技術文章,以及後續發佈的技術文章(以原創爲主),而且逐漸脫離 iOS 的內容,將側重點會轉移到提升編程能力的方向上。
  • 讀書筆記類文章:分享編程類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

由於公衆號天天發佈的消息數有限制,因此到目前爲止尚未將全部過去的精選文章都發布在公衆號上,後續會逐步發佈的。

並且由於各大博客平臺的各類限制,後面還會在公衆號上發佈一些短小精幹,以小見大的乾貨文章哦~

掃下方的公衆號二維碼並點擊關注,期待與您的共同成長~

公衆號:程序員維他命
相關文章
相關標籤/搜索