In iOS development, we can use the KVO mechanism to listen for changes in a property of an object.

Those of you who have used KVO should know that callbacks in KVO are implemented as proxies: after observations are added to an object, the callback proxy method needs to be implemented somewhere else. This design felt scattered, so I suddenly decided to try implementing KVO with blocks, writing the code to add observations together with the code to handle callbacks. After learning the implementation of the ImplementKVO, I wrote one myself: SJKVOController

The use of the SJKVOController

To use SJKVOController, just import the NSObject+ sjkvoController.h header file. Take a look at its header file:

#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
Copy the code

As you can see from the API above, this little wheel:

  1. Supports viewing multiple properties of the same object at once.
  2. You can observe only one property of an object at a time.
  3. You can remove observations of multiple properties of an object.
  4. You can remove observations of an object on a property.
  5. You can remove an object that you are observing.
  6. You can remove all objects that are observing you.
  7. Print out all information about the object you are observing, including the object itself, the properties you are observing, and setter methods.

Here’s a Demo of how to use this little wheel:

Click on either of the above two buttons to add observation:

Add at once:

- (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; }); }}]; }Copy the code

Add in two batches:

- (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;
        });
        
    }];
    
}
Copy the code

After adding, click the bottom button to display all the observations:

- (IBAction)showAllObservingItems:(UIButton *)sender {
    
    [self.model sj_listAllObservers];
}
Copy the code

Output:

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:}
Copy the code

Here I override the description method to print out the object and key for each observation, as well as the setter method.

Clicking the Update button now updates the Model’s number and color properties, triggering 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];
}
Copy the code

We can see that the number and background color displayed on the middle Label are changing, which successfully implements KVO:

Now to remove the observation, click the Remove button

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    [self.model sj_removeAllObservers];   
}
Copy the code

After removing all observers, it prints:

SJKVOController[80499:4242749] SJKVOLog:Removed all obserbing objects of object:<Model: 0x60000003b700>
Copy the code

And if you print the observer list at this point, it will print:

SJKVOController[80499:4242749] SJKVOLog:There is no observers obserbing object:<Model: 0x60000003b700>
Copy the code

Note that there are several options for removing a key from an object, as well as several keys from an object. To verify this, we can use the list method to verify that the removal is successful:

Verification 1: Remove Nunber’s observation after adding number and color observations:

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    [self.model sj_removeObserver:self forKey:@"number"];
}
Copy the code

After removing, we call the list method, which prints:

SJKVOController[80850:4278383] SJKVOLog:==================== Start Listing All Observers: ====================
SJKVOController[80850:4278383] SJKVOLog:observer item:{observer: <ViewController: 0x7ffeec408560> | key: color | setter: setColor:}
Copy the code

Now only the color property is observed. Take a look at this in action:

We can see that only the color is changing, but the number is not. Verify that this removal method is correct.

Verification 2: After adding number and color observations, remove nunber and color observations:

- (IBAction)removeAllObservingItems:(UIButton *)sender {
    
    [self.model sj_removeObserver:self forKeys:@[@"number".@"color"]];
}
Copy the code

After removing, we call the list method, which prints:

SJKVOController[80901:4283311] SJKVOLog:There is no observers obserbing object:<Model: 0x600000220fa0>
Copy the code

Now the color and number attributes are not observed. Take a look at this in action:

As you can see, both color and number are now unchanged. Verify that this removal method is correct.

OK, now that we know how to use SJKVOController, let me show you the code:

SJKVOController code parsing

First of all, roughly explain the implementation of SJKVOController:

  1. To make it less intrusive, SJKVOController is designed as a class of NSObject.
  2. SJKVOController mimics the implementation idea of KVO. After adding observation, it dynamically generates the subclass of the current class at runtime, adds the set method of observed attributes to this subclass, and uses the way of Isa Swizzle to convert the current object into the subclass of the current class.
  3. The subclass also uses associative objects to hold a set of “observations,” each of which encapsulates the behavior of an observation (with a de-duplication mechanism) : the observation of its own object, its observed properties, and the block passed in.
  4. Do three things when the set method of the current class, that is, the subclass, is called:
    • The first thing is to use KVC to find the old value of the current attribute.
    • The second thing is to call the set method of the parent (the original class) (set the new value).
    • The third thing is to find the corresponding block in the set of observations based on the current observation object and key and call it.

Let’s take a look at some of the classes of this little wheel:

  • SJKVOController: a class that implements the main functions of KVO.
  • SJKVOObserverItem: Class that encapsulates observations.
  • SJKVOTool: setter and getter interconversion and related runtime query methods.
  • SJKVOError: indicates the encapsulation error type.
  • SJKVOHeader: A header file that references the runtime.

Let’s start by explaining the source of each class one by one:

SJKVOController

Take another look at the header file:

#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
Copy the code

The meaning of each method is understood by the reader, so let’s talk about the implementation. Start with sj_addObserver:forKey withBlock:

Sj_addObserver :forKey withBlock:

Aside from some bad judgments, the method does the following:

1. Determine whether the currently observed class has a setter method corresponding to the passed key:

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;
}
Copy the code

2. If so, determine whether the currently observed class is already a KVO class (in the KVO mechanism, once an object is observed, it becomes an instance of a class with a prefix containing KVO). If it is already a KVO class, then the isa pointer to the current instance points to its parent (the class that was first observed) :

    //get original class(current class,may be KVO class)
    NSString *originalClassName = NSStringFromClass(OriginalClass);
    
    // If the current class is a class with a KVO prefix (that is, a class that has been observed), then you need to remove the KVO prefix and say
    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)];
    }
Copy the code

3. If it is not a KVO class (meaning the current instance is not being observed), create a class with the prefix KVO and point the isa pointer to the current instance to the newly created class:

    //create a KVO class
    Class KVOClass = [self createKVOClassFromOriginalClassName:originalClassName];
    
    //swizzle isa from self to KVO class
    object_setClass(self, KVOClass);
Copy the code

Let’s see how to create a new class:

- (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;
}
Copy the code

4. Check the observation item set. If the observation item in the set has been saved, you need to create an empty observation item set and put the saved observation item into the new 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) {
            [selfKVOConfigurationWithObserver:item.observer key:item.key block:item.block kvoClass:KVOClass setterSelector:item.setterSelector setterMethod:setterMethod]; }}Copy the code

Take a look at how to save observations:

- (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];
}
Copy the code

Here we first add setter methods to the KVO class:

//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); }); }}}Copy the code

Then instantiate the corresponding observation item:

- (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]; }}Copy the code

5. Determine whether the new observation will be repeated with the saved observation item (when the observation object and key are the same). If the observation object is repeated, no new observation will be added:

    / /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];
    }
Copy the code

A method that adds multiple keys at once is just a call to the method that adds a single key at once:

- (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];
    }];
}
Copy the code

To remove the observation implementation, just find the observation item in the set that encapsulates the corresponding observation object and 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]; }}}Copy the code

Take another look at removing all observers:

- (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); }}Copy the code

SJKVOObserverItem

This class is responsible for encapsulating information about each observation, including:

  • The observer object.
  • The key being observed.
  • Setter method name (SEL)
  • Setter Method (Method)
  • The callback block

It should be noted that in this small wheel, different keys can be observed for the same object, which distinguishes the two keys and belongs to different observation items. So it should be wrapped in a different instance of 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

Copy the code

SJKVOTool

This class is responsible for converting setter and getter methods, as well as runtime operations, and serves SJKVOController. Take a look at its header:

#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
Copy the code

##SJKVOError

The little wheel mimics JSONModel error management, with a single class, SJKVOError, to return various errors:

#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
Copy the code

OK, so the introduction is over, I hope you can actively correct ~

This post has been synchronized to my blog: KVO with Block

— — — — — — — — — — — — — — — — — — — — — — — — — — — — on July 17, 2018 update — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Pay attention!!

The author recently opened a personal public account, mainly to share programming, reading notes, thinking articles.

  • Programming articles: including selected technical articles published by the author before, and subsequent technical articles (mainly original), and gradually away from iOS content, will shift the focus to improve the direction of programming ability.
  • Reading notes: Share reading notes on programming, thinking, psychology, and career books.
  • Thinking article: to share the author’s thinking on technology and life.

Because the number of messages released by the official account has a limit, so far not all the selected articles in the past have been published on the official account, and will be released gradually.

And because of the various restrictions of the major blog platform, the back will also be released on the public number of some short and concise, to see the big dry goods article oh ~

Scan the qr code of the official account below and click follow, looking forward to growing with you