使用 Block 實現 KVO

J_Knight_發表於2019-03-04

在iOS開發中,我們可以通過KVO機制來監聽某個物件的某個屬性的變化。

用過KVO的同學都應該知道,KVO的回撥是以代理的形式實現的:在給某個物件新增觀察以後,需要在另外一個地方實現回撥代理方法。這種設計給人感覺比較分散,因此突然想試試用Block來實現KVO,將新增觀察的程式碼和回撥處理的程式碼寫在一起。在學習了ImplementKVO的實現以後,自己也寫了一個:SJKVOController

SJKVOController的用法

只需要引入NSObject+SJKVOController.h標頭檔案就可以使用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
複製程式碼

從上面的API可以看出,這個小輪子:

  1. 支援一次觀察同一物件的多個屬性。
  2. 可以一次只觀察一個物件的一個屬性。
  3. 可以移除對某個物件對多個屬性的觀察。
  4. 可以移除對某個物件對某個屬性的觀察。
  5. 可以移除某個觀察自己的物件。
  6. 可以移除所有觀察自己的物件。
  7. 列印出所有觀察自己的物件的資訊,包括物件本身,觀察的屬性,setter方法。

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

在點選上面兩個按鈕中的任意一個,增加觀察:

一次性新增:

- (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;
        });
        
    }];
    
}
複製程式碼

新增以後,點選最下面的按鈕來顯示所有的觀察資訊:

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

輸出:

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 的內容,將側重點會轉移到提高程式設計能力的方向上。
  • 讀書筆記類文章:分享程式設計類思考類心理類職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術上生活上的思考。

因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。

而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~

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

公眾號:程式設計師維他命

相關文章