Implementing KVO with Block

In iOS development, we can monitor the change of an object's attributes through KVO mechanism.

Students who have used KVO should know that KVO callbacks are implemented in the form of proxy: after adding observations to an object, callback proxy methods need to be implemented in another place. This design feels scattered, so suddenly I want to try to implement KVO with Block, and write the code that adds observation and callback processing together. Learning ImplementKVO After the realization, I also wrote one: SJKVOController

Usage of SJKVOController

SJKVOController can be used by simply introducing NSObject+SJKVOController.h header file.
First 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

As can be seen from the API above, this small wheel:
1. Supports the observation of multiple attributes of the same object at one time.
2. It is possible to observe only one property of an object at a time.
3. The observation of multiple attributes on an object can be removed.
4. The observation of an object to an attribute can be removed.
5. You can remove an object that observes you.
6. You can remove all objects that observe you.
6. Print out all the information about observing your own object, including the object itself, the attributes of the observation, and the setter method.

Here's how to use this wheel in conjunction with Demo.

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

One-time addition:

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

    }];
}

Add two times:

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

    }];

}

After adding, click the bottom button to display all the observation information:

- (IBAction)showAllObservingItems:(UIButton *)sender {

    [self.model sj_listAllObservers];
}

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:}

Here I rewrote the description method, printing out each object and key observed, as well as the setter method.

Now click the update button to update the number and color attributes of the model, 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];
}

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

Now let's remove the observation and click the remove button

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

When all observers are removed, they are printed out:

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

And if you print the list of observers at this time, it will output:

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

It should be noted that there are many options for removal: you can remove a key of an object, or you can remove several keys of an object. To verify, we can use the list method to verify whether the removal is successful:

Verification 1: After adding number s and color observations, remove nunber observations:

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

After removal, we call the list method and output:

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

Now only the color attribute is observed. Take a look at the actual effect:

We can see that only the color is changing now, and the number is not changing. Verify that the 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"]];
}

After removal, we call the list method and output:

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

Now neither color nor number attributes are observed. Take a look at the actual effect:

We can see that now both color and number are unchanged, verifying that the removal method is correct.

OK, now you know how to use SJKVOController. Let me show you the code below.

SJKVOController Code Parsing

Firstly, the realization of SJKVOController is outlined.
1. To reduce invasiveness, SJKVOController is designed as a classification of NSObject.
2. SJKVOController imitates the idea of KVO implementation. After adding observations, it dynamically generates subclasses of the current class at runtime, adds set methods of observed attributes to this subclass, and converts the current object into the implementation of subclasses of the current class by using isa switch.
3. At the same time, this subclass also uses related objects to save a set of "observation items", each of which encapsulates the behavior of one observation (with a de-duplication mechanism): including observing its own objects, its observed attributes, and the incoming block.
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 property.
- The second thing is to call the set method (set a new value) of the parent class (the original class).
- The third thing is to find the corresponding block in the observation set and call it based on the current observation object and key.

Let's look at some of the small wheels.

  • SJKVOController: A class that implements the main functions of KVO.
  • SJKVOObserver Item: A class that encapsulates observations.
  • SJKVOTool: setter and getter conversion and related runtime query methods.
  • SJKVOError: Encapsulation error type.
  • SJKVOHeader: Refers to the runtime header file.

    Let's start by explaining the source code for each class one by one.

SJKVOController

Look at the header file again:

#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

The meaning of each method is believed to be understood by the reader. Now let's talk about the concrete implementation. Start with sj_addObserver: for Key with Block:

Sj_addObserver: forKey with Block: Method:

In addition to some erroneous judgments, the method does the following things:

1. Determine whether the currently observed class has a setter method corresponding to the incoming 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;
}

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 KVO prefix). If it is already a KVO class, point the isa pointer of the current instance to its parent class (the class initially 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), you need to delete the class with a 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)];
    }

3. If it is not a KVO class (indicating that the current instance is not observed), create a class with a KVO prefix and point the isa pointer of the current instance to the new class:

    //create a KVO class
    Class KVOClass = [self createKVOClassFromOriginalClassName:originalClassName];

    //swizzle isa from self to KVO class
    object_setClass(self, KVOClass);

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;
}

4. Look at the observation set. If there are saved observations in the set, you need to create an empty observation set and put the saved observations 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) {
            [self KVOConfigurationWithObserver:item.observer key:item.key block:item.block kvoClass:KVOClass setterSelector:item.setterSelector setterMethod:setterMethod];
        }    
    }

See how to save the 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];
}

Firstly, the setter method is added 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);
            });
        }
    }
}

Then the corresponding observation item is instantiated:

- (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. Determine whether the new observation will repeat with the saved observation (when the object and key are identical), and if it repeats, 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];
    }

The method of adding multiple keys at one time only calls the method of adding a single key at one time many times.

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

With regard to the implementation of removing observations, only the observation items encapsulating the corresponding observation objects and key s are found in the observation item set.

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

    }
}

Look again 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);
    }
}

SJKVOObserverItem

This class is responsible for encapsulating information for each observation item, including:
- Observers.
- The observed key.
- setter method name (SEL)
- setter Method
- Callback block

It should be noted that:
In this small wheel, the case that different keys can be observed for the same object is to distinguish the two keys, which belong to different observation items. So different SJKVOObserver Item instances should be encapsulated.

#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

This class serves SJKVOController by converting setter methods to getter methods and by performing runtime-related operations. Look at its header file:

#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

The little wheel imitated it. JSONModel Error management, using a separate 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

OK, this is the end of the introduction. I hope you can make positive corrections.

This article has been synchronized to personal blog: Implementing KVO with Block

Keywords: Attribute iOS

Added by moriman on Mon, 01 Jul 2019 03:00:13 +0300