Implementing KVO using Block

In iOS development, we can monitor the changes in an attribute of an object through the KVO mechanism.

Students who have used KVO should know that the callback for KVO is implemented as an agent: after adding an object to an object, the callback agent method needs to be implemented in another place. This design is more diffuse, so you suddenly want to try using Block to implement KVO, writing code that adds the observed code to the callback process. After learning the implementation of ImplementKVO, I wrote a: SJKVOController

Implementing KVO using Block
uses Block to implement KVO

The use of SJKVOController

You can use SJKVOController only by introducing the NSObject+SJKVOController.h header file.
, look at its header file first:

#import < Foundation/Foundation.h> #import "SJKVOHeader.h" @interface NSObject add observer (SJKVOController) //============== ===============// - (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 you can see from the above API, this small wheel supports multiple properties of the same object at a time. You can observe only one property of an object at a time. You can remove the observation of multiple attributes of an object. You can remove the observation of an object against an attribute. You can remove an object that looks at yourself. You can remove all objects that you see yourself. Print out all the information that looks at your object, including the object itself, the properties of the observation, and the setter method.

Here’s how to use this little wheel in conjunction with Demo:

Increase the observation by clicking either of the two buttons above:

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

Added 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:@ withBlock:^ ID observedObject ("color" NSString, *key, ID oldValue, ID newValue) {dispatch_async (dispatch_get_main_queue), ^{(self.numberLabel.backgroundColor = newValue;}});}];

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

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

Here, I rewrote the description method, print out each object and key, and the setter method.

Now click the update button to update the model’s number and color properties to trigger KVO:

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

We can see the Label in the center. The numbers and background colors shown above are changing, and KVO has been successfully implemented:

Implementing KVO using Block
also looks at changes in color and numbers

Now we remove the observation and click the remove button

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

After all the observers have been removed, they are printed out:

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

Also, if you print the observer list at this time, it will output:

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

Note that here you have many options: remove a key can move an object, can also remove a few keys, in order to verify the object, we can verify whether successful removal combined with list method:

Verify that 1: removes the observation of nunber after adding number and color:

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

Only the color attribute is now observed. Take a look at the actual effect:

Implementing KVO using Block
only looks at changes in color

We can see that only color is changing now, and the numbers have not changed. Verify that the removal method is correct.

Verify that 2: removes the observation of nunber and color after the addition of number and color:

- (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>

Neither the color nor the number attribute is now observed. Take a look at the actual effect:

The changes in color and number of Implementing KVO using Block,
, are no longer observed

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

OK, now that you know how to use SJKVOController, I’ll show you the code below:

SJKVOController code parsing

First of all explain the implementation of SJKVOController ideas:

  1. To reduce invasiveness, SJKVOController has been designed as a classification of NSObject.
  2. SJKVOController modeled on the realization way of the KVO, in addition to observe dynamically at runtime to generate the current class, the set method was observed to add the attribute of this subclass and use the ISA Swizzle method will be converted to the current object class implementation.
  3. At the same time, the sub class use the associated object to hold a “observation” of the set, every observation package an observed behavior (to heavy mechanism): including their own observation object, attribute the observed, and block came in.
  4. In the current class, that is, when the set method of the subclass is called, do three things: 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 (new value) of the parent class (the original class). The third thing is to find the corresponding block and call in the observation item set, based on the current object of observation and key.

Let’s look at some of the smaller wheels:

  • SJKVOController: classes that implement the main functions of KVO.
  • SJKVOObserverItem: class that encapsulates observation items.
  • SJKVOTool:setter and getter conversion and related run-time query methods.
  • SJKVOError: package error type.
  • SJKVOHeader: referenced the header file of the runtime.

Start the next one to explain the source code for each class:


Look at the file again:

#import < Foundation/Foundation.h> #import "SJKVOHeader.h" @interface NSObject add observer (SJKVOController) //============== ===============// - (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 that the reader can already understand, and now talk about the specific implementation. Start with sj_addObserver:forKey withBlock:

Sj_addObserver:forKey withBlock: method:

Apart from some erroneous judgment, the method makes the following things:

1. determine whether the class currently being observed exists the 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 class being observed is already a KVO class (in the KVO mechanism, if an object is observed, then the object becomes an instance of the class with the KVO prefix). If it is already a KVO class, point the ISA pointer of the current instance to its parent class (the class that was first observed):

//get original class (current class, may be KVO class NSString *originalClassName) = NSStringFromClass (OriginalClass); / / if the class is prefixed with KVO class (which has been observed to be KVO class), prefix class deletes, and 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 the KVO class (indicating that the current instance has not been observed), create a class with the 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 build 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 to be the original / / pretending 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)); Register this new / / finally, KVO class objc_registerClassPair (KVOClass); return KVOClass;}

4. look at the item “set”. If there are already saved observation items in this set, you need to create a new empty item “set” to put the saved item in 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: key:item.key block:item.block) {kvoClass:KVOClass setterSelector:item.setterSelector setterMethod:setterMethod]}};

Look at how to save the observation items:

- (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, kvo_setter_implementation, method_getTypeEncoding ((IMP) setterMethod));} //add item of this observer& & amp key pair [self; addObserverItem:observer key:key; setterSelector:setterSelector setterMethod:setterMethod block:block];}

Here first 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_msgSendS) Uper; / / 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 (observers.count); if < = 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, newValu (E)) (});}

The corresponding observation term is then 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! = [[NSMutableSet) {observers 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 be repeated with the observed item (when the object is consistent with the key) and, if repeated, do not add new observations:

/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 ( = = observer) & & [item.key; isEqualToString:key]) {findSameObserverAndKey = YES;}}} (if! FindSameObserverAndKey) {[self KVOConfigurationWithObserver:observer key:key block:block kvoClass:KVOClass setterSelector: setterSelector setterMethod:setterMethod];}

And the method of adding multiple key at once only calls the method of adding single key multiple times:

- (void) sj_addObserver: (NSObject *) observer forKeys: (NSArray < NSString *> *) keys withBlock: (SJKVOBlock) block keys array is nil {//error: 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 * _Nonnull IDX, BOOL stop) {[self sj_addObserver:observer forKey:key}}]; withBlock:block];

With regard to removing the implementation of the observation, it is only possible to find an observation object in the observation item set that encapsulates the corresponding observation object and the 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 ( = = observer & & [item.key; isEqualToString:key]) {removingItem = item; break; if (removingItem)}}}}} {[observers removeObject:removingItem];

Take a look at removing all observers:

- (void) sj_removeAllObservers NSMutableSet* objc_getAssociatedObject (observers = {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);}}


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

  • Observer object.
  • The observed key.
  • Setter method name (SEL)
  • Setter method (Method)
  • Callback block

It should be noted that
in this small wheel can observe different key for the same object, which distinguishes the two key and belongs to different observation items. Therefore, different SJKVOObserverItem instances should be used to encapsulate.

#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 (nonatomic, setterSelector; @property assign setterMethod @property (Method); SJKVOBlock block; nonatomic, copy) - (instancetype) initWithObserver: (NSObject * observer) Key: (NSString *) key setterSelector: (SEL) setterSelector setterMethod: (Method) setterMethod block: (SJKVOBlock) block; @end


This class is responsible for the conversion of the setter method to the getter method, as well as the operation associated with the runtime, and serves the SJKVOController. Take a 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


The small wheel, modeled on the error management of JSONModel, returns a variety of errors with a single class 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, that’s all for now. I hope all of you can make a positive correction

This article has been synchronized to the personal blog: using Block to implement KVO

This article has been filed on copyright record. If you want to reprint, please visit copyright print. Forty-eight million four hundred and twenty-two thousand nine hundred and twenty-eight

Get authorization